"use client";
import { Copy, Heart, Plus, Share2 } from "lucide-react";
import { toast } from "sonner";
import {
SpeedDial,
SpeedDialAction,
SpeedDialContent,
SpeedDialItem,
SpeedDialLabel,
SpeedDialTrigger,
} from "@/components/ui/speed-dial";
export function SpeedDialDemo() {
return (
<SpeedDial>
<SpeedDialTrigger className="transition-transform duration-200 ease-out data-[state=closed]:rotate-0 data-[state=open]:rotate-135">
<Plus />
</SpeedDialTrigger>
<SpeedDialContent>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Share</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Shared")}>
<Share2 />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Copy</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Copied")}>
<Copy />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Like</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Liked")}>
<Heart />
</SpeedDialAction>
</SpeedDialItem>
</SpeedDialContent>
</SpeedDial>
);
}Installation
CLI
npx shadcn@latest add @diceui/speed-dialManual
Install the following dependencies:
npm install @radix-ui/react-slot class-variance-authorityCopy and paste the following code into your project.
"use client";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { useComposedRefs } from "@/lib/compose-refs";
import { cn } from "@/lib/utils";
const ROOT_NAME = "SpeedDial";
const TRIGGER_NAME = "SpeedDialTrigger";
const CONTENT_NAME = "SpeedDialContent";
const ITEM_NAME = "SpeedDialItem";
const ACTION_NAME = "SpeedDialAction";
const LABEL_NAME = "SpeedDialLabel";
const ACTION_SELECT = "speeddial.actionSelect";
const INTERACT_OUTSIDE = "speeddial.interactOutside";
const EVENT_OPTIONS = { bubbles: true, cancelable: true };
const DEFAULT_GAP = "0.5rem";
const DEFAULT_OFFSET = "0.5rem";
const DEFAULT_ITEM_DELAY = 50;
type Side = "top" | "right" | "bottom" | "left";
interface DivProps extends React.ComponentProps<"div"> {
asChild?: boolean;
}
type RootElement = React.ComponentRef<typeof SpeedDialRoot>;
type TriggerElement = React.ComponentRef<typeof SpeedDialTrigger>;
type ActionElement = React.ComponentRef<typeof SpeedDialAction>;
interface InteractOutsideEvent extends CustomEvent {
detail: {
originalEvent: PointerEvent;
};
}
const useIsomorphicLayoutEffect =
typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
function useAsRef<T>(props: T) {
const ref = React.useRef<T>(props);
useIsomorphicLayoutEffect(() => {
ref.current = props;
});
return ref;
}
function useLazyRef<T>(fn: () => T) {
const ref = React.useRef<T | null>(null);
if (ref.current === null) {
ref.current = fn();
}
return ref as React.RefObject<T>;
}
function getDataState(open: boolean): string {
return open ? "open" : "closed";
}
function getTransformOrigin(side: Side): string {
switch (side) {
case "top":
return "bottom center";
case "bottom":
return "top center";
case "left":
return "right center";
case "right":
return "left center";
}
}
interface StoreState {
open: boolean;
}
interface Store {
subscribe: (callback: () => void) => () => void;
getState: () => StoreState;
setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;
notify: () => void;
}
const StoreContext = React.createContext<Store | null>(null);
function useStoreContext(consumerName: string) {
const context = React.useContext(StoreContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return context;
}
function useStore<T>(
selector: (state: StoreState) => T,
ogStore?: Store | null,
): T {
const contextStore = React.useContext(StoreContext);
const store = ogStore ?? contextStore;
if (!store) {
throw new Error(`\`useStore\` must be used within \`${ROOT_NAME}\``);
}
const getSnapshot = React.useCallback(
() => selector(store.getState()),
[store, selector],
);
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
}
interface NodeData {
id: string;
ref: React.RefObject<HTMLElement | null>;
disabled: boolean;
}
interface SpeedDialContextValue {
contentId: string;
side: Side;
onNodeRegister: (node: NodeData) => void;
onNodeUnregister: (id: string) => void;
getNodes: () => NodeData[];
}
const SpeedDialContext = React.createContext<SpeedDialContextValue | null>(
null,
);
function useSpeedDialContext(consumerName: string) {
const context = React.useContext(SpeedDialContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return context;
}
interface SpeedDialRootProps extends DivProps {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onInteractOutside?: (event: InteractOutsideEvent) => void;
side?: Side;
}
function SpeedDialRoot(props: SpeedDialRootProps) {
const {
open: openProp,
defaultOpen,
onOpenChange,
onPointerDownCapture: onPointerDownCaptureProp,
onEscapeKeyDown,
onInteractOutside,
side = "top",
asChild,
className,
ref,
...rootProps
} = props;
const contentId = React.useId();
const rootRef = React.useRef<RootElement>(null);
const composedRefs = useComposedRefs(ref, rootRef);
const nodesRef = React.useRef<Map<string, NodeData>>(new Map());
const listenersRef = useLazyRef(() => new Set<() => void>());
const stateRef = useLazyRef<StoreState>(() => ({
open: openProp ?? defaultOpen ?? false,
}));
const propsRef = useAsRef({
onOpenChange,
onEscapeKeyDown,
onInteractOutside,
});
const onNodeRegister = React.useCallback((node: NodeData) => {
nodesRef.current.set(node.id, node);
}, []);
const onNodeUnregister = React.useCallback((id: string) => {
nodesRef.current.delete(id);
}, []);
const getNodes = React.useCallback(() => {
return Array.from(nodesRef.current.values())
.filter((node) => node.ref.current)
.sort((a, b) => {
const elementA = a.ref.current;
const elementB = b.ref.current;
if (!elementA || !elementB) return 0;
const position = elementA.compareDocumentPosition(elementB);
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
return -1;
}
if (position & Node.DOCUMENT_POSITION_PRECEDING) {
return 1;
}
return 0;
});
}, []);
const store = React.useMemo<Store>(() => {
return {
subscribe: (cb) => {
listenersRef.current.add(cb);
return () => listenersRef.current.delete(cb);
},
getState: () => stateRef.current,
setState: (key, value) => {
if (Object.is(stateRef.current[key], value)) return;
if (key === "open" && typeof value === "boolean") {
stateRef.current.open = value;
propsRef.current?.onOpenChange?.(value);
} else {
stateRef.current[key] = value;
}
store.notify();
},
notify: () => {
for (const cb of listenersRef.current) {
cb();
}
},
};
}, [listenersRef, stateRef, propsRef]);
const open = useStore((state) => state.open, store);
useIsomorphicLayoutEffect(() => {
if (openProp !== undefined) {
store.setState("open", openProp);
}
}, [openProp, store]);
const ownerDocument = rootRef.current?.ownerDocument ?? globalThis?.document;
const isPointerInsideReactTreeRef = React.useRef(false);
const onPointerDownCapture = React.useCallback(
(event: React.PointerEvent<RootElement>) => {
onPointerDownCaptureProp?.(event);
if (event.defaultPrevented) return;
const target = event.target as HTMLElement;
const nodes = getNodes();
const isInteractiveElement = nodes.some((node) =>
node.ref.current?.contains(target),
);
isPointerInsideReactTreeRef.current = isInteractiveElement;
},
[onPointerDownCaptureProp, getNodes],
);
React.useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
propsRef.current?.onEscapeKeyDown?.(event);
if (event.defaultPrevented) return;
store.setState("open", false);
}
if (event.key === "Tab") {
const focusableElements = getNodes()
.filter((node) => !node.disabled)
.map((node) => node.ref.current);
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const activeElement = ownerDocument.activeElement;
if (event.shiftKey) {
if (activeElement === firstElement) {
store.setState("open", false);
}
} else {
if (activeElement === lastElement) {
store.setState("open", false);
}
}
}
};
ownerDocument.addEventListener("keydown", onKeyDown);
return () => ownerDocument.removeEventListener("keydown", onKeyDown);
}, [open, propsRef, ownerDocument, store, getNodes]);
const onClickRef = React.useRef<() => void>(() => {});
React.useEffect(() => {
if (!open) return;
isPointerInsideReactTreeRef.current = false;
const onPointerDown = (event: PointerEvent) => {
if (event.target && !isPointerInsideReactTreeRef.current) {
const target = event.target as HTMLElement;
const isOutside = !rootRef.current?.contains(target);
function onDismiss() {
if (isOutside) {
const interactEvent = new CustomEvent(INTERACT_OUTSIDE, {
...EVENT_OPTIONS,
detail: { originalEvent: event },
}) as InteractOutsideEvent;
propsRef.current?.onInteractOutside?.(interactEvent);
if (interactEvent.defaultPrevented) return;
}
store.setState("open", false);
}
if (event.pointerType === "touch") {
ownerDocument.removeEventListener("click", onClickRef.current);
onClickRef.current = onDismiss;
ownerDocument.addEventListener("click", onClickRef.current, {
once: true,
});
} else {
onDismiss();
}
} else {
ownerDocument.removeEventListener("click", onClickRef.current);
}
isPointerInsideReactTreeRef.current = false;
};
const timerId = window.setTimeout(() => {
ownerDocument.addEventListener("pointerdown", onPointerDown);
}, 0);
return () => {
window.clearTimeout(timerId);
ownerDocument.removeEventListener("pointerdown", onPointerDown);
ownerDocument.removeEventListener("click", onClickRef.current);
};
}, [open, propsRef, ownerDocument, store]);
const contextValue = React.useMemo<SpeedDialContextValue>(
() => ({
contentId,
side,
onNodeRegister,
onNodeUnregister,
getNodes,
}),
[contentId, side, onNodeRegister, onNodeUnregister, getNodes],
);
const RootPrimitive = asChild ? Slot : "div";
return (
<StoreContext.Provider value={store}>
<SpeedDialContext.Provider value={contextValue}>
<RootPrimitive
data-slot="speed-dial"
{...rootProps}
ref={composedRefs}
className={cn("relative flex flex-col items-end", className)}
onPointerDownCapture={onPointerDownCapture}
/>
</SpeedDialContext.Provider>
</StoreContext.Provider>
);
}
function SpeedDialTrigger(props: React.ComponentProps<typeof Button>) {
const {
onClick: onClickProp,
className,
disabled,
id,
ref,
...triggerProps
} = props;
const store = useStoreContext(TRIGGER_NAME);
const { onNodeRegister, onNodeUnregister, contentId } =
useSpeedDialContext(TRIGGER_NAME);
const open = useStore((state) => state.open);
const instanceId = React.useId();
const triggerId = id ?? instanceId;
const triggerRef = React.useRef<TriggerElement>(null);
const composedRef = useComposedRefs(ref, triggerRef);
useIsomorphicLayoutEffect(() => {
onNodeRegister({
id: triggerId,
ref: triggerRef,
disabled: !!disabled,
});
return () => {
onNodeUnregister(triggerId);
};
}, [onNodeRegister, onNodeUnregister, triggerId, disabled]);
const onClick = React.useCallback(
(event: React.MouseEvent<TriggerElement>) => {
onClickProp?.(event);
if (event.defaultPrevented) return;
store.setState("open", !open);
},
[onClickProp, store, open],
);
return (
<Button
type="button"
role="button"
id={triggerId}
aria-haspopup="menu"
aria-expanded={open}
aria-controls={contentId}
data-slot="speed-dial-trigger"
data-state={getDataState(open)}
size="icon"
disabled={disabled}
{...triggerProps}
ref={composedRef}
className={cn("size-11 rounded-full", className)}
onClick={onClick}
/>
);
}
const SpeedDialItemImplContext = React.createContext<number | null>(null);
function useSpeedDialItemImplContext() {
return React.useContext(SpeedDialItemImplContext);
}
const speedDialContentVariants = cva(
"absolute z-50 flex gap-[var(--speed-dial-gap)]",
{
variants: {
side: {
top: "right-0 bottom-full mb-[var(--speed-dial-offset)] flex-col-reverse items-end",
bottom:
"top-full right-0 mt-[var(--speed-dial-offset)] flex-col items-end",
left: "right-full mr-[var(--speed-dial-offset)] flex-row-reverse items-center",
right: "left-full ml-[var(--speed-dial-offset)] flex-row items-center",
},
},
defaultVariants: {
side: "top",
},
},
);
interface SpeedDialContentProps
extends DivProps,
VariantProps<typeof speedDialContentVariants> {}
function SpeedDialContent(props: SpeedDialContentProps) {
const { asChild, className, style, children, ...contentProps } = props;
const open = useStore((state) => state.open);
const { contentId, side } = useSpeedDialContext(CONTENT_NAME);
const orientation =
side === "top" || side === "bottom" ? "vertical" : "horizontal";
const ContentPrimitive = asChild ? Slot : "div";
return (
<ContentPrimitive
id={contentId}
role="menu"
aria-orientation={orientation}
data-slot="speed-dial-content"
data-orientation={orientation}
data-side={side}
{...contentProps}
className={cn(speedDialContentVariants({ side, className }))}
style={
{
"--speed-dial-gap": DEFAULT_GAP,
"--speed-dial-offset": DEFAULT_OFFSET,
"--speed-dial-transform-origin": getTransformOrigin(side),
...style,
} as React.CSSProperties
}
>
{React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return child;
const totalChildren = React.Children.count(children);
const delay = open
? index * DEFAULT_ITEM_DELAY
: (totalChildren - index - 1) * DEFAULT_ITEM_DELAY;
return (
<SpeedDialItemImplContext.Provider value={delay}>
{child}
</SpeedDialItemImplContext.Provider>
);
})}
</ContentPrimitive>
);
}
const speedDialItemVariants = cva(
"flex items-center gap-2 transition-all duration-200 [transform-origin:var(--speed-dial-transform-origin)] [transition-delay:var(--speed-dial-delay)]",
{
variants: {
side: {
top: "justify-end",
bottom: "justify-end",
left: "flex-row-reverse justify-start",
right: "justify-start",
},
open: {
true: "translate-x-0 translate-y-0 scale-100 opacity-100",
false: "",
},
},
compoundVariants: [
{
side: "top",
open: false,
className: "translate-y-4 scale-0 opacity-0",
},
{
side: "bottom",
open: false,
className: "-translate-y-4 scale-0 opacity-0",
},
{
side: "left",
open: false,
className: "translate-x-4 scale-0 opacity-0",
},
{
side: "right",
open: false,
className: "-translate-x-4 scale-0 opacity-0",
},
],
defaultVariants: {
side: "top",
open: false,
},
},
);
const SpeedDialItemContext = React.createContext<string | null>(null);
function useSpeedDialItemContext(consumerName: string) {
const context = React.useContext(SpeedDialItemContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``);
}
return context;
}
function SpeedDialItem(props: DivProps) {
const { asChild, className, style, children, ...itemProps } = props;
const open = useStore((state) => state.open);
const { side } = useSpeedDialContext(ITEM_NAME);
const delay = useSpeedDialItemImplContext() ?? 0;
const labelId = React.useId();
const ItemPrimitive = asChild ? Slot : "div";
return (
<SpeedDialItemContext.Provider value={labelId}>
<ItemPrimitive
role="none"
data-slot="speed-dial-item"
data-state={getDataState(open)}
{...itemProps}
className={cn(speedDialItemVariants({ side, open, className }))}
style={
{
"--speed-dial-delay": `${delay}ms`,
"--speed-dial-transform-origin": getTransformOrigin(side),
...style,
} as React.CSSProperties
}
>
{children}
</ItemPrimitive>
</SpeedDialItemContext.Provider>
);
}
interface SpeedDialActionProps
extends Omit<React.ComponentProps<typeof Button>, "onSelect"> {
onSelect?: (event: Event) => void;
}
function SpeedDialAction(props: SpeedDialActionProps) {
const {
onSelect,
onClick: onClickProp,
className,
disabled,
id,
ref,
...actionProps
} = props;
const store = useStoreContext(ACTION_NAME);
const { onNodeRegister, onNodeUnregister } = useSpeedDialContext(ACTION_NAME);
const labelId = useSpeedDialItemContext(ACTION_NAME);
const instanceId = React.useId();
const actionId = id ?? instanceId;
const actionRef = React.useRef<ActionElement>(null);
const composedRefs = useComposedRefs(ref, actionRef);
useIsomorphicLayoutEffect(() => {
onNodeRegister({
id: actionId,
ref: actionRef,
disabled: !!disabled,
});
return () => {
onNodeUnregister(actionId);
};
}, [onNodeRegister, onNodeUnregister, actionId, disabled]);
const onClick = React.useCallback(
(event: React.MouseEvent<ActionElement>) => {
onClickProp?.(event);
if (event.defaultPrevented) return;
const action = actionRef.current;
if (!action) return;
const actionSelectEvent = new CustomEvent(ACTION_SELECT, EVENT_OPTIONS);
action.addEventListener(ACTION_SELECT, (event) => onSelect?.(event), {
once: true,
});
action.dispatchEvent(actionSelectEvent);
if (actionSelectEvent.defaultPrevented) return;
store.setState("open", false);
},
[onClickProp, onSelect, store],
);
return (
<Button
type="button"
role="menuitem"
id={actionId}
aria-labelledby={labelId}
data-slot="speed-dial-action"
variant="outline"
size="icon"
disabled={disabled}
ref={composedRefs}
{...actionProps}
className={cn("size-11 shrink-0 rounded-full shadow-md", className)}
onClick={onClick}
/>
);
}
function SpeedDialLabel({ asChild, className, ...props }: DivProps) {
const labelId = useSpeedDialItemContext(LABEL_NAME);
const LabelPrimitive = asChild ? Slot : "div";
return (
<LabelPrimitive
id={labelId}
data-slot="speed-dial-label"
className={cn(
"pointer-events-none whitespace-nowrap rounded-md bg-popover px-2 py-1 text-popover-foreground text-sm shadow-md",
className,
)}
{...props}
/>
);
}
export {
SpeedDialRoot as Root,
SpeedDialTrigger as Trigger,
SpeedDialContent as Content,
SpeedDialItem as Item,
SpeedDialAction as Action,
SpeedDialLabel as Label,
//
SpeedDialRoot as SpeedDial,
SpeedDialTrigger,
SpeedDialContent,
SpeedDialItem,
SpeedDialAction,
SpeedDialLabel,
};Layout
Import the parts, and compose them together.
import * as SpeedDial from "@/components/ui/speed-dial";
return (
<SpeedDial.Root>
<SpeedDial.Trigger />
<SpeedDial.Content>
<SpeedDial.Item>
<SpeedDial.Label />
<SpeedDial.Action />
</SpeedDial.Item>
</SpeedDial.Content>
</SpeedDial.Root>
)Examples
With Labels
Display visible labels next to each action button for better discoverability.
"use client";
import { Copy, Heart, Plus, Share2 } from "lucide-react";
import { toast } from "sonner";
import {
SpeedDial,
SpeedDialAction,
SpeedDialContent,
SpeedDialItem,
SpeedDialLabel,
SpeedDialTrigger,
} from "@/components/ui/speed-dial";
export function SpeedDialLabelsDemo() {
return (
<SpeedDial>
<SpeedDialTrigger className="transition-transform duration-200 ease-out data-[state=closed]:rotate-0 data-[state=open]:rotate-135">
<Plus />
</SpeedDialTrigger>
<SpeedDialContent>
<SpeedDialItem>
<SpeedDialLabel>Share</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Shared")}>
<Share2 />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel>Copy</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Copied")}>
<Copy />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel>Like</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Liked")}>
<Heart />
</SpeedDialAction>
</SpeedDialItem>
</SpeedDialContent>
</SpeedDial>
);
}Controlled
Use the open and onOpenChange props to control the speed dial state programmatically.
"use client";
import { Copy, Heart, Plus, Share2, X } from "lucide-react";
import * as React from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
SpeedDial,
SpeedDialAction,
SpeedDialContent,
SpeedDialItem,
SpeedDialLabel,
SpeedDialTrigger,
} from "@/components/ui/speed-dial";
export function SpeedDialControlledDemo() {
const [open, setOpen] = React.useState(false);
return (
<div className="flex items-center gap-4">
<SpeedDial open={open} onOpenChange={setOpen}>
<SpeedDialTrigger className="transition-transform duration-200 ease-out data-[state=closed]:rotate-0 data-[state=open]:rotate-135">
{open ? <X /> : <Plus />}
</SpeedDialTrigger>
<SpeedDialContent>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Share</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Shared")}>
<Share2 />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Copy</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Copied")}>
<Copy />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Like</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Liked")}>
<Heart />
</SpeedDialAction>
</SpeedDialItem>
</SpeedDialContent>
</SpeedDial>
<Button variant="outline" onClick={() => setOpen(!open)}>
{open ? "Close" : "Open"}
</Button>
</div>
);
}Sides
The speed dial can expand in different directions using the side prop.
"use client";
import { Copy, Heart, Plus, Share2 } from "lucide-react";
import { toast } from "sonner";
import {
SpeedDial,
SpeedDialAction,
SpeedDialContent,
SpeedDialItem,
SpeedDialLabel,
SpeedDialTrigger,
} from "@/components/ui/speed-dial";
const sides = ["top", "right", "bottom", "left"] as const;
export function SpeedDialSideDemo() {
return (
<div className="grid grid-cols-2 gap-24">
{sides.map((side) => (
<div key={side} className="flex flex-col items-center gap-2">
<span className="text-muted-foreground text-sm capitalize">
{side}
</span>
<SpeedDial side={side}>
<SpeedDialTrigger className="transition-transform duration-200 ease-out data-[state=closed]:rotate-0 data-[state=open]:rotate-135">
<Plus />
</SpeedDialTrigger>
<SpeedDialContent>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Share</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Shared")}>
<Share2 />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Copy</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Copied")}>
<Copy />
</SpeedDialAction>
</SpeedDialItem>
<SpeedDialItem>
<SpeedDialLabel className="sr-only">Like</SpeedDialLabel>
<SpeedDialAction onSelect={() => toast.success("Liked")}>
<Heart />
</SpeedDialAction>
</SpeedDialItem>
</SpeedDialContent>
</SpeedDial>
</div>
))}
</div>
);
}API Reference
Root
The main container component for the speed dial.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-state] | "open" | "closed" |
Trigger
The button that toggles the speed dial open/closed state.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-state] | "open" | "closed" |
Content
The container for the action items that appears when the speed dial is open.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-state] | "open" | "closed" |
[data-orientation] | "horizontal" | "vertical" |
[data-side] | "top" | "right" | "bottom" | "left" |
| CSS Variable | Description |
|---|---|
--speed-dial-gap | Gap between action items. Defaults to 0.5rem. |
--speed-dial-offset | Offset distance from the trigger. Defaults to 0.5rem. |
--speed-dial-transform-origin | Transform origin for animations based on the side. |
Item
A wrapper for each action and its associated label.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-state] | "open" | "closed" |
| CSS Variable | Description |
|---|---|
--speed-dial-delay | Animation delay for staggered item appearance. |
--speed-dial-transform-origin | Transform origin for item animations. |
Action
An interactive button within the speed dial that triggers an action.
Prop
Type
Label
A text label that describes the associated action.
Prop
Type
Accessibility
Keyboard Interactions
| Key | Description |
|---|---|
| EnterSpace | When focus is on the trigger, toggles the speed dial open/closed. |
| Escape | Closes the speed dial and returns focus to the trigger. |
| Tab | Moves focus between actions. Closes the speed dial when focus moves out. |
| Shift + Tab | Moves focus to the previous action. Closes the speed dial when focus moves out. |