Tabs
Switches between views of the same thing, like the details, items and history of one order, without leaving the page. Only one panel shows at a time.
Installation
Set up Camaleo once (Installation), then import what you need. It renders on the client.
import { Tabs, TabsList, Tab, TabsPanel } from "@camaleo/react";Anatomy
A Tab and a TabsPanel with the same value belong together. The list is one stop in the Tab order; the arrow keys move between its tabs.
Examples
Every example is a real file you can copy. They all wear the skin, mode and brand from the bar above. Click a tab, then try the arrow keys.
Acme Store
Online store · created on 03/02/2025
Shown on invoices and in emails to customers.
Vertical
With orientation="vertical" the list stands beside the panel and ↑ ↓ move between tabs. It suits settings with more sections than fit in a row.
- Order #4821 arrived damaged
- Can I change my delivery address?
With icons and counts
A count tells people where the work is before they click. Keep it short: the number becomes part of the tab's name.
Many tabs
When the tabs don't fit, the list scrolls sideways and keeps the active tab in view. Past eight or so, a sidebar or a select is easier to scan.
/orders/4821?tab=items
In the URL
Click a tab and watch the address. With value and onValueChange you can keep the tab in the URL, so a link or a reload opens the same view.
Load on first open
Open Activity. A hidden panel isn't rendered, so you can fetch its data the first time it opens and show a skeleton meanwhile.
Follow focus
By default the arrow keys only move focus and Enter or Space opens the tab. Add activateOnFocus when panels show instantly, as these numbers do.
Tax reports open after your first full month of sales, on 12/01/2026.
Disabled, with a reason
The arrow keys still reach a disabled tab but can't open it. Say why it's off and when it opens, linked with aria-describedby so it's read on focus.
In context
These are whole pieces of UI you can copy, in the skin you picked.
Hand coffee grinder
$89.90“Consistent grind and easy to clean. I use it every morning.”
Marina L., 10/02/2026
Order #4821
- Bourbon coffee beans, 500 g$96.50
- Hand coffee grinder$89.90
Guidelines
Summary, orders and notes all describe the same customer, and people can read them in any order.
A checkout has an order and a next step. Tabs let people skip ahead and hide how far they've come; use a stepper.
Moving to another page is navigation: links with aria-current="page" keep the back button, new tabs and shared URLs working.
Screen readers announce a tab list, so people expect the content to change in place, not the whole page.
Props
Tab. Use when the component is controlled.
When the value is null, no Tab will be active.null, no Tab will be active.reason is 'none' for user-initiated changes, such as a click
or keyboard navigation; 'initial' for the first automatic selection or
fallback in uncontrolled roots when defaultValue is omitted or
undefined, including when the implicit initial value is disabled or
missing; 'disabled' for automatic fallback when the selected tab becomes
disabled in uncontrolled roots; or 'missing' for automatic fallback when
the selected tab is removed, or when an explicit defaultValue never
matches a mounted tab in uncontrolled roots.
For automatic changes, the selected value can be null when no enabled Tab
is available as a fallback.
Automatic changes cannot be canceled; calling eventDetails.cancel() for
'initial', 'disabled', or 'missing' has no effect.Tab and TabsPanel take a value that pairs them. Tab also takes disabled.
TabsList decides how the keyboard behaves:
TabsPanel can stay in the page while hidden:
Styling hooks
Restyle it without forking: target the attributes, or set the tokens on a parent, a skin or the list itself.
Attributes
Tokens it reads
Color
Shape & size
Type & motion
This component only
/* pill tabs on a tinted track, only in this card */
.billing [data-slot="tabs-list"] {
--cml-tabs-list-bg: var(--cml-surface-sunken);
--cml-tabs-list-radius: var(--cml-radius-surface);
--cml-tab-radius: var(--cml-radius-surface);
--cml-tab-active-bg: var(--cml-surface-raised);
}Accessibility
- Each tab has role=tab and aria-selected; each panel has role=tabpanel and is named by its tab through aria-labelledby.
- Name the list with aria-label or aria-labelledby, so people know which set of tabs they are in.
- The panel can take focus, so keyboard users reach its text even when it has no links or fields.
- A disabled tab keeps focus with aria-disabled, so people hear it exists; Enter and Space don't open it.