SpeedDial
Floating action trigger with radial shortcuts.
Live demo
01Linear Β· four directions
Circle Β· 360Β° fan-out
Semi-circle Β· 180Β° edge fan
With modal mask
Implementation
02<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
03What 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 menuThe 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 distancey = 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 ButtonLayout Types
Linear (simple)
direction="up"
3
β
2
β
1
β
[β] β Main buttonCircle (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:
- Track which button is focused using
document.activeElement - Find its index in the enabled buttons array
- On Tab: move to next (or wrap to first)
- On Shift+Tab: move to previous (or wrap to last)
- 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 explainerLast updated: 26 December 2025
API
04| Prop | Type | Default | Description |
|---|---|---|---|
actions | SpeedDialAction[] | [] | 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. |
radius | number | 80 | Distance in px from centre for circular layouts. |
transitionDelay | number | 30 | Stagger between item animations (ms). |
showTooltip | boolean | true | Show label tooltips on hover/focus. |
tooltipPosition | 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'auto' | Tooltip placement relative to each action. |
mask | boolean | false | Show a modal backdrop when open. |
disabled | boolean | false | Disable the entire dial. |
buttonIcon | string | '+' SVG | Custom emoji, HTML, or SVG for the trigger. |
buttonLabel | string | 'Open menu' | Accessible label for the trigger. |
isOpen | boolean | false | Bindable open/closed state. |
class | string | '' | Extra CSS class on the container. |