Animated tabs
FreeTabs with an expanding or morphing indicator, including closable tabs.
pnpm dlx shadcn@latest add https://caliperui.site/r/animated-tabs.jsonUnread messages appear here.
Use Alt and left or right arrow to reorder a focused tab.
Unread messages appear here.
When to use #
Use it for
- Workspaces where people open and close several documents.
- A more expressive tab bar for showcase pages.
Skip it when
- A plain set of views; use tabs.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/animated-tabs.json
Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from ExpandableTabs and MorphingTabs.
import { Calendar, FileText, Inbox } from "lucide-react" import { useState } from "react" import { ExpandableTabs, MorphingTabs, type AnimatedTab } from "@/components/ui/animated-tabs" const tabItems: AnimatedTab[] = [ { id: "inbox", label: "Inbox", icon: <Inbox aria-hidden="true" />, content: <p className="text-sm">Unread messages appear here.</p> }, { id: "files", label: "Files", icon: <FileText aria-hidden="true" />, content: <p className="text-sm">Recent files appear here.</p> }, { id: "calendar", label: "Calendar", icon: <Calendar aria-hidden="true" />, content: <p className="text-sm">Upcoming events appear here.</p> }, ]; export function ExpandableTabsDemo() { const [value, setValue] = useState("inbox"); const [items, setItems] = useState(tabItems); return ( <div className="grid w-full max-w-lg gap-6"> <ExpandableTabs label="Workspace" items={tabItems} value={value} onValueChange={setValue} /> <MorphingTabs label="Open documents" items={items} value={value} onValueChange={setValue} onItemsChange={setItems} /> </div> ); }
API reference #
ExpandableTabs
component
MorphingTabs
component
Accessibility #
It exposes tab and status semantics, with aria-hidden, aria-label and aria-describedby kept in step with state. Changes that matter are announced through a live region.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.