Lumen Docs

Tabs

Underlined tab bar. The single tab pattern in Lumen. Located at apps/web/components/ui/tabs.tsx.

Usage

const [tab, setTab] = useState<"threads" | "files" | "members">("threads");

<Tabs
  value={tab}
  onChange={setTab}
  tabs={[
    { key: "threads", label: "Threads", count: threads.length },
    { key: "files",   label: "Files",   count: docs.length },
    { key: "members", label: "Members", count: members.length },
  ]}
/>

{tab === "threads" && <ThreadsList />}
{tab === "files"   && <FilesList />}
{tab === "members" && <MembersList />}

Props

interface Tab<K extends string = string> {
  key: K;
  label: string;
  count?: number;      // optional count shown after label, muted
  badge?: ReactNode;   // optional badge (e.g. notification dot)
  disabled?: boolean;
}

interface TabsProps<K extends string = string> {
  tabs: Array<Tab<K>>;
  value: K;
  onChange: (key: K) => void;
  className?: string;
}

Visual

  • Underline on active tab (border-b-2 border-accent, with -mb-px so it merges with parent border)
  • Accent color text on active
  • Muted text on inactive, hovers to primary

With count

{ key: "users", label: "Users", count: 42 }

Renders Users 42 with the count muted. Auto-formatted; no manual span needed.

Disabled tab

{ key: "billing", label: "Billing", disabled: true }

Grayed out, unclickable.

Common pattern: tabs + filter bar

<div className="flex items-center border-b border-line mb-4">
  <Tabs value={tab} onChange={setTab} tabs={tabs} />

  {/* Right-aligned filters, only visible on certain tabs */}
  {tab === "users" && (
    <div className="ml-auto flex items-center gap-2">
      <SearchInput />
    </div>
  )}
</div>

The <Tabs> component has its own border-b. The parent border-b is redundant but kept for consistency — will be removed once every page uses Tabs exclusively.

Don't

  • Don't hand-roll tab buttons. This is the canonical primitive. If you need a new variant (e.g., vertical tabs), extend this component.
  • Don't switch value type without updating all three tabs. TypeScript generics keep onChange typed, but typeof tab must match.
  • Don't conditionally render the entire <Tabs> — just hide/disable individual tabs via disabled: true.