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

  1. A trigger button with commandfor pointing at the popover’s id and command="toggle-popover". Give it an aria-label, since it only shows an icon.
  2. A popover element right after the trigger, holding the action buttons. Being next in the source keeps the tab order natural: the actions follow the trigger.
  3. The small .speed-dial rule. It removes the default popover box, and position-area places the actions above the trigger, aligned to its end edge. Opening a popover from an invoker makes that invoker its implicit anchor, so no anchor-name is 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-expanded yourself.
  • Every icon only button needs a name: an aria-label on 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.