Fab
The floating action button, built out of parts that already exist. A circular accent trigger hangs off a viewport corner, and what it opens is a Dropdown whose rows are the actions. Three layouts share that one DOM: the menu surface as shipped, a speed-dial stack of labelled circles, and a flower that fans the actions on an arc around the trigger — the second two are pure CSS over the same rows. Fab adds the fixed corner, the circle, the corner-to-placement mapping, an optional scrim, the plus rotating into a cross, and the two attributes the laid-out variants key on. Everything else is Dropdown's, and this component holds no state, no hook and no handler of its own.
- Stable
- React 19
- Built on Dropdown
- Zero hooks, zero handlers
Closed, then open. The whole open state of the skin is one attribute — the aria-expanded Dropdown writes on the trigger.
Code
const ITEMS: DropdownItem[] = [{ kind: 'group', label: 'Crear' },{ kind: 'item', label: 'Nota', icon: <NoteIcon />, shortcut: '⌘N', onSelect: newNote },{ kind: 'item', label: 'Foto', icon: <PhotoIcon />, shortcut: '⌘⇧F', onSelect: newPhoto },];
<Fab items={ITEMS} triggerLabel="Acciones rápidas" /><div class="wayra-fab" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="Acciones rápidas" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div>Two required props and nothing else. items is the content — the same
DropdownItem union Dropdown
documents, unchanged — and triggerLabel is the accessible name of the circle,
required text rather than a default so the button is translated by
construction. There is no trigger to pass and no surface to place: the corner
picks the placement, and mounting the component anywhere in the tree docks it.
It composes Dropdown, whole
Section titled “It composes Dropdown, whole”fab.tsx has no useState, no useEffect and no event handler. It renders a
<div>, an optional scrim <span>, and a Dropdown whose trigger is the
circular <button>. Everything in this table therefore behaves exactly as
Dropdown documents it, because it is Dropdown:
| What | Where it comes from |
|---|---|
| The top layer, and the flip when there is no room | Popover, under Dropdown |
| Anchor positioning against the trigger | Popover |
| Light dismiss, a click outside | the popover attribute |
role="menu", the rows, the roving tabindex |
Dropdown |
Arrow keys, Home / End, typeahead, focus return |
Dropdown |
| Escape, from the row that took the focus on opening | Dropdown, which stops it before the browser’s own close request |
aria-haspopup, aria-expanded, aria-controls on the trigger |
Dropdown, cloned onto it |
| Closing after a row runs | Dropdown |
What Fab adds is the skin and the geometry — and the skin’s open state is read straight off the attribute Dropdown already writes:
.wayra-fab:has(.wayra-fab-trigger[aria-expanded="true"]) .wayra-fab-scrim { … }.wayra-fab-trigger[aria-expanded="true"] > svg { rotate: 135deg; }There is no data-open on the root. aria-expanded is already the truth of
the open state, it is already on the element that changes, and :has() is what
lets a parent read it — so the rotation, the neutral open fill and the scrim all
cost zero JavaScript, and hand-written markup gets them by writing one
attribute.
Corners
Section titled “Corners”Four corners in one stage. Each circle is a separate Fab; the stage is the demo's box, not the component's.
Code
<Fab position="bottom-end" items={ITEMS} triggerLabel="…" /> {/* default */}<Fab position="bottom-start" items={ITEMS} triggerLabel="…" /><Fab position="top-end" items={ITEMS} triggerLabel="…" /><Fab position="top-start" items={ITEMS} triggerLabel="…" /><div style="position: relative; flex: 1 1 100%; inline-size: 100%; block-size: 12rem; overflow: clip; border-radius: 0.75rem; box-shadow: inset 0 0 0 1px var(--wayra-color-base-300)"> <div class="wayra-fab" style="position: absolute"> <button type="button" class="wayra-fab-trigger" aria-label="bottom-end" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" data-position="bottom-start" style="position: absolute"> <button type="button" class="wayra-fab-trigger" aria-label="bottom-start" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" data-position="top-end" style="position: absolute"> <button type="button" class="wayra-fab-trigger" aria-label="top-end" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" data-position="top-start" style="position: absolute"> <button type="button" class="wayra-fab-trigger" aria-label="top-start" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div></div>position docks the button and decides where the menu goes, in one prop:
the menu opens away from the block edge the button is nearest, and lines up with
the same inline edge the button hangs off.
position |
Menu placement |
Menu align |
|---|---|---|
bottom-end (default) |
block-start — opens upward |
end |
bottom-start |
block-start — opens upward |
start |
top-end |
block-end — opens downward |
end |
top-start |
block-end — opens downward |
start |
Both halves are logical. The inline half follows the writing direction, so
bottom-end is the bottom-right corner under dir="ltr" and the bottom-left
under dir="rtl", with no second rule and no mirrored stylesheet — the whole
mapping is two CSS rules, each flipping one axis, and the default corner needs
no rule at all. data-position is always written, bottom-end included;
markup written by hand may leave it off, and a missing attribute reads as
bottom-end.
position="custom" — no corner at all
Section titled “position="custom" — no corner at all”<div className="my-toolbar"> <Fab position="custom" placement="block-start" align="end" items={ITEMS} triggerLabel="Acciones rápidas" /></div>The fifth value opts out of the viewport entirely: the root becomes a normal
in-flow box — position: static, no insets — and where it sits is your CSS’s
business, a toolbar, a card corner, a sticky footer. Because there is no
corner left to decide the menu’s geometry, two props hand that decision back:
placement ("block-end" by default — the menu opens downward) and align
("start" by default). Under any corner both are ignored: one prop, one
truth — the corner is the geometry.
Two things follow and are worth knowing. The scrim still veils the whole
viewport (it stays position: fixed), and the root takes isolation: isolate
so the veil keeps painting between the page and the button. And a custom
flower has no corner to pick its sweep from, so it defaults to the bottom-end
arc and you aim it with the two public knobs --wayra-fab-arc-from /
--wayra-fab-arc-to.
For plain corner nudging you never need custom: --wayra-fab-edge moves a
docked fab off its edges, on both axes at once.
Stack and flower
Section titled “Stack and flower”variant="stack" open. The same Dropdown rows, re-dressed: the icon span is an opaque circle, the label span a pill, and each arrives on its own 35ms step of the settle curve.
The default variant="menu" opens the Dropdown surface every other page
documents. The two other values keep exactly the same DOM — the same rows,
the same keyboard, the same aria-* — and change only the paint:
stackis the speed dial: a column of circular actions above (or below) the trigger, each named by a pill. The circle sits on the trigger’s own inline axis and the pill points inward, mirroring for-startcorners and underdir="rtl"with no extra rule.flowerfans the actions on an arc around the trigger. The angle is trigonometry, not a lookup table: each row interpolates its angle from--wayra-dropdown-indexover--wayra-dropdown-count— the two custom properties Dropdown writes inline — so two actions take the ends of the sweep and eight spread evenly between them, and the radius grows with the count so they never overlap. Labels are clipped, not removed: every circle still has its accessible name. The arc is physical, so it mirrors through a sign read off adir="rtl"on the fab or any ancestor — which works at the Chrome 111 floor — and:dir(), where the browser has it (Chrome 120+), puts an LTR island inside an RTL page back. The menu itself opens withplacement="cover", laid over the trigger, whatever the corner — markup written by hand writesdata-placement="cover"on it for the same reason.
variant="flower" open in the default corner: the arc sweeps from due inline-start up to due block-start, and reverses under dir="rtl".
Because a row is also its geometry slot in these two layouts, items admits
only item and checkbox rows there — a separator or a group throws,
the way Tabs throws on a mixed item list, instead of rendering a heading
floating in an arc.
The keyboard does not change: the arrows walk the actions in item order,
typeahead still jumps by label — the flower’s clipped pills and the stack’s
visible ones are the same string — Escape closes and refocuses the circle,
and the focused action paints its circle in the accent, which is the menu
highlight transplanted onto a button. A press does the same with the menu’s
press step: the circle darkens toward base-content (5.18:1 or more in every
role of the AA themes) and gives under the pointer, a stack’s pill takes one
press wash, and the row behind them stays transparent.
Labels on demand
Section titled “Labels on demand”labels="hover": the pills are at opacity 0 until the pointer or the keyboard reaches their row. Hover an action to see one.
labels="hover" (stack only) fades each pill in on its row’s :hover and
:focus-visible, so a keyboard user walking the stack gets the same
disclosure a pointer gets. Faded, never removed — the accessible name stays in
the tree in every state. flower ignores the prop (its labels are always
clipped); menu has nothing to fade.
Code
<Fab size="sm" items={ITEMS} triggerLabel="sm" /><Fab items={ITEMS} triggerLabel="md" /> {/* md, the default */}<Fab size="lg" items={ITEMS} triggerLabel="lg" /><div class="wayra-fab" data-size="sm" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="sm" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="md" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" data-size="lg" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="lg" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div>| Step | Diameter knob | At the default unit |
|---|---|---|
sm |
11 × --wayra-size-selector |
44px |
md |
14 × --wayra-size-selector |
56px |
lg |
16 × --wayra-size-selector |
64px |
Three steps off a ladder of its own, not the field ladder every control
walks. The small end sits on WCAG 2.5.5’s 44px comfortable target line rather
than on 2.5.8’s 24px floor, because a floating button is a thumb target before
it is anything else — there is nowhere smaller to go and no xs.
The same prop is the menu’s row step, so a small fab opens a small menu and the two never look like they came from different sets.
The action circles of stack and flower walk a second named ladder one seat
down — 9 / 11 / 13 × --wayra-size-selector, so 36 / 44 / 52px — which makes
the md action exactly the sm trigger. Every step clears WCAG 2.5.8’s 24px
floor, and in the stack the pill is part of the same button, so the real
target is wider still.
Colour
Section titled “Colour”Code
<Fab items={ITEMS} triggerLabel="…" /> {/* primary, the default */}<Fab color="accent" items={ITEMS} triggerLabel="…" /><Fab color="error" items={ITEMS} triggerLabel="…" /><div class="wayra-fab" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="primary" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" data-color="accent" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="accent" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" data-color="error" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="error" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div>color travels as data-color, primary included. Markup written by hand
may leave it off: color.css falls back to primary. The circle is the role solid on its -content pair, which is the
library’s two-token colour contract doing the contrast work.
The same value is passed on to the composed menu, so the highlight that sweeps
the active row matches the button that opened it. A row carrying
tone: 'danger' still re-points the accent to error for itself, exactly as on
the Dropdown page.
The open state
Section titled “The open state”The same markup twice, with aria-expanded hard-coded to true on the second.
Code
{/* Nothing to write: Dropdown flips aria-expanded and CSS does the rest. */}<Fab items={ITEMS} triggerLabel="Acciones rápidas" /><div class="wayra-fab" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="Cerrado" aria-expanded="false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div><div class="wayra-fab" style="position: static"> <button type="button" class="wayra-fab-trigger" aria-label="Abierto" aria-expanded="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button></div>Two things change when the menu opens, and both are the open state’s signature: the
accent gives way to a neutral surface — 14% base-content over base-100, so
the glyph on it clears the 3:1 non-text floor — and the + rotates 135°
into an ×. One glyph, one rotation on the settle curve; there is no second
icon to cross-fade and no triggerIconOpen prop.
That is also the constraint on triggerIcon: whatever you pass is turned 135°,
so a glyph that is not symmetric about both axes reads oddly at that angle. A
plus, a cross, a four-pointed star and a dot all survive it; a pencil does not.
The scrim
Section titled “The scrim”<Fab scrim items={ITEMS} triggerLabel="Acciones rápidas" />scrim dims the page while the menu is open. It is a flat color-mix —
20% base-content over the page — and never a blur: the library ships flat
opaque surfaces, and backdrop-filter was withdrawn in a design review rather
than kept behind a prop.
It is purely visual, in three ways worth stating plainly:
pointer-events: none. It is not a click catcher. Light dismiss belongs to the browser, and adding a second one would only race it.aria-hidden="true". It is a<span>with no text and nothing to announce.z-index: -1, which keeps it inside the fab’s own stacking context — under the trigger, over the page. The menu is in the top layer and paints above both regardless.
Nothing in JavaScript switches it on. The rule reads the trigger’s
aria-expanded through :has(), which is why the scrim carries no attribute of
its own and why the component needs no state to draw it.
Open state, controlled
Section titled “Open state, controlled”const [open, setOpen] = useState(false);
<Fab open={open} onOpenChange={setOpen} items={ITEMS} triggerLabel="Acciones rápidas" />open, defaultOpen and onOpenChange are passed straight to
Dropdown and behave
exactly as documented there — including where focus goes, which is the part
worth re-reading: opening from open or defaultOpen moves no focus, opening
from the trigger lands on a row, and closing on purpose hands focus back to the
circle. onOpenChange fires for the closes the browser started too, so a
controlled fab that ignored it would reopen on the next render.
Hand-written markup
Section titled “Hand-written markup”The class plus the data-* attributes is the public API, and every preview on
this page is written against it with no React involved. The skin — the
corner, the circle, the role fill, the rotation — needs no
JavaScript beyond flipping one attribute. Every data-* is optional here:
the component writes all of them, defaults included, and a missing one reads
as its default.
<div class="wayra-fab" data-color="accent"> <button type="button" class="wayra-fab-trigger" aria-label="Acciones rápidas" aria-haspopup="menu" aria-expanded="false" aria-controls="fab-menu"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"> <path d="M12 5v14"/><path d="M5 12h14"/> </svg> </button></div>const trigger = document.querySelector('.wayra-fab-trigger');
trigger.addEventListener('click', () => { const open = trigger.getAttribute('aria-expanded') === 'true'; trigger.setAttribute('aria-expanded', String(!open));});Six lines, and the circle turns neutral, the plus rotates into a cross and the scrim fades in — because all three are keyed on that one attribute.
The menu half is a different exercise, one level down: it is not a class,
it is a component, and reproducing it by hand is
Dropdown’s own hand-written markup section.
What that section says applies here unchanged — the paint is reproducible, the
keyboard is not. The roving tabindex, typeahead, Escape and the focus return
are the component’s, and they are the whole reason it exists.
Public CSS knobs
Section titled “Public CSS knobs”| Property | Default | What it moves |
|---|---|---|
--wayra-fab-size |
the size step — 56px at md |
Diameter of the circle. Re-declared by each data-size, so override it after the step or per instance. |
--wayra-fab-edge |
1.25rem |
The gap to each docked edge, on both axes at once. |
--wayra-fab-z |
20 |
Stacking number, chosen to clear the in-flow overlay fallback (10). |
--wayra-fab-gap |
0.75rem |
The column gap between two stacked actions. |
--wayra-fab-arc |
unset — computed from the count | Radius of the flower. Unset on purpose: the fallback grows with --wayra-dropdown-count so eight actions clear each other; set a length on the root to pin it. |
--wayra-fab-arc-from / --wayra-fab-arc-to |
180deg / 90deg, re-pointed per corner |
The two ends of the flower’s sweep. Public because a position="custom" fab has no corner to pick them; each corner preset re-points the pair. |
--wayra-fab-z only matters below the top-layer tier. Where the browser has
popover and anchor positioning, the menu is in the top layer and any
number is inert there; the knob exists for the fallback path, and for a page
whose own furniture is stacked higher than the fab.
Everything else the file uses — the glyph size, the two durations, the ring
colour — is a private --_wayra-fab-* value and not API.
What’s not included
Section titled “What’s not included”No second widget behind the fan
Section titled “No second widget behind the fan”The port audit first cut the daisyUI-shaped fan outright (2026-08-23); the same
week the decision was reopened and the fan came back — as CSS. What stayed
dead is the part that made the legacy fan worse than a menu: there is no second
geometry engine, no second keyboard, no per-action component and no unlabelled
circle. stack and flower are stylesheet re-dressings of the same
role="menuitem" rows, which is why they cost no JavaScript and why every
circle kept its name. The one thing the layouts refuse is a separator or
group row — a heading has no seat on an arc, so the component throws rather
than guessing.
No mainAction, no closeButton
Section titled “No mainAction, no closeButton”Both are gone from the legacy API. A fab that runs one action and opens a menu
is two controls in one target — the trigger opens the menu, and the action that
would have been mainAction is simply the first row. closeButton is what the
trigger already becomes: the same button closes the menu, which is exactly what
the rotation into an × is telling the user.
children — the type rejects it
Section titled “children — the type rejects it”The root’s children are the scrim and the menu, so items is the content and
the props type omits children (the Dropdown precedent). One smuggled past
the types through a cast is still not rendered.
API reference
Section titled “API reference”FabProps extends ComponentProps<"div"> minus children and color, so id, style,
ref, className, every aria-* and every data-* land on the root — the box
that is fixed to the corner.
| Prop | Type | Default | Description |
|---|---|---|---|
items |
readonly DropdownItem[] |
— required | The actions, in order, as Dropdown rows: item, checkbox, separator, group. A button that opens an empty menu does nothing. Under stack / flower only item and checkbox are admitted — a separator or group throws. |
triggerLabel |
string |
— required | Accessible name of the circle. Required text rather than a default, so the button is translated by construction — the glyph inside names nothing. |
variant |
"menu" | "stack" | "flower" |
"menu" |
The layout of what opens: the Dropdown surface, the speed-dial column, or the trigonometric arc. Same DOM in all three; travels as data-variant. |
labels |
"always" | "hover" |
"always" |
Stack only: hover fades each pill in on its row’s :hover / :focus-visible. The accessible name never leaves the tree. Travels as data-labels. |
color |
WayraColor |
"primary" |
Semantic role of the trigger, as data-color. Passed on to the menu, so the row highlight matches. |
size |
"sm" | "md" | "lg" |
"md" |
Diameter off the Fab ladder: 44 / 56 / 64px. Also the menu’s row step and the action-circle step. Travels as data-size. |
position |
"bottom-end" | "bottom-start" | "top-end" | "top-start" | "custom" |
"bottom-end" |
Which corner it docks to, logically — or custom to opt out of the viewport and place the root yourself. A corner also decides the menu’s placement and align. Travels as data-position. |
placement |
"block-start" | "block-end" |
"block-end" |
Which side the menu opens on — honored only under position="custom"; a corner decides its own, and a flower always covers the trigger. |
align |
"start" | "center" | "end" |
"start" |
Which trigger edge the menu lines up with — honored only under position="custom". Under custom, the stack column follows it too. |
scrim |
boolean |
false |
Dim the page while the menu is open. A flat veil, no blur; pointer-events: none and aria-hidden. |
triggerIcon |
ReactNode |
a plus <svg> |
Glyph inside the circle. Decorative, and rotated 135° while the menu is open. |
open |
boolean |
— | Controlled open state, passed to Dropdown. Pair it with onOpenChange. |
defaultOpen |
boolean |
false |
Open state on the first render, uncontrolled. |
onOpenChange |
(open: boolean) => void |
— | Fires with the new state, including the closes the browser started. |
unstyled |
boolean |
false |
Strips the FAB skin only — the root’s class, the trigger’s and the scrim’s. The composed menu keeps its own classes; Dropdown has an unstyled of its own to give up. Every data-* and the whole behaviour survive. |
Accessibility
Section titled “Accessibility”The keyboard is Dropdown’s, whole and
unchanged: Enter / Space and the arrows open the menu onto a row, the arrows
walk it, typeahead jumps, Escape closes it and hands focus back to the circle,
Tab closes it and lets the browser carry on to the next control.
- The trigger says what it opens, before it opens.
triggerLabelis its whole accessible name;aria-haspopup="menu",aria-expandedtracking the state andaria-controlspointing at the surface are all cloned on by Dropdown. A screen reader announces the label plus collapsed or expanded. - The menu names itself back through
aria-labelledbyon the trigger, sotriggerLabelis also the menu’s accessible name — one string to translate, not two. - The glyph is decoration and says so.
aria-hidden="true"on the default plus, and the same is expected of whatever you pass astriggerIcon. Nothing in the circle is announced except the label. - The scrim is out of the tree.
aria-hidden="true", no text, and no pointer events — a screen reader never meets it, and the pointer goes straight through to the browser’s own light dismiss. - Every step clears 44px, the comfortable target line, which is the reason
the ladder starts where it does. The focus ring is the one rule in
states.css; the trigger declares--_wayra-ring-colorand nothing else. - The flower’s names are clipped, not cut. The pill is visually hidden
with the clip pattern, never
display: none, so every circle keeps its accessible name and typeahead still matches it.labels="hover"fades withopacityfor the same reason — and reveals on:focus-visibletoo, so the keyboard gets the same disclosure the pointer does. - Reduced motion zeroes the press, the background transition, the glyph
rotation and the entry stagger by re-pointing
--_wayra-fab-duration,--_wayra-fab-glyph-durationand--_wayra-fab-stagger— variables, never properties, so nothing freezes mid-turn and the open state still reads as open. The actions and the scrim keep a 160ms fade: the actions fade in together where they land, at full size, with no travel from the trigger. A pressed action circle keeps its press colour and drops its give. - Windows high contrast gives the circle a
CanvasTextborder, because the solid role fill that normally delimits it is not painted there, and removes the scrim outright — a semi-transparent wash is painted opaque there, which would black the page out.
Do and don’t
Section titled “Do and don’t”Do
One fab, one screen, one corner. It is the screen's single most likely action, and the menu holds the rest.
Don't
Two floating buttons competing for the same job. There is no primary action any more — and the second one is a Button in the toolbar it belongs to.
Do
A glyph symmetric about both axes. At 135° the plus is a clean ×, which is what tells the user the same button closes the menu.
Don't
An asymmetric triggerIcon. The rotation is unconditional, so an open menu leaves the glyph lying at an angle that means nothing.