Expandable Toolbar
ericts-ui/expandable-toolbarFreeComponent
A controlled or uncontrolled toolbar primitive that measures its action slot and expands from either side.
Live preview
live · rendered by the registry
Rendered by ericts-ui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ui.ericts.com/r/expandable-toolbar.jsonSource
1"use client";23import {4 Children,5 type ComponentPropsWithoutRef,6 type CSSProperties,7 type KeyboardEvent,8 type ReactNode,9 type Ref,10 useCallback,11 useEffect,12 useId,13 useLayoutEffect,14 useRef,15 useState,16} from "react";17import { AnimatePresence, motion, useReducedMotion } from "motion/react";1819import { Button } from "@/components/ui/button";20import { cn } from "@/lib/utils";2122const EASE_OUT = [0.22, 1, 0.36, 1] as const;23const DEFAULT_CONTENT_MAX_WIDTH = "min(32rem, calc(100vw - 2rem))";2425const useIsomorphicLayoutEffect =26 typeof window === "undefined" ? useEffect : useLayoutEffect;2728const TOOLBAR_TRANSITION = {29 width: { duration: 0.2, ease: EASE_OUT },30 opacity: { duration: 0.14, ease: EASE_OUT },31} as const;32const TRIGGER_SURFACE_TRANSITION = { duration: 0.18, ease: EASE_OUT } as const;33// An icon swap is a tiny state change, so keep it snappy: ease-out, well34// under 150ms — the copy button's copy → check treatment, trimmed a touch35// because the swap runs exit-then-enter and the perceived time is doubled.36const TRIGGER_ICON_TRANSITION = {37 duration: 0.1,38 ease: [0.215, 0.61, 0.355, 1],39} as const;40const TRIGGER_ICON_VARIANTS = {41 hidden: { opacity: 0, scale: 0.5 },42 visible: { opacity: 1, scale: 1 },43} as const;44const TOOLBAR_PADDING = 2;45/** Must match the toolbar surface's `border` class. */46const TOOLBAR_BORDER_WIDTH = 1;47const TRIGGER_RADIUS_OPEN = 8;48const TRIGGER_RADIUS_CLOSED = TRIGGER_RADIUS_OPEN + TOOLBAR_PADDING;4950type ExpandableToolbarSide = "start" | "end" | "center";51type ExpandableToolbarAnchor = "toolbar" | "trigger";52type ExpandableToolbarTriggerProps = ComponentPropsWithoutRef<"button"> & {53 "data-state": "open" | "closed";54};5556export type ExpandableToolbarClassNames = {57 triggerWrapper?: string;58 trigger?: string;59 triggerSeparator?: string;60 panel?: string;61 content?: string;62};6364export type ExpandableToolbarTriggerRenderProps = {65 open: boolean;66 disabled: boolean;67 label: string;68 controlsId: string;69 triggerProps: ExpandableToolbarTriggerProps;70};7172type ExpandableToolbarBaseProps = Omit<73 ComponentPropsWithoutRef<"div">,74 "children" | "defaultValue" | "onChange"75> & {76 /** Controlled open state. */77 open?: boolean;78 /** Initial open state for uncontrolled usage. */79 defaultOpen?: boolean;80 /** Called whenever the toolbar requests an open-state change. */81 onOpenChange?: (open: boolean) => void;82 /**83 * Which side of the trigger the content should expand into. `center` splits84// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from ericts-ui
All 38 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Adaptive Draweradaptive-drawer | ericts-ui | Components | Free | 2 deps | |
| Check Animationcheck-animation | ericts-ui | Components | Free | no deps · 2 files | |
| Context Cursorcontext-cursor | ericts-ui | Components | Free | 1 dep | |
| Copy Buttoncopy-button | ericts-ui | Components | Free | 2 deps | |
| Expandable Dialogexpandable-modal | ericts-ui | Components | Free | 1 dep | |
| Expandable Tabsexpandable-tabs | ericts-ui | Components | Free | 1 dep | |
| Expanding Panelexpanding-panel | ericts-ui | Components | Free | 2 deps | |
| Expanding Segmented Tabsexpanding-segmented-tabs | ericts-ui | Components | Free | 1 dep |
