Skip to main content
WireKit
Copy for LLM

FAB

FAB stands for "floating action button" — the round button that hovers over the content in a screen corner, holding the one thing this screen is really for. The pattern comes from mobile design systems, where the primary action needs to stay reachable no matter how far the page has scrolled.

WireKit ships two of them. <x-wirekit::fab> is the speed dial: a floating trigger that expands into a small stack of secondary actions. <x-wirekit::fab.button> is the single-action version — one tap, no menu.

A speed dial with three actions

The preview sits in a position: relative frame so the fixed button is contained in the box — in your app you just drop in <x-wirekit::fab> and it pins to the viewport corner (see the Show Code above, which omits the demo frame).

Usage

<x-wirekit::fab label="Create">
    <x-wirekit::fab.action label="New note" icon="edit" />
    <x-wirekit::fab.action label="Upload file" icon="upload" />
    <x-wirekit::fab.action label="Docs" icon="book-open" href="/docs" />
</x-wirekit::fab>

Every action needs a label. The buttons are icons, so the label is their whole name — without one a screen reader announces "button" three times.

An action with an href renders as a link, and one without renders as a button. A thing that navigates is a link; a thing that acts is a button. Getting that wrong costs middle-click, "open in new tab", and the status bar.

Single action

Not every floating button fans out. When there is exactly ONE action — open a feedback modal, start a new message — reach for <x-wirekit::fab.button> instead of the speed dial. It is a plain button (or a link, with href): one tap, no menu, and it announces its target honestly through aria-haspopup (default dialog) rather than promising a menu that is really a dialog.

A single-action FAB that opens a dialog

The button really does open the dialog above — click it. That matters more than it sounds: aria-haspopup="dialog" is a promise made to assistive technology, and a button that announces a dialog and opens nothing tells a screen-reader user something untrue. The attribute describes behavior; it does not create it.

Pass haspopup="menu" / "listbox" / "true" when the button opens something other than a dialog, or a falsy value when it just navigates or fires an event. With href it renders an <a> and auto-injects rel="noopener noreferrer" for target="_blank". It shares the speed dial's corner positioning, including the safe-area insets that keep it clear of the iOS home indicator and a landscape notch.

Sitting above a bottom nav

Both are fixed, so a FAB on a page with a bottom nav lands on top of it. Add wk-fab-above-nav:

<x-wirekit::fab label="Create" class="wk-fab-above-nav">…</x-wirekit::fab>

Opt-in, because this component cannot see whether a nav exists elsewhere in the document — and guessing would lift every FAB off the corner it belongs in. The lift disappears at md along with the nav; add wk-fab-above-nav-always if you kept the nav on desktop.

Position

position follows the writing direction rather than assuming everyone reads left to right:

position Where
end The inline-end corner (default — bottom right in English)
start The inline-start corner
center Centered along the bottom

placement is the other axis, and the two combine into all four corners. The single-action <x-wirekit::fab.button> accepts it; the default block-end is the familiar bottom corner.

placement Where
block-end Along the bottom edge (default)
block-start Along the top edge
<x-wirekit::fab.button placement="block-start" position="start" icon="plus" label="Add" />

Both edges keep clear of the hardware: the bottom offset folds in the iOS home indicator, the top one the status bar and notch.

The name a screen reader reads

A FAB with an icon and nothing else needs label — the icon is decorative, so the label is the entire accessible name.

A FAB with words in it does not. The visible text becomes the accessible name, and label is left out of the markup:

{{-- Announced as "Send feedback", the same words that are on screen --}}
<x-wirekit::fab.button>Send feedback</x-wirekit::fab.button>

That is WCAG 2.5.3, Label in Name: someone using voice control activates a control by the words they can see, so a button reading Send feedback has to answer to Send feedback.

A label alongside visible text is kept when it contains that text — useful for adding context (Send feedback about this page). One that contradicts it is dropped rather than shipped, because it would break exactly the voice-control case above.

A slot holding markup rather than words — a custom icon — still needs label: there is nothing in it to read.

Action names are visible, too

An icon-only action tells a screen reader what it does through its label. That label is also shown beside the button when you hover it or reach it with the keyboard — otherwise anyone looking at the screen sees a column of circles and has to press one to find out what it is.

The name appears on focus as well as hover, so it is not a mouse-only affordance. It is positioned so revealing it never shifts the buttons, and it cannot intercept a click meant for the action beneath it.

An action that fills its own slot instead of using label shows nothing extra — its markup already says what it is.

Turning the visible name off

Pass hideLabel when you want the bare circles — the icon-only speed dial the pattern started as. It takes the name off the SCREEN only: aria-label stays, so a screen reader still announces the action.

Open both and hover an action to see the difference:

With names on hover (default) and with hideLabel

label stays required either way. Hiding a name is a visual choice; not having one is a defect, and the component says so during development.

Props

<x-wirekit::fab>

Prop Type Default Description
label string 'Actions' Names the trigger and the menu. The trigger is an icon, so this is all a screen reader gets.
position string 'end' end, start, center.
scope string|null null Class-scope override.

<x-wirekit::fab.action>

Prop Type Default Description
label string '' The action's name. Announced to screen readers, and shown beside the icon on hover and on keyboard focus.
hideLabel bool false Keep the accessible name but take it off the screen — the bare icon-only speed dial.
icon string|null null Icon name. Falls back to the slot.
href string|null null Renders a link instead of a button.
scope string|null null Class-scope override.

<x-wirekit::fab.button>

The single-action FAB — a plain button, no menu.

Prop Type Default Description
label string|null null The accessible name when the button carries no words of its own. Falls back to Action. Ignored when the button shows text, unless it contains that text — see The name a screen reader reads.
position string 'end' end, start, center — the inline corner, shared with the speed dial.
placement string 'block-end' block-end (bottom) or block-start (top). Combines with position, so all four corners are reachable.
haspopup string|bool 'dialog' What it opens, for aria-haspopup. A falsy value omits the attribute.
href string|null null Renders an <a> instead of a <button>.
icon string|null null Icon name. A slot overrides it; a plus is the fallback.
scope string|null null Class-scope override.

Slots

Slot Description
trigger Replaces the default plus icon.
default The actions.

Accessibility

The interesting part of a speed dial is not the fan — it is what happens to focus. A menu that opens and leaves focus on the trigger is a menu a keyboard user cannot reach; one that traps focus is a menu they cannot leave. This does neither:

  • Opening moves focus to the first action.
  • / walk the actions and wrap.
  • Esc closes and hands focus back to the trigger — where the reader was — but only if focus was inside the menu. A stray Escape from elsewhere on the page does not yank it.
  • The trigger's aria-haspopup="menu" and the panel's role="menu" agree, so a screen reader is not promised a popup whose contents are something else.
  • Actions are 44px, the touch minimum, and are the smallest targets on the screen.
  • The plus/close icons both stay in the DOM so they can cross-fade, so the inactive one is hidden from assistive tech — otherwise the trigger announces both states.
  • The rotation is dropped under prefers-reduced-motion.

Keyboard Interaction

Key Action
Tab Move focus to the trigger.
Enter / Space Open the actions and focus the first.
Move between actions.
Esc Close and return focus to the trigger.

Was this page helpful?

Voting requires cookies or local storage. What we store