Folder tabs
FreeTabs as folders in a drawer. The chosen folder comes to the front and the rest sit behind it with their tabs showing, so you can see how many there are and where you are. Distance back is the number of steps from the open tab.
pnpm dlx shadcn@latest add @caliper/folder-tabsOverview
Workspace at a glance
- Seats12 of 15
- PlanTeam
- RegionEU (Frankfurt)
Billing
Next invoice and payment method
- Next invoiceNov 01 · $199.00
- CardVisa ending 4821
- Tax IDAdded
Members
Who can sign in
- Owners2
- Admins3
- Pending invites4
Audit
Everything that changed
- Last 24 hours18 events
- Last exportOct 12
- Retention1 year
When to use #
Use it for
- A handful of related sections such as settings, where place in the set is worth showing.
- Tabs that should feel like one object rather than a toolbar.
Skip it when
- More than about eight sections; use the tabs or a sidebar.
- Tabs that need to scroll sideways.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- A real tablist
- Arrows, Home and End move and select. Each panel is labelled by its tab and the other folders are hidden and inert.
Styling
- Tabs never move sideways
- The row of labels stays put; only the folders change place, so you do not have to find a tab again after pressing it.
Usage
- The stack is as tall as the tallest panel
- Switching never makes the page jump. Long tab names are cut with an ellipsis and keep their full name as a title.
Installation #
pnpm dlx shadcn@latest add @caliper/folder-tabs
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.
import { FolderTabs, type FolderTab } from "@/components/ui/folder-tabs" const rows = (items: [string, string][]) => ( <ul className="m-0 mt-1 grid list-none gap-0 p-0"> {items.map(([name, value]) => ( <li key={name} className="flex justify-between gap-2.5 border-b border-border py-2 text-[13px]"><span className="text-muted-foreground">{name}</span><b className="text-right font-semibold">{value}</b></li> ))} </ul> ); const section = (title: string, text: string, items: [string, string][]) => ( <div className="grid gap-2"> <h4 className="m-0 text-[17px] font-bold tracking-tight">{title}</h4> <p className="m-0 max-w-[44ch] text-[13px] text-muted-foreground">{text}</p> {rows(items)} </div> ); const settings: FolderTab[] = [ { id: "overview", label: "Overview", content: section("Overview", "Workspace at a glance", [["Seats", "12 of 15"], ["Plan", "Team"], ["Region", "EU (Frankfurt)"]]) }, { id: "billing", label: "Billing", content: section("Billing", "Next invoice and payment method", [["Next invoice", "Nov 01 · $199.00"], ["Card", "Visa ending 4821"], ["Tax ID", "Added"]]) }, { id: "members", label: "Members", content: section("Members", "Who can sign in", [["Owners", "2"], ["Admins", "3"], ["Pending invites", "4"]]) }, { id: "audit", label: "Audit", content: section("Audit", "Everything that changed", [["Last 24 hours", "18 events"], ["Last export", "Oct 12"], ["Retention", "1 year"]]) }, ]; export function FolderTabsDemo() { return ( <div className="w-full max-w-xl"> <FolderTabs label="Workspace settings" tabs={settings} defaultValue="overview" /> </div> ); }
API reference #
FolderTabs
component
Accessibility #
It exposes tablist, tab and tabpanel semantics, with aria-label, aria-owns, aria-selected, aria-controls and aria-labelledby kept in step with state.
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.