Skip to content

FloatingActionButton

The one action a screen is about, as a round button floating over it: a FAB. Give it FloatingActions as children and it becomes a small set that fans out when it is pressed.

tsx
import { FloatingActionButton } from 'neba';

<FloatingActionButton icon={<PencilIcon />} label="Compose" onClick={compose} />;

Props

PropTypeDefaultDescription
variantshared'solid' | 'outline' | 'text''solid'Weight of the surface: filled, hairline, or none
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''lg'Height. The same ladder a Button is on, started a step up: this is the one control that has to be found and hit with a thumb without being looked at
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'Padding only: never the height, never the type scale
elevationshared0 | 1 | 2 | 32Drop shadow depth. 2 for the reason Pill’s is: this button is not part of the page, it hovers over it
iconReactNodea plusThe glyph on the button
label * stringWhat the button does, in words. Required: a button whose whole label is a drawing has no accessible name at all. With extended it is also the word written on it
extendedbooleanfalseWrites label beside the glyph, which turns the disc into a stadium
openIconReactNodeThe glyph while the dial is open. Defaults to a × when the button has actions; pass the same node as icon to keep it unchanged
position'static' | 'sticky' | 'fixed' | 'absolute''fixed'How it sits. fixed pins it to a corner of the window; absolute pins it to a corner of the nearest positioned ancestor
cornershared'top-start' | 'top-end' | 'bottom-start' | 'bottom-end''bottom-end'Which corner it is pinned to
offsetnumber | string16How far in from both edges, as a CSS length or a number of pixels
direction'top' | 'bottom'Which way the actions fan out. Taken from corner when it is left out
openbooleanWhether the dial is open. Use with onOpenChange for a controlled dial
defaultOpenbooleanfalseWhere an uncontrolled dial starts
onOpenChange(open: boolean) => voidCalled when the dial opens or closes
openOnHoverbooleantrueOpens the dial when a mouse comes to rest on the button. Touch and pen are excluded
closeOnActionbooleantrueCloses the dial when one of the actions is pressed
showLabelsbooleantrueDraws each action’s name on a lozenge beside it. Turned off, the names are still read out
disabledbooleanfalseUnavailable. The button and every action stop answering
onClickMouseEventHandler<HTMLButtonElement>Fires when the button is pressed. It still fires when the button has actions, where the press also opens and closes the dial
childrenReactNodeThe FloatingActions, if there are any
PropTypeDefaultDescription
iconReactNodeThe glyph
label * stringWhat the action does, in words. Drawn beside it, and always read out
disabledbooleanfalseUnavailable, but still part of the dial
onClickMouseEventHandler<HTMLButtonElement>Fires when the action is pressed

Every other <div> attribute passes through to the root and every other <button> attribute to each action. onClick belongs to the button itself.

The shared axes (variant size color density elevation corner) are defined in prop conventions.

Examples

Fanning out actions

Give it FloatingAction children and the button becomes a dial: pressing it (or resting a mouse on it) fans the actions out and swaps the glyph for a ×. Each action's name is drawn on a lozenge beside it.

closeOnAction decides whether the dial goes away once an action is pressed, showLabels whether the names are drawn, and openOnHover whether a resting mouse opens it.

extended

extended writes label beside the glyph, which turns the disc into a stadium: the shape to use when a drawing alone does not say what the screen is for. label is the accessible name either way, so the word that is drawn and the word that is read can never differ.

position, corner, offset

position defaults to fixed, which pins the button to a corner of the window. absolute pins it to a corner of the nearest positioned ancestor (a card, a map, the screen of a Mockup), and static puts it back in the flow.

corner picks one of the four corners and offset is the distance from both edges. Which way the actions fan out follows from corner, and direction overrides it.

variant, size, color

The button itself is a Button, unchanged: the variants, the elevation ladder, the pointer light and the press behaviour are all the ones every other control has. Only size starts a step higher, at lg, because this is the one control that has to be found and hit with a thumb without being looked at. The actions are drawn a step back down.

Controlling it

Pass open and the dial keeps no state of its own.

tsx
const [open, setOpen] = useState(false);

<FloatingActionButton label="Share" open={open} onOpenChange={setOpen}>
  <FloatingAction icon={<LinkIcon />} label="Copy link" />
</FloatingActionButton>;

Accessibility

  • label is required. A button whose whole label is a drawing has no accessible name at all.
  • With actions, the button carries aria-expanded and an aria-controls pointing at the set it revealed. It is not a role="menu": a menu promises one tab stop for the set, arrow keys within it and typeahead, and what wants those is Menu.
  • The actions are ordinary buttons in the tab order right after the one that revealed them.
  • Escape closes the dial and hands the focus back to the button. A press outside closes it too.
  • The lozenge beside an action is aria-hidden, because the same string is already the button's accessible name and would otherwise be announced twice.

Released under the MIT License