caliper uidocs

Folder tabs

Free

Tabs 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.

Installs components/ui/folder-tabs.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add @caliper/folder-tabs

Overview

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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
tabs *readonly FolderTab[]—
label *string—Accessible name of the tab list.
valuestring—The open tab. Controlled when set; the first tab is open by default.
defaultValuestring—
onValueChange((id: string) => void)—
statusDepthStatus"ready"
messagesDepthMessages—
onRetry(() => void)—
disabledboolean—
classNamestring—

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.

KeyBehaviour
→Moves to the next item or value.
←Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.

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.