Tabs

Animated, accessible tab component with keyboard navigation and an animated indicator.

Made by raouf.codes
Edit on GitHub

Overview

The Tabs component provides a small, composable, and accessible tab system with first-class keyboard support and smooth motion.

Subcomponents & hooks:

  • Tabs — root component (state, ids, keyboard logic)
  • Tabs.List — tablist container
  • Tabs.Trigger — interactive tab button
  • Tabs.Indicator — animated underline / selection indicator
  • Tabs.Content — layout wrapper for panels
  • Tabs.Panel — individual animated panel
  • useTabs() — advanced hook for custom integrations

Advanced users can import useTabs() from the registry context for id helpers and keyboard/register callbacks.

Loading...

Installation

Usage

It can be used uncontrolled for simplicity, or controlled when you need external state management.

Basic (uncontrolled)

function Basic() {
  return (
    <Tabs defaultValue="account">
      <Tabs.List>
        <Tabs.Trigger value="account">Account</Tabs.Trigger>
        <Tabs.Trigger value="security">Security</Tabs.Trigger>
        <Tabs.Trigger value="billing">Billing</Tabs.Trigger>
      </Tabs.List>

      <Tabs.Content>
        <Tabs.Panel value="account">Account content</Tabs.Panel>
        <Tabs.Panel value="security">Security content</Tabs.Panel>
        <Tabs.Panel value="billing">Billing content</Tabs.Panel>
      </Tabs.Content>
    </Tabs>
  );
}

Controlled

function Controlled() {
  const [value, setValue] = React.useState('notifications');

  return (
    <Tabs value={value} onValueChange={setValue} intent="primary">
      <Tabs.List>
        <Tabs.Trigger value="profile">Profile</Tabs.Trigger>
        <Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>
      </Tabs.List>

      <Tabs.Content>
        <Tabs.Panel value="profile">Profile content</Tabs.Panel>
        <Tabs.Panel value="notifications">Notifications content</Tabs.Panel>
      </Tabs.Content>
    </Tabs>
  );
}

Using useTabs() inside a custom panel

import { useTabs } from '@/registry/components/azemmur/tabs/tabs-context';

function Panel({
  value,
  children,
}: {
  value: string;
  children: React.ReactNode;
}) {
  const { getPanelId, getTriggerId } = useTabs();
  return (
    <div
      role="tabpanel"
      id={getPanelId(value)}
      aria-labelledby={getTriggerId(value)}
    >
      {children}
    </div>
  );
}

API Reference

Tabs (root)

PropTypeDefault
value?
string
-
defaultValue?
string
-
onValueChange?
function
-
activation?
enum
"manual"
ltr?
boolean
true
intent?
enum
"primary"
styling?
enum
"underline"
visuals?
enum
"classic"
size?
enum
"md"
shape?
enum
"rounded"
className?
string
-
...props?
ComponentProps<"div">
-

Tabs.List

PropTypeDefault
className?
string
-
...props?
ComponentProps<"div">
-

Tabs.Trigger

Azemmur API Reference - Button Primitive
PropTypeDefault
value
string
-
triggerClassName?
string
-
indicatorClassName?
string
-
...props?
HTMLMotionProps<"button">
-

Tabs.Content

PropTypeDefault
className?
string
-
...props?
ComponentProps<"div">
-

Tabs.Panel

PropTypeDefault
value
string
-
className?
string
-
...props?
ComponentProps<typeof motion.div>
-

Tabs.Indicator

PropTypeDefault
intent?
enum
"primary"
styling?
enum
"underline"
visuals?
enum
"classic"
shape?
enum
"rounded"
...props?
ComponentProps<typeof motion.div>
-

useTabs() hook

{
  tabsId: string;
  activeValue?: string;
  onValueChange: (value: string) => void;
  registerTrigger: (value: string, node: HTMLButtonElement | null) => void;
  handleKeyDown: (e: React.KeyboardEvent, value: string) => void;
  getPanelId: (value: string) => string;
  getTriggerId: (value: string) => string;
  intent?: string;
  styling?: string;
  visuals?: string;
  size?: string;
  shape?: string;
  ltr?: boolean;
  activation?: "auto" | "manual";
}

Variants

Visual variants are powered by tabs-variants and applied consistently across the component:

  • intent — semantic intent (primary / secondary / accent)
  • styling — trigger & indicator style
  • visuals — surface / background treatment
  • size — spacing & typography
  • shape — border radius

For more details, refer to the tables above.

Accessibility

  • Tabs.List renders role="tablist" with aria-orientation.
  • Tabs.Trigger uses role="tab", aria-selected, and aria-controls.
  • Tabs.Panel uses role="tabpanel", id, and aria-labelledby.
  • Ids are generated via useTabs() helpers for consistency.

Keyboard support

  • ArrowRight / ArrowLeft — move focus between tabs (respects ltr)
  • Home / End — jump to first / last tab
  • 'auto' vs 'manual' activation controls when selection changes

Notes

  • Tabs manages focus and selection internally unless controlled via value.
  • This component exposes a stable public API. Lower-level primitives (auto-height contents, advanced layout helpers) live in the shared UI package and are intentionally not part of this surface.

Built by raouf.codes. The source code is available on GitHub.

Last updated: 1/28/2026

On this page