# Tabs

> Tabs: anatomy, keyboard behavior, state, and theming in Askr.

Source: [https://askrjs.com/docs/components/tabs](https://askrjs.com/docs/components/tabs)

Status: stable. Packages: @askrjs/themes/tabs.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

```tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@askrjs/themes/components';

<Tabs value={tab()} onValueChange={setTab}>
  <TabsList aria-label="Project sections">
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="activity">Activity</TabsTrigger>
  </TabsList>
  <TabsContent value="overview"><ProjectOverview /></TabsContent>
  <TabsContent value="activity"><ProjectActivity /></TabsContent>
</Tabs>
```

## Purpose

This page is themes-only, since @askrjs/ui has no tabs primitive to pair it with — and "tabs" here actually names two unrelated things. Tabs/Tab is a router-aware navigation bar built on Link, for switching between routes (a settings page with URL-backed sections is the canonical case). TabsList/TabsTrigger/TabsContent, by contrast, are presentational parts shaped for the classic tabbed-panel pattern; they carry no state or panel-switching logic of their own, so wiring that up is left to you.

## Install and import

Both flavors come from the same place: import Tabs and Tab, or TabsList, TabsTrigger, and TabsContent, from @askrjs/themes/tabs (which, like every themes subpath, is really just the shared components barrel). Tab's props extend your router's LinkProps, so it needs to run inside your app's router context to navigate correctly. TabsList and friends have no such dependency — they're plain elements you can drop anywhere.

## Live examples

For route-based tabs, wrap a Tabs (a styled nav element) around a Tab per destination, passing `active` or letting `match="prefix"` decide highlighting based on the current URL. For a panel-switcher, render TabsList containing one TabsTrigger per option, and pair each with a TabsContent — but you're responsible for tracking which one is selected and swapping data-state or conditionally rendering the matching panel yourself.

## Anatomy

Tabs/Tab is flat: a nav-like Tabs wrapper around any number of Tab links, no content panels involved because it's driving navigation rather than showing/hiding content. TabsList/TabsTrigger/TabsContent looks like the ARIA tabs pattern, but the role coverage is partial: `TabsList` gets `role="tablist"` and `TabsContent` gets `role="tabpanel"`, but `TabsTrigger` itself sets no `role` at all — you need to add `role="tab"` yourself if you want the full pattern. There's no hidden Context tracking a current tab or associating a trigger with its panel either way.

## State model

For Tab, "selected" state is really route state — but Tab computes it for you: it compares the current URL against its own destination (respecting the `match` prop, `"prefix"` or `"exact"`) and sets `aria-current="page"` automatically when active, the same way the rest of the shared nav-link machinery does. You only need to pass `active` yourself if you want to override that computed value. For TabsTrigger/TabsContent, by contrast, state really is entirely your responsibility — store the selected tab id in your own component state and use it to set data-state="active" on the current trigger and conditionally render (or hide) the matching content.

## Keyboard and accessibility

Tab renders as a real link, so keyboard behavior — Tab key to move focus, Enter to navigate — comes for free from the browser and your router, and `aria-current="page"` on the active link is what screen readers announce as the current section; that active state is computed automatically off the current route (unless you pass `active` to override it), not something you have to track yourself. TabsList and TabsContent apply `tablist`/`tabpanel` roles, but TabsTrigger gets no role from the component — add `role="tab"` yourself, along with `aria-selected` on the active trigger and `aria-controls`/`id` pairing between a trigger and its panel, if you want full tab-pattern accessibility. Arrow-key navigation between triggers isn't provided either — you'd need to add that yourself if the panel pattern requires it.

## Styling and tokens

Tab and Tabs style through .tab/.tabs classes (with [data-slot="tab"]/[data-slot="tabs"] as a fallback), using data-active and aria-current="page" as the hooks for the active-state look. TabsList, TabsTrigger, and TabsContent live in a separate stylesheet keyed off [data-slot="tabs-list"], [data-slot="tabs-trigger"], and [data-slot="tabs-content"], with the active trigger styled off either data-state="active" or data-active="true" — pick whichever attribute your own state logic sets. Both sets read from the same shared --ak-* tokens as the rest of the theme, so they stay visually consistent with everything else.

## API

Tab's props are LinkProps merged with `active` and `match` ("prefix" | "exact"), so anything your router's Link accepts is fair game here too. Tabs itself takes just `children`, plus an `asChild` escape hatch if you need to render a different wrapper element. TabsList, TabsTrigger, and TabsContent are effectively untyped pass-throughs — they accept your usual element props (as, asChild, class, children) with no tabs-specific props at all, which is the clearest signal that the state logic isn't included.

## Edge cases

Because TabsTrigger/TabsContent don't manage state, if you forget to update data-state or conditionally render the panels, clicking a trigger will change its visual styling only in whatever way your click handler drives — nothing switches automatically. If you need a fully accessible, self-contained tabbed-panel widget with keyboard arrow navigation between tabs, you'll need to build that state and interaction logic on top of these parts yourself; it isn't shipped. For route-driven tabs, make sure `match` is set correctly ("exact" vs "prefix") or a parent route's tab will stay highlighted on child routes you didn't intend.

## Related pages

If you actually want route-scoped navigation without the tab-strip visual, Navbar and Navigation Menu and the plain NavLink/NavItem primitives are worth a look — Tab is really a styled variant of that same navigation family. For grouping content sections that don't need URL-backed navigation, Accordion and Collapsible cover the show/hide interaction with real built-in state, which Tabs here does not.

## Documentation navigation

[Previous](https://askrjs.com/docs/components/menubar/index.md) | [Next](https://askrjs.com/docs/components/sidebar/index.md)
