Speed Dial / FAB

Pro

Floating action button with up/down/left/right expand directions, a custom action list, pulse animation, tooltip labels and viewport-aware positioning that auto-flips and clamps so the menu never leaves the screen.

</>sv-speed-dial fabactionsfloating
import { SvSpeedDialComponent } from '@styloviz-pro/layout';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Left expand with tooltips

Pro

Always-labeled variant

Pro

Pinned to a corner, clear of other floating UI

Pro

floating pins the dial and joins the shared floating layer, so Scroll to Top, a host chat launcher, a cookie banner or a bottom nav in the same corner stack instead of overlapping.

Destructive action with confirmation

Pro

The first activation arms the item and announces the prompt; the second confirms; Escape or moving away cancels. No window.confirm.

When to use

  • Exposing a primary action plus a few related quick actions in a floating button.
  • Mobile-first or content-heavy screens where a toolbar would crowd the layout.
  • For a row of always-visible actions, use the Toolbar.

Async states

  • error

Item identity & owned state

Restorable state
  • busyIds

Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their Change events to store what the user does.

Inputs

NameTypeDefaultDescription
actionsrequiredreadonly SpeedDialAction[]—The quick actions revealed when the dial opens.
iconstring'plus'Icon-library name for the closed (trigger) state.
closeIconstring'close'Icon-library name for the open state.
directionSpeedDialDirection'up'Direction the actions fan out: up, down, left or right.
variantSpeedDialVariant'default'Visual variant.
labelstring'Actions'Accessible label for the trigger.
colorstring'primary'Trigger color (semantic token or CSS color).
tooltipbooleantrueShow tooltips next to each action.
floatingSpeedDialFloatingCorner | nullnullPin the dial `position: fixed` to this screen corner and join the shared floating layer, so it stacks clear of Scroll to Top, a host's chat launcher, cookie banner or bottom nav, and the device safe area. `null` leaves positioning to the host exactly as before. ⚠️ **To coordinate with Scroll to Top etc., set `floating`.** With the default `null` the dial is host-positioned — wherever the host's own CSS puts it — so it cannot know which corner it occupies and does NOT register with the floating layer. A Scroll to Top in the same corner will then sit on top of it. This stays opt-in so an existing host's layout is not moved under it.
floatingOrdernumberSV_FLOATING_ORDER.fabRank in the floating layer's corner stack; lower sits nearer the edge. Only read while `floating` is set.
confirmTextstring'Confirm'Word that prefixes an action's label while it waits for confirmation — the armed item is named "`confirmText`: `label`". Localise it here. Only used for actions with `destructive` / `confirm` and no custom prompt.
busyIdsreadonly string[] | ReadonlySet<string>[]Ids of actions with a request in flight. Those controls disable and show a busy state, so a double-click cannot fire the same request twice — the most common way an action button creates duplicate records.
errorSvDataError | string | nullnullFailure from the last action. Rendered above the control group; nothing is removed from view, so the user can see what state things are in.

Outputs

EventPayload typeDescription
actionClickstringEmitted with the id of the clicked action.

Customization & theming

  • Four expand directions.
  • Three action styles: default, labeled and mini.
  • Per-action colour, badge and disabled state.
  • Solid, high-contrast label chips that stay legible while the page scrolls.
  • Viewport-aware positioning: measures the panel, flips to the side with room, then shifts it fully on-screen.
  • Floating layer (`floating`, `floatingOrder`): pins the dial to a corner with `position: fixed`, adds the device safe-area inset, and stacks it with every other registered region — Scroll to Top, affixed blocks, and anything the host registers through `SvFloatingLayerService` (chat launcher, cookie banner, bottom nav). A full-width edge region pushes every corner on its side; `floatingOrder` (default 10) decides who sits nearest the edge. `null` (default) leaves positioning to you.
  • Confirmation: `destructive` (danger styling + confirmation) and `confirm` (`true` or a custom prompt string, `false` to opt out) per action; `confirmText` localises the default "Confirm: <label>" prompt.
  • Variant axes: `variant` (default, labeled, mini) and `color` are appearance; `direction`, `tooltip`, `floating` and per-action `confirm` are behavioural modes. Every appearance works with every mode; `mini` with `tooltip=false` leaves only icons, so keep labels meaningful for the accessible name.
  • Portals and layers: the menu is portalled to `<body>` through the kit's portal host, which carries a locally scoped `.dark`, `data-theme`, `dir` and `lang` from where the dial sits, and takes its z-index from the shared overlay stack — above page chrome, below a dialog opened later. The dial itself, when `floating`, sits in the floating band (z 50), always under overlays. It is not portalled, though: with `floating` set it is `position: fixed`, so an ancestor with `transform`, `filter`, `perspective`, `backdrop-filter`, `contain` or `container-type` becomes its containing block and it pins to that box, not the viewport — mount a floating dial outside such wrappers.
  • Reduced motion: the pulse ring is removed, and the entrance stagger, icon turn and hover scale are stilled under `prefers-reduced-motion`. Forced colors: the pulse is hidden and the trigger and items get a ButtonText edge, plus a system-coloured focus outline (the focus ring is a box shadow, which forced colors removes).
  • Browser limits: safe-area insets only apply when the page sets `viewport-fit=cover`; iOS Safari's virtual keyboard does not resize the layout viewport, so a fixed dial can sit under the keyboard while an input is focused — hide it (`@if`) while editing if that matters.

Best practices

  • Do: Keep the action count small (3–5) so the fan stays tappable.
  • Do: Label each action with a tooltip since the buttons are icon-only.
  • Tip: The menu auto-flips to the opposite side and clamps to the viewport near edges — still set a sensible default `direction` for where the FAB usually sits.
  • Do: Mark delete, archive and revoke actions `destructive` — on a phone a floating menu is one mis-tap from data loss, and the two-step confirmation is keyboard- and screen-reader-operable.
  • Do: Use `floating` instead of hand-positioning the dial with `fixed bottom-6 right-6` when anything else can occupy that corner — only registered regions can be stacked around.
  • Tip: Floating layer (X-5): the default dial (`floating` unset) is HOST-POSITIONED — it cannot know which corner your CSS put it in, so it registers nothing, and Scroll to Top or a chat launcher in the same corner will overlap it. To coordinate with them, set `floating` to its corner.
  • Tip: Wrap action literals in `svSpeedDialAction()` to make an empty `label` a compile error. The field stays a plain `string` so data-driven actions keep compiling; a blank label that reaches the component logs a dev-mode warning instead.
  • Don't: Don't grow past about five actions — beyond that it is a floating menu, and a Toolbar or a menu button serves it better.

Accessibility

  • Trigger exposes aria-haspopup/aria-expanded/aria-controls; actions live in a labelled `role="menu"`.
  • Focus moves into the menu on open and returns to the trigger on Escape/select; Arrow keys, Home and End navigate items.
  • A confirming item keeps focus, is renamed to its prompt, and the prompt is announced assertively. Enter confirms, Escape cancels (a second Escape closes the menu), arrowing away cancels.
  • Escape is owned by the topmost overlay, so a dialog opened over the dial closes first.

Feedback on Speed Dial / FAB

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.