Warren UI

Tabs

Zero-dependency tab list with active underline.

12 REQUESTS QUEUED · 3 IN REVIEW

Source
"use client"

import { Tabs } from "@/registry/warren/tabs/tabs"

export default function TabsDemo() {
  return (
    <Tabs
      items={[
        {
          id: "intake",
          label: "Intake",
          content: (
            <p className="font-mono text-xs text-muted-foreground">
              12 REQUESTS QUEUED · 3 IN REVIEW
            </p>
          ),
        },
        {
          id: "linking",
          label: "Linking",
          content: (
            <p className="font-mono text-xs text-muted-foreground">
              5,208 EDGES RESOLVED · 1,003 PENDING
            </p>
          ),
        },
        {
          id: "export",
          label: "Export",
          content: (
            <p className="font-mono text-xs text-muted-foreground">
              LAST RUN 14:32:07 UTC · 0 FAILURES
            </p>
          ),
        },
      ]}
    />
  )
}

Installation

npx shadcn@latest add https://warren.eduard3v.com/r/tabs.json

Usage

import { Tabs } from "@/components/ui/tabs"
<Tabs
  items={[
    { id: "intake", label: "Intake", content: <p>Intake panel</p> },
    { id: "linking", label: "Linking", content: <p>Linking panel</p> },
  ]}
/>

Uncontrolled by default: pass defaultValue to select an initial tab, or leave it out to start on the first item. Each item takes an id, a label (string or node), and content rendered inside the active tab panel. role/aria wiring (tablist, tab, tabpanel, aria-selected) is built in, and label sets the tablist's accessible name (default "tabs").

Keyboard navigation

The tablist implements the WAI-ARIA tabs pattern with automatic activation: ArrowRight/ArrowDown and ArrowLeft/ArrowUp move focus to and select the next/previous tab (wrapping around), and Home/End jump to the first/last tab. Only the active tab is in the tab order (tabindex roving focus), so the whole list is reachable with Tab once plus the arrow keys.

Controlled

LINKING PANEL · SHOWS EDGE RESOLUTION STATE

active: linking

Source
"use client"

import { useState } from "react"
import { Tabs } from "@/registry/warren/tabs/tabs"

export default function TabsControlled() {
  const [active, setActive] = useState("linking")

  return (
    <div className="w-full space-y-2">
      <Tabs
        value={active}
        onValueChange={setActive}
        items={[
          {
            id: "intake",
            label: "Intake",
            content: (
              <p className="font-mono text-xs text-muted-foreground">
                INTAKE PANEL · FILTERS BY CREATED_AT
              </p>
            ),
          },
          {
            id: "linking",
            label: "Linking",
            content: (
              <p className="font-mono text-xs text-muted-foreground">
                LINKING PANEL · SHOWS EDGE RESOLUTION STATE
              </p>
            ),
          },
          {
            id: "export",
            label: "Export",
            content: (
              <p className="font-mono text-xs text-muted-foreground">
                EXPORT PANEL · TARGETS S3 BUCKET
              </p>
            ),
          },
        ]}
      />
      <p className="font-mono text-[10px] uppercase tracking-wide text-muted-foreground">
        active: {active}
      </p>
    </div>
  )
}

Pass value together with onValueChange to own the active tab. The selected id is the only argument to onValueChange, so it composes directly with useState or any store.

const [active, setActive] = useState("linking")

<Tabs
  value={active}
  onValueChange={setActive}
  items={[{ id: "linking", label: "Linking", content: <p>Linking panel</p> }]}
/>

API Reference

Prop

Type

TabItem

Prop

Type

On this page