Floating Toolbar

Compact action bar that floats above a selection — primary / ghost / destructive variants. Install Floating Toolbar from the VLLNT UI registry with the shadcn CLI.

Report a bug

Compact action bar that floats above a selection — primary / ghost / destructive variants. Part of the data-display family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add Floating Toolbar to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/floating-toolbar.json

Source

"use client"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; import { cn } from "../../lib/utils"; /** * One toolbar action. * * @public */ export type FloatingToolbarAction = { /** Optional aria-label override (defaults to the visible label). */ ariaLabel?: string; /** When `true`, renders dimmed and ignores clicks. */ disabled?: boolean; /** Optional leading glyph. */ glyph?: ReactNode; /** Stable identifier. */ id: string; /** Visible label. */ label: ReactNode; /** Click handler. */ onActivate: () => void; /** Optional emphasis level. Defaults to `"ghost"`. */ variant?: "destructive" | "ghost" | "primary"; }; const VARIANT_CLASSES: Record< NonNullable<FloatingToolbarAction["variant"]>, string > = { destructive: "border-red-300 bg-red-500/10 text-red-700 hover:bg-red-500/20 dark:text-red-300", ghost: "border-border bg-background text-foreground hover:bg-accent", primary: "border-primary bg-primary text-primary-foreground hover:bg-primary/90", }; /** * Localizable strings. * * @public */ export type FloatingToolbarLabels = { /** Aria-label for the toolbar. Defaults to `"Selection actions"`. */ region?: string; }; const DEFAULT_LABELS = { region: "Selection actions", } as const satisfies Required<FloatingToolbarLabels>; /** * Props for {@link FloatingToolbar}. * * @public */ export type FloatingToolbarProps = { /** Toolbar actions in render order. */ actions: FloatingToolbarAction[]; /** Localizable strings. */ labels?: FloatingToolbarLabels; /** X coordinate in container px (left edge of the toolbar). */ x: number; /** Y coordinate in container px (bottom edge of the toolbar — the bar floats above this point). */ y: number; } & Omit<ComponentPropsWithoutRef<"div">, "style">; /** * Compact action bar that floats above a selection. Pair with * {@link SelectionHalo} — surface the toolbar at the halo's top edge so * the user sees the available actions for the current selection. * * @example * ```tsx * <FloatingToolbar * x={120} * y={80} * actions={[ * { id: "rename", label: "Rename", onActivate: rename, variant: "primary" }, * { id: "duplicate", label: "Duplicate", onActivate: duplicate }, * { id: "delete", label: "Delete", onActivate: remove, variant: "destructive" }, * ]} * /> * ``` * * @public */ export const FloatingToolbar = ({ ref, ...props }: FloatingToolbarProps & { ref?: React.Ref<HTMLDivElement> }) => { const { actions, className, labels, x, y, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; return ( <div aria-label={resolvedLabels.region} className={cn( "absolute z-30 flex -translate-y-full items-center gap-1 rounded-md border border-border bg-background/95 p-1 shadow-md backdrop-blur", className, )} data-floating-toolbar ref={ref} role="toolbar" style={{ left: `${x.toString()}px`, top: `${y.toString()}px` }} {...rest} > {actions.map((action) => { const variant = action.variant ?? "ghost"; const handleActivateToolbarAction = (): void => { action.onActivate(); }; return ( <button aria-label={action.ariaLabel ?? undefined} className={cn( "inline-flex h-7 items-center gap-1 rounded-md border px-2 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring", VARIANT_CLASSES[variant], action.disabled ? "cursor-not-allowed opacity-50" : "", )} data-action-id={action.id} data-variant={variant} disabled={action.disabled} key={action.id} onClick={handleActivateToolbarAction} type="button" > {action.glyph ? ( <span aria-hidden="true" className="inline-flex size-3"> {action.glyph} </span> ) : null} <span>{action.label}</span> </button> ); })} </div> ); }; FloatingToolbar.displayName = "FloatingToolbar";

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0