Askr
UI & Components

Tabs

Use tabs with the current published Askr packages.

  • @askrjs/themes/tabs0.0.13

How to use tabs

Tabs should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep tabs labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
  3. Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@askrjs/themes/components';

<Tabs value={tab()} onValueChange={tab.set}>
  <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 (role="tablist", role="tab", role="tabpanel" are all applied for you) but stops at styling and roles — there's no hidden Context tracking a current tab or associating a trigger with its panel.

State model

For Tab, "selected" state is really route state: active styling is driven by aria-current="page" or a `data-active` attribute you set based on your router's current location, not by anything the component tracks internally. For TabsTrigger/TabsContent, state 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. TabsList/TabsTrigger/TabsContent apply the correct ARIA roles (tablist/tab/tabpanel), but since there's no built-in state, you also own wiring up 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 — 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.

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.