Fylgja Speed Dial
A speed dial is a floating action button that opens a short stack of related actions. The actions live in a native popover, opened by the trigger with an invoker command, so the browser handles the open state, light dismiss, Escape to close, and the expanded state for assistive technology. CSS anchor positioning keeps the actions attached to the trigger.
Reach for it when a screen has one main place to start from, like creating or sharing something, with a few variations. For a single action, a plain Icon Button is enough.
Preview
The demo sits in the corner of its own box. On a real page you would fix it to the viewport, as shown under Fixed to the page.
<div class="grid align" style="--align: end; --size-y: 16rem">
<button type="button" class="btn --primary round" commandfor="speed-dial" command="toggle-popover" aria-label="Quick actions">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</button>
<div id="speed-dial" popover class="speed-dial flex-col gap-sm open:flex">
<button type="button" class="round" aria-label="Share">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.59 13.51 6.83 3.98"/><path d="m15.41 6.51-6.82 3.98"/></svg>
</button>
<button type="button" class="round" aria-label="Copy link">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
</button>
<button type="button" class="round" aria-label="Bookmark">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg>
</button>
</div>
</div>
<style>
.speed-dial {
--btn-bg: var(--root-bg);
--btn-hover-bg: color-mix(in oklab, var(--root-bg) 86%, var(--root-fg));
margin: 0 0 0.75rem;
inset: auto;
position-area: block-start span-inline-start;
position-try-fallbacks: flip-block, flip-inline;
padding: 0;
border: 0;
background: none;
overflow: visible;
}
[commandfor]:has(+ .speed-dial:popover-open) > svg {
rotate: 45deg;
}
</style>Usage
- A trigger button with
commandforpointing at the popover’sidandcommand="toggle-popover". Give it anaria-label, since it only shows an icon. - A
popoverelement right after the trigger, holding the action buttons. Being next in the source keeps the tab order natural: the actions follow the trigger. - The small
.speed-dialrule. It removes the default popover box, andposition-areaplaces the actions above the trigger, aligned to its end edge. Opening a popover from an invoker makes that invoker its implicit anchor, so noanchor-nameis needed.
The popover is an auto popover, so clicking outside or pressing Escape closes it, and
opening another auto popover closes this one. open:flex only switches on the flex
layout while it is open, which keeps the popover’s own hidden state intact.
Anatomy
| Part | Responsibility |
|---|---|
Trigger <button command> |
Toggles the actions with command="toggle-popover" and commandfor. |
<div popover class="speed-dial"> |
Holds the actions, rendered in the top layer while open. |
Action <button> |
One related action, round with an icon or labeled with text. |
position-area |
Places the actions above the trigger, and flips them when out of room. |
Accessibility
- The trigger is exposed as a button with an expanded or collapsed state, set by the
browser from the popover. Do not add
aria-expandedyourself. - Every icon only button needs a name: an
aria-labelon the trigger and on each action. Prefer the labeled variant when there is room, since visible text helps everyone. - Escape closes the actions and returns focus to the trigger when focus was inside them. Clicking outside closes them as well.
- Keep each target at least 24 by 24 CSS pixels, and give a fixed speed dial enough space from the viewport edge so it does not cover content the reader needs.
Variants
With labels
Wrap a text label in a <span> next to each icon, so the actions read as words.
--align-items set to end lines them up against the trigger. This uses the same
.speed-dial rule as the preview.
<div class="grid align" style="--align: end; --size-y: 16rem">
<button type="button" class="btn --primary round" commandfor="speed-dial-labels" command="toggle-popover" aria-label="Create">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</button>
<div id="speed-dial-labels" popover class="speed-dial flex-col gap-sm open:flex align" style="--align-items: end">
<button type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>
<span>New document</span>
</button>
<button type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
<span>New folder</span>
</button>
<button type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/></svg>
<span>Upload file</span>
</button>
</div>
</div>Fixed to the page
On a real page the trigger stays in the corner of the viewport. Add the fixed
utility with --inset-b and --inset-e to the trigger. The popover keeps following
it through anchor positioning, so nothing else changes. Raise --z on the trigger if
other positioned content covers it.
<button type="button" class="btn --primary round fixed" style="--inset-b: 1.5rem; --inset-e: 1.5rem" commandfor="page-actions" command="toggle-popover" aria-label="Quick actions">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
</button>
<div id="page-actions" popover class="speed-dial flex-col gap-sm open:flex">
<!-- the same action buttons as above -->
</div>
Browser support
Popovers and invoker commands are supported in current browsers. Anchor positioning is newer, so check Can I use for the browsers you support. Without it, the actions still open but land at the edge of the viewport. For a fixed speed dial you can avoid anchor positioning entirely by placing the popover with fixed insets, as described in the FAQ.
Related: Tooltip, Icon Button.
FAQ
How many actions should a speed dial hold?
Three to five. It is a shortcut to a few closely related actions, not a navigation menu. When the list grows longer, or the actions are unrelated, use a regular menu or put the actions on the page itself.
Does the speed dial close after I pick an action?
Not on its own, since an action button only runs your code. Call hidePopover() on the speed dial at the end of that code. Clicking outside, or pressing Escape, already closes it.
Why does the trigger not show the actions on hover?
Hover does not exist on touch screens, and an opened state that depends on where the pointer rests is hard to use with a keyboard. A click or tap opens it everywhere, and the browser reports the open state to assistive technology.
Do I need anchor positioning?
Only to place the actions next to the trigger automatically. For a page level speed dial you can drop position-area and the inset reset, and place the popover with the fixed utility and --inset-b and --inset-e instead, which works in every browser that supports popovers.