SpeedDial

Floating action trigger with radial shortcuts.

Live demo

01

Linear Β· four directions

up
down
left
right

Circle Β· 360Β° fan-out

type="circle"

Semi-circle Β· 180Β° edge fan

up
left

With modal mask

mask=true

Implementation

02
SpeedDial.svelte
<script lang="ts">
  import SpeedDial from '$lib/components/SpeedDial.svelte';
  import type { SpeedDialAction } from '$lib/types';
​
  const actions: SpeedDialAction[] = [
    { id: 'add', label: 'Add', icon: 'βž•', onclick: () => console.log('add') },
    { id: 'edit', label: 'Edit', icon: '✏️', onclick: () => console.log('edit') },
    { id: 'delete', label: 'Delete', icon: 'πŸ—‘οΈ', onclick: () => console.log('delete') }
  ];
</script>
​
<SpeedDial {actions} type="circle" direction="up" radius={90} />

SpeedDial wraps a primary FAB with a list of actions that animate out around it. The type prop chooses the geometry β€” linear (a row), circle (full 360Β°), semi-circle (180Β°), or quarter-circle (90Β° corner). Each action is staggered by transitionDelay; tooltips auto-position relative to the trigger so labels never collide with screen edges. Press Esc or click outside to close β€” focus returns to the trigger.

Logic explainer

03

What Does It Do? (Plain English)

SpeedDial is a floating action button (FAB) that expands to reveal multiple action items when clicked. Think of it like a flower blooming - when you tap the main button, smaller buttons "bloom" around it in various patterns (straight line, circle, semi-circle, or quarter-circle).

Think of it like: A Swiss Army knife. The main tool is always visible, but click it and other tools pop out ready to use!


How It Works (Pseudo-Code)

WHEN component loads:
  1. SET isOpen to false
  2. STORE references to all buttons
  3. SET up keyboard listener on window

WHEN main button CLICKED:
  1. IF disabled β†’ do nothing
  2. TOGGLE isOpen state
  3. FOR each action item:
     - CALCULATE position using layout type
     - APPLY staggered animation delay
     - ANIMATE into position

WHEN action item CLICKED:
  1. IF not disabled β†’ execute action callback
  2. CLOSE the menu
  3. RETURN focus to main button

WHEN Escape key PRESSED:
  1. IF isOpen β†’ close menu and focus main button

WHEN Tab key PRESSED (while open):
  1. FIND currently focused button
  2. IF at end β†’ wrap to first
  3. IF at start + Shift β†’ wrap to last
  4. PREVENT default (trap focus in menu)

WHEN clicking OUTSIDE:
  1. IF isOpen AND click not on container
  2. CLOSE menu

The Core Concept: Trigonometric Positioning

The "magic" of SpeedDial is positioning action items in circular patterns. This requires basic trigonometry!

The Unit Circle

Imagine a clock face centred on the main button. Each action item sits on the edge of this circle at a specific angle.

        90Β° (up)
          β”‚
          β”‚
  180Β° ───┼─── 0Β° (right)
  (left)  β”‚
          β”‚
        270Β° (down)

Converting Angles to Positions

For any angle, we calculate X and Y positions using:

  • x = cos(angle) Γ— radius β†’ horizontal distance
  • y = sin(angle) Γ— radius β†’ vertical distance
Example: Position at 45Β° with radius 80px

         .  ← Target position
        /β”‚
       / β”‚
      /  β”‚ y = sin(45Β°) Γ— 80 = 56.57px
     /   β”‚
    /45Β° β”‚
   ●─────┴─────
   ↑     x = cos(45Β°) Γ— 80 = 56.57px
Main Button

Layout Types

Linear (simple)

direction="up"
    3
    β”‚
    2
    β”‚
    1
    β”‚
   [●]  ← Main button

Circle (360Β°)

direction="up" (starts at 90Β°)
       1
     /   \
    4     2
     \   /
       3
      [●]

Semi-Circle (180Β°)

direction="up"
    1   2   3
     \  β”‚  /
      \ β”‚ /
       \β”‚/
       [●]

Quarter-Circle (90Β°)

direction="up" (bottom-right corner placement)
    1
     \
      2
       \
        3
        [●]  ← Perfect for corners!

Focus Trapping

When the menu is open, Tab key cycles through action items without leaving the menu. This is essential for keyboard accessibility.

[Main Button] β†’ [Action 1] β†’ [Action 2] β†’ [Action 3]
       ↑                                      β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    (wraps around)

Implementation:

  1. Track which button is focused using document.activeElement
  2. Find its index in the enabled buttons array
  3. On Tab: move to next (or wrap to first)
  4. On Shift+Tab: move to previous (or wrap to last)
  5. Call event.preventDefault() to override default Tab behaviour

XSS Protection

Icons can be SVG strings, which creates an XSS risk. We use DOMPurify to sanitize:

// Unsafe: action.icon could contain malicious script
{@html action.icon}

// Safe: sanitizeSVG strips dangerous elements
{@html sanitizeSVG(action.icon)}

The sanitizeSVG function (from $lib/utils) uses DOMPurify with strict configuration to allow only safe SVG elements and attributes.


State Flow Diagram

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   CLOSED     β”‚
                    β”‚  isOpen=falseβ”‚
                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚ click on main  β”‚ click outside  β”‚ Escape key
          β”‚ button         β”‚                β”‚
          β–Ό                β”‚                β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚                β”‚
    β”‚    OPEN      β”‚       β”‚                β”‚
    β”‚ isOpen=true  β”‚β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”‚ click on action
           β”‚
           β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ EXECUTE      β”‚
    β”‚ action.onclickβ”‚
    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   CLOSED     β”‚
    │ focus→trigger│
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

CSS Animation Strategy

Staggered Entrance

Each action item has a delay based on its index:

.speed-dial-action {
  transition-delay: var(--delay, 0ms);
}
style="--delay: {index * transitionDelay}ms;"

With transitionDelay=30, three items animate:

  • Item 0: 0ms delay
  • Item 1: 30ms delay
  • Item 2: 60ms delay

This creates a satisfying "cascade" effect.

Position Animation

Items animate from scale(0) at centre to scale(1) at target position:

/* Closed state */
.speed-dial-action {
  transform: translate(-50%, -50%) scale(0);
  opacity: 0;
}

/* Open state */
.is-visible .speed-dial-action {
  transform: translate(calc(-50% + var(--item-x)), calc(-50% + var(--item-y))) scale(1);
  opacity: 1;
}

Props Reference

Prop Type Default Description
actions SpeedDialAction[] [] Action items to display
direction 'up'β”‚'down'β”‚'left'β”‚'right' 'up' Which way items expand
type 'linear'β”‚'circle'β”‚'semi-circle'β”‚'quarter-circle' 'linear' Layout pattern
radius number 80 Distance from main button (circular layouts)
transitionDelay number 30 ms delay between item animations
showTooltip boolean true Show labels on hover
tooltipPosition 'auto'β”‚'left'β”‚'right'β”‚'top'β”‚'bottom' 'auto' Tooltip placement
mask boolean false Show backdrop overlay
disabled boolean false Disable all interactions
buttonIcon string - Custom icon (emoji or SVG)
buttonLabel string 'Open menu' ARIA label for main button
isOpen boolean false Bindable open state

Edge Cases

Situation Behaviour
Zero actions Menu opens but shows nothing
One action Linear: appears directly above/below/etc
One action (circular) Positioned at start angle
All actions disabled Menu opens, items visible but not clickable
Quick open/close Animations complete cleanly
Click during animation State updates immediately

Dependencies

  • DOMPurify (external): XSS protection for SVG icons
  • $lib/types: SpeedDialProps, SpeedDialAction interfaces
  • $lib/utils: sanitizeSVG function

File Structure

SpeedDial.svelte      # The component
SpeedDial.test.ts     # Unit tests
SpeedDial.md          # This explainer

Last updated: 26 December 2025

API

04
PropTypeDefaultDescription
actionsSpeedDialAction[][]Action items shown when the dial opens.
direction'up' | 'down' | 'left' | 'right''up'Linear direction or start angle for circular layouts.
type'linear' | 'circle' | 'semi-circle' | 'quarter-circle''linear'Layout geometry of the action items.
radiusnumber80Distance in px from centre for circular layouts.
transitionDelaynumber30Stagger between item animations (ms).
showTooltipbooleantrueShow label tooltips on hover/focus.
tooltipPosition'auto' | 'left' | 'right' | 'top' | 'bottom''auto'Tooltip placement relative to each action.
maskbooleanfalseShow a modal backdrop when open.
disabledbooleanfalseDisable the entire dial.
buttonIconstring'+' SVGCustom emoji, HTML, or SVG for the trigger.
buttonLabelstring'Open menu'Accessible label for the trigger.
isOpenbooleanfalseBindable open/closed state.
classstring''Extra CSS class on the container.