# Tabs
**📖 Live documentation:** https://cds.coinbase.com/components/navigation/Tabs/
Tabs is a flexible, accessible tab list for switching between related views. Use `DefaultTab` and `DefaultTabsActiveIndicator` for a standard underline tab row without wiring custom components, or supply your own `TabComponent` and `TabsActiveIndicatorComponent` for full control. For pill-style selection, see SegmentedTabs.
## Import
```tsx
import { Tabs, DefaultTabsActiveIndicator } from '@coinbase/cds-web/tabs'
```
## Examples
Tabs manages which tab is active and positions the animated indicator. For the common **underline** pattern, pass **`TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}`** and rely on the default **`TabComponent` (`DefaultTab`)**. Use a custom **`TabComponent`** when you need layout or content beyond what `DefaultTab` provides. For **pill / segmented** controls, use [SegmentedTabs](/components/navigation/SegmentedTabs) instead.
### Basics
Out of the box, **`Tabs`** uses **`DefaultTab`** for each row (headline text, optional [DotCount](/components/other/DotCount/) via `count` / `max` on each tab) and **`DefaultTabsActiveIndicator`** for the animated underline. **`activeBackground`** sets the **underline color** (it is forwarded to the indicator as its `background` token).
```jsx live
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
You can omit `TabComponent` explicitly: **`Tabs`** defaults it to **`DefaultTab`**.
#### No initial selection
```jsx live
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
const [activeTab, setActiveTab] = useState(null);
return (
);
}
```
#### Dot counts
Optional **`count`** and **`max`** on each tab are forwarded to the badge next to the label (see [DotCount](/components/other/DotCount/)).
```jsx live
function Example() {
const tabs = [
{ id: 'inbox', label: 'Inbox', count: 3, max: 99 },
{ id: 'sent', label: 'Sent' },
];
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
#### Disabled
Disable the whole row with **`disabled`**, or set **`disabled: true`** on individual tab items.
```jsx live
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
{ id: 'tab2', label: 'Tab 2', disabled: true },
{ id: 'tab3', label: 'Tab 3' },
];
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
### Custom `TabComponent`
Use **`useTabsContext`** inside your own tab button.
```jsx live
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
const TabComponent = useCallback(({ id, label, disabled, ...props }) => {
const { activeTab, updateActiveTab } = useTabsContext();
const isActive = activeTab?.id === id;
return (
updateActiveTab(id)}
disabled={disabled}
aria-pressed={isActive}
{...props}
>
{label}
);
}, []);
const ActiveIndicator = useCallback(
(props) => ,
[],
);
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
#### Custom label content
Pass extra fields on each tab and read them in your `TabComponent` (for example icons).
```jsx live
function Example() {
const tabs = [
{ id: 'home', label: 'Home', icon: 'home' },
{ id: 'profile', label: 'Profile', icon: 'user' },
{ id: 'settings', label: 'Settings', icon: 'settings' },
];
const CustomTab = useCallback(({ id, label, icon, disabled, ...props }) => {
const { activeTab, updateActiveTab } = useTabsContext();
const isActive = activeTab?.id === id;
return (
updateActiveTab(id)}
disabled={disabled}
aria-pressed={isActive}
{...props}
>
{label}
);
}, []);
const ActiveIndicator = useCallback(
(props) => ,
[],
);
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
### Scrolling with TabsScrollArea
When the tab row can overflow horizontally, wrap **`Tabs`** in [TabsScrollArea](/components/navigation/TabsScrollArea/). Pass the render prop’s **`onActiveTabElementChange`** into **`Tabs`** so the active tab scrolls into view. Narrow the viewport or set **`width`** / **`maxWidth`** on **`TabsScrollArea`** to see overflow controls.
```jsx live
function Example() {
const tabs = [
{ id: 't1', label: 'Overview' },
{ id: 't2', label: 'Markets' },
{ id: 't3', label: 'Trade' },
{ id: 't4', label: 'Earn' },
{ id: 't5', label: 'Learn' },
{ id: 't6', label: 'More' },
];
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
{({ onActiveTabElementChange }) => (
)}
);
}
```
### Accessibility
Provide a descriptive **`accessibilityLabel`** on **`Tabs`** for the tab list. **`DefaultTab`** sets `aria-controls` / `aria-selected` for each tab; pair tabs with **`role="tabpanel"`** regions in your page content when you switch panels.
## Props
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `activeTab` | `TabValue \| null` | Yes | `-` | React state for the currently active tab. Setting it to null results in no active tab. |
| `onChange` | `(activeTab: TTab \| null) => void` | Yes | `-` | Callback that is fired when the active tab changes. Use this callback to update the activeTab state. |
| `tabs` | `(TTab & { Component?: TabComponent \| undefined; })[]` | Yes | `-` | The array of tabs data. Each tab may optionally define a custom Component to render. |
| `TabComponent` | `TabComponent` | No | `-` | The default Component to render each tab. |
| `TabsActiveIndicatorComponent` | `TabsActiveIndicatorComponent` | No | `-` | The default Component to render the tabs active indicator. |
| `activeBackground` | `Color` | No | `-` | Background color passed to the TabsActiveIndicatorComponent. |
| `alignContent` | `ResponsiveProp` | No | `-` | - |
| `alignItems` | `ResponsiveProp` | No | `-` | - |
| `alignSelf` | `ResponsiveProp` | No | `-` | - |
| `as` | `div` | No | `-` | The underlying element or component the polymorphic component will render. Changing as also changes the inherited native props (e.g. href for as=a) and the expected ref type. |
| `aspectRatio` | `ResponsiveProp` | No | `-` | - |
| `background` | `ResponsiveProp` | No | `-` | - |
| `borderBottomLeftRadius` | `ResponsiveProp` | No | `-` | - |
| `borderBottomRightRadius` | `ResponsiveProp` | No | `-` | - |
| `borderBottomWidth` | `ResponsiveProp` | No | `-` | - |
| `borderColor` | `ResponsiveProp` | No | `-` | - |
| `borderEndWidth` | `ResponsiveProp` | No | `-` | - |
| `borderRadius` | `ResponsiveProp` | No | `-` | - |
| `borderStartWidth` | `ResponsiveProp` | No | `-` | - |
| `borderTopLeftRadius` | `ResponsiveProp` | No | `-` | - |
| `borderTopRightRadius` | `ResponsiveProp` | No | `-` | - |
| `borderTopWidth` | `ResponsiveProp` | No | `-` | - |
| `borderWidth` | `ResponsiveProp` | No | `-` | - |
| `bordered` | `boolean` | No | `-` | Add a border around all sides of the box. |
| `borderedBottom` | `boolean` | No | `-` | Add a border to the bottom side of the box. |
| `borderedEnd` | `boolean` | No | `-` | Add a border to the trailing side of the box. |
| `borderedHorizontal` | `boolean` | No | `-` | Add a border to the leading and trailing sides of the box. |
| `borderedStart` | `boolean` | No | `-` | Add a border to the leading side of the box. |
| `borderedTop` | `boolean` | No | `-` | Add a border to the top side of the box. |
| `borderedVertical` | `boolean` | No | `-` | Add a border to the top and bottom sides of the box. |
| `bottom` | `ResponsiveProp>` | No | `-` | - |
| `classNames` | `{ root?: string; tab?: string \| undefined; activeIndicator?: string \| undefined; } \| undefined` | No | `-` | Custom class names for individual elements of the Tabs component |
| `color` | `ResponsiveProp` | No | `-` | - |
| `columnGap` | `ResponsiveProp` | No | `-` | - |
| `dangerouslySetBackground` | `string` | No | `-` | - |
| `disabled` | `boolean` | No | `-` | Disable interactions on all the tabs. |
| `display` | `ResponsiveProp` | No | `-` | - |
| `elevation` | `ResponsiveProp` | No | `-` | - |
| `flexBasis` | `ResponsiveProp>` | No | `-` | - |
| `flexDirection` | `ResponsiveProp` | No | `-` | - |
| `flexGrow` | `ResponsiveProp` | No | `-` | - |
| `flexShrink` | `ResponsiveProp` | No | `-` | - |
| `flexWrap` | `ResponsiveProp` | No | `-` | - |
| `font` | `ResponsiveProp` | No | `-` | - |
| `fontFamily` | `ResponsiveProp` | No | `-` | - |
| `fontSize` | `ResponsiveProp` | No | `-` | - |
| `fontWeight` | `ResponsiveProp` | No | `-` | - |
| `gap` | `ResponsiveProp` | No | `-` | - |
| `grid` | `ResponsiveProp` | No | `-` | - |
| `gridArea` | `ResponsiveProp` | No | `-` | - |
| `gridAutoColumns` | `ResponsiveProp>` | No | `-` | - |
| `gridAutoFlow` | `ResponsiveProp` | No | `-` | - |
| `gridAutoRows` | `ResponsiveProp>` | No | `-` | - |
| `gridColumn` | `ResponsiveProp` | No | `-` | - |
| `gridColumnEnd` | `ResponsiveProp` | No | `-` | - |
| `gridColumnStart` | `ResponsiveProp` | No | `-` | - |
| `gridRow` | `ResponsiveProp` | No | `-` | - |
| `gridRowEnd` | `ResponsiveProp` | No | `-` | - |
| `gridRowStart` | `ResponsiveProp` | No | `-` | - |
| `gridTemplate` | `ResponsiveProp` | No | `-` | - |
| `gridTemplateAreas` | `ResponsiveProp` | No | `-` | - |
| `gridTemplateColumns` | `ResponsiveProp>` | No | `-` | - |
| `gridTemplateRows` | `ResponsiveProp>` | No | `-` | - |
| `height` | `ResponsiveProp>` | No | `-` | - |
| `justifyContent` | `ResponsiveProp` | No | `-` | - |
| `key` | `Key \| null` | No | `-` | - |
| `left` | `ResponsiveProp>` | No | `-` | - |
| `lineHeight` | `ResponsiveProp` | No | `-` | - |
| `margin` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - |
| `marginBottom` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - |
| `marginEnd` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - |
| `marginStart` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - |
| `marginTop` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - |
| `marginX` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - |
| `marginY` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - |
| `maxHeight` | `ResponsiveProp>` | No | `-` | - |
| `maxWidth` | `ResponsiveProp>` | No | `-` | - |
| `minHeight` | `ResponsiveProp>` | No | `-` | - |
| `minWidth` | `ResponsiveProp>` | No | `-` | - |
| `onActiveTabElementChange` | `((element: HTMLElement \| null) => void)` | No | `-` | Optional callback to receive the active tab element. |
| `opacity` | `ResponsiveProp` | No | `-` | - |
| `overflow` | `ResponsiveProp` | No | `-` | - |
| `padding` | `ResponsiveProp` | No | `-` | - |
| `paddingBottom` | `ResponsiveProp` | No | `-` | - |
| `paddingEnd` | `ResponsiveProp` | No | `-` | - |
| `paddingStart` | `ResponsiveProp` | No | `-` | - |
| `paddingTop` | `ResponsiveProp` | No | `-` | - |
| `paddingX` | `ResponsiveProp` | No | `-` | - |
| `paddingY` | `ResponsiveProp` | No | `-` | - |
| `pin` | `PinningDirection` | No | `-` | Direction in which to absolutely pin the box. |
| `position` | `ResponsiveProp` | No | `absolute` | - |
| `ref` | `ForwardedRef` | No | `-` | - |
| `right` | `ResponsiveProp>` | No | `-` | - |
| `rowGap` | `ResponsiveProp` | No | `-` | - |
| `style` | `CSSProperties` | No | `-` | - |
| `styles` | `{ root?: CSSProperties; tab?: CSSProperties \| undefined; activeIndicator?: CSSProperties \| undefined; } \| undefined` | No | `-` | Custom styles for individual elements of the Tabs component |
| `testID` | `string` | No | `tabs-active-indicator` | Used to locate this element in unit and end-to-end tests. Under the hood, testID translates to data-testid on Web. On Mobile, testID stays the same - testID |
| `textAlign` | `ResponsiveProp` | No | `-` | - |
| `textDecoration` | `ResponsiveProp` | No | `-` | - |
| `textTransform` | `ResponsiveProp` | No | `-` | - |
| `top` | `ResponsiveProp>` | No | `-` | - |
| `transform` | `ResponsiveProp` | No | `-` | - |
| `userSelect` | `ResponsiveProp` | No | `-` | - |
| `visibility` | `ResponsiveProp` | No | `-` | - |
| `width` | `ResponsiveProp>` | No | `-` | - |
| `zIndex` | `ResponsiveProp` | No | `-` | - |
## Styles
| Selector | Static class name | Description |
| --- | --- | --- |
| `root` | `-` | Root element |
| `tab` | `-` | Tab element |
| `activeIndicator` | `-` | Active indicator element |