Tabs (Lite)

Tabs whose triggers are links to pages, without JavaScript.

LiteUnreleased
Overview
View your key metrics and recent project activity. Track progress across all your active projects.
You have 12 active projects and 3 pending tasks.
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  TabsLite,
  TabsLiteContent,
  TabsLiteList,
  TabsLiteTrigger,
} from "@/components/ui/tabs-lite"

export default function TabsLiteDemo() {
  return (
    <TabsLite value="overview" class="w-[400px]">
      <TabsLiteList aria-label="Project">
        <TabsLiteTrigger value="overview" href="#overview">
          Overview
        </TabsLiteTrigger>
        <TabsLiteTrigger value="analytics" href="#analytics">
          Analytics
        </TabsLiteTrigger>
        <TabsLiteTrigger value="reports" href="#reports">
          Reports
        </TabsLiteTrigger>
        <TabsLiteTrigger value="settings" href="#settings">
          Settings
        </TabsLiteTrigger>
      </TabsLiteList>
      <TabsLiteContent>
        <Card>
          <CardHeader>
            <CardTitle>Overview</CardTitle>
            <CardDescription>
              View your key metrics and recent project activity. Track progress
              across all your active projects.
            </CardDescription>
          </CardHeader>
          <CardContent class="text-sm text-muted-foreground">
            You have 12 active projects and 3 pending tasks.
          </CardContent>
        </Card>
      </TabsLiteContent>
    </TabsLite>
  )
}

About

Tabs (Lite) is a hand-written alternative to shadcn/ui's Tabs for multi-page apps: each tab is a page with its own URL, like the tabs of a repository or a settings page. The triggers are links in a navigation landmark, and the server renders only the current page's content, so it needs no JavaScript, and the back button, bookmarks and shared links work. It keeps upstream's look in every style.

Use Tabs to switch panels within one page.

Installation

pnpm dlx shadcnui-hono-jsx@latest add tabs-lite

Usage

import {
  TabsLite,
  TabsLiteContent,
  TabsLiteList,
  TabsLiteTrigger,
} from "@/components/ui/tabs-lite"
app.get("/settings/:tab", (c) => {
  const tab = c.req.param("tab")
  return c.render(
    <TabsLite value={tab}>
      <TabsLiteList aria-label="Settings">
        <TabsLiteTrigger value="account" href="/settings/account">
          Account
        </TabsLiteTrigger>
        <TabsLiteTrigger value="password" href="/settings/password">
          Password
        </TabsLiteTrigger>
      </TabsLiteList>
      <TabsLiteContent>
        {tab === "account" ? <AccountForm /> : <PasswordForm />}
      </TabsLiteContent>
    </TabsLite>
  )
})

The trigger whose value matches TabsLite's value is the current page: it gets aria-current="page" and upstream's active style. Give the list an aria-label, since a page can have several navigation landmarks. Query links (href="?tab=password") work as well as paths. The examples on this page link to fragments, so their current tab does not change.

Line

import {
  TabsLite,
  TabsLiteList,
  TabsLiteTrigger,
} from "@/components/ui/tabs-lite"

export default function TabsLiteLine() {
  return (
    <TabsLite value="overview">
      <TabsLiteList variant="line" aria-label="Project">
        <TabsLiteTrigger value="overview" href="#overview">
          Overview
        </TabsLiteTrigger>
        <TabsLiteTrigger value="analytics" href="#analytics">
          Analytics
        </TabsLiteTrigger>
        <TabsLiteTrigger value="reports" href="#reports">
          Reports
        </TabsLiteTrigger>
      </TabsLiteList>
    </TabsLite>
  )
}

Vertical

import {
  TabsLite,
  TabsLiteList,
  TabsLiteTrigger,
} from "@/components/ui/tabs-lite"

export default function TabsLiteVertical() {
  return (
    <TabsLite value="account" orientation="vertical">
      <TabsLiteList aria-label="Settings">
        <TabsLiteTrigger value="account" href="#account">
          Account
        </TabsLiteTrigger>
        <TabsLiteTrigger value="password" href="#password">
          Password
        </TabsLiteTrigger>
        <TabsLiteTrigger value="notifications" href="#notifications">
          Notifications
        </TabsLiteTrigger>
      </TabsLiteList>
    </TabsLite>
  )
}

Disabled

A disabled trigger renders without its href, as a link that is unavailable.

import {
  TabsLite,
  TabsLiteList,
  TabsLiteTrigger,
} from "@/components/ui/tabs-lite"

export default function TabsLiteDisabled() {
  return (
    <TabsLite value="home">
      <TabsLiteList aria-label="Sections">
        <TabsLiteTrigger value="home" href="#home">
          Home
        </TabsLiteTrigger>
        <TabsLiteTrigger value="settings" href="#settings" disabled>
          Disabled
        </TabsLiteTrigger>
      </TabsLiteList>
    </TabsLite>
  )
}

Notes

  • A lite alternative to tabs (not a port) for multi-page apps, with no JavaScript: the triggers are links (<a href>) in a <nav>, each to its tab's page, and the server renders only the current page's content. The trigger whose value matches TabsLite's value gets aria-current="page" and upstream's active style. Use the tabs port to switch panels within one page.
  • Links rather than the ARIA tabs pattern: there are no tablist, tab or tabpanel roles, every link is a tab stop (the arrow keys do not move between them), and a disabled trigger renders without href (role="link" with aria-disabled). There is no defaultValue or activateOnFocus, and TabsLiteContent takes no value.
  • Accepts class instead of className.
  • The port: Tabs.
  • Upstream documentation: shadcn/ui Tabs. See also Differences from shadcn/ui.