Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/docgen.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ module.exports = {
'tabs/TabLabel',
'tabs/TabNavigation',
'tabs/Tabs',
'tabs/TabsScrollArea',
'tag/Tag',
'tour/Tour',
'typography/Link',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function ExampleDefault() {

### Compact

```jsx lived
```jsx live
function ExampleCompactNoStart() {
const tabs = [
{ id: 'all', label: 'All' },
Expand Down Expand Up @@ -48,7 +48,7 @@ function ExampleWithPaddles() {
### With autoScrollOffset

:::tip Auto-scroll offset
The `autoScrollOffset` prop controls the X position offset when auto-scrolling to the active tab. This prevents the active tab from being covered by the paddle on the left side. Try clicking tabs near the edges to see the difference.
The `autoScrollOffset` prop controls the horizontal offset when auto-scrolling the active tab into view, so it stays clear of the leading overflow control. Try selecting tabs near the edges to see the difference.
:::

```jsx live
Expand Down Expand Up @@ -78,14 +78,14 @@ function ExampleAutoScrollOffset() {
}
```

### With custom sized paddles
### With custom sized overflow controls

:::tip Paddle styling
You can adjust the size of the paddles via `styles.paddle`.
:::tip Overflow control styling
Target the chevron buttons with **`styles.overflowIndicatorButton`** (or **`overflowIndicator`** / **`overflowIndicatorGradient`**).
:::

```jsx live
function ExampleCustomPaddles() {
function ExampleCustomOverflowControls() {
const tabs = Array.from({ length: 10 }).map((_, i) => ({
id: `t_${i + 1}`,
label: `Item ${i + 1}`,
Expand All @@ -96,7 +96,7 @@ function ExampleCustomPaddles() {
activeTab={activeTab}
onChange={setActiveTab}
tabs={tabs}
styles={{ paddle: { transform: 'scale(0.8)' } }}
styles={{ overflowIndicatorButton: { transform: 'scale(0.8)' } }}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"label": "Tabs",
"url": "/components/navigation/Tabs/"
},
{
"label": "TabsScrollArea",
"url": "/components/navigation/TabsScrollArea/"
},
{
"label": "SelectChip",
"url": "/components/inputs/SelectChip/"
Expand Down
36 changes: 36 additions & 0 deletions apps/docs/docs/components/navigation/Tabs/_mobileExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,42 @@ function Example() {
}
```

## 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`**. Set **`width`** / **`maxWidth`** on **`TabsScrollArea`** so the row overflows and edge gradients appear.

```jsx
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 (
<TabsScrollArea accessibilityLabel="Scrollable tab list" maxWidth={320} width="100%">
{({ onActiveTabElementChange }) => (
<Tabs
TabComponent={DefaultTab}
TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}
accessibilityLabel="Tabs"
activeBackground="bgPrimary"
activeTab={activeTab}
background="bg"
gap={4}
onActiveTabElementChange={onActiveTabElementChange}
onChange={setActiveTab}
tabs={tabs}
/>
)}
</TabsScrollArea>
);
}
```

## Accessibility

Set **`accessibilityLabel`** on **`Tabs`**. **`DefaultTab`** wires `accessibilityRole="tab"` and selection state; keep tab panels in sync in your screen content.
36 changes: 36 additions & 0 deletions apps/docs/docs/components/navigation/Tabs/_webExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,42 @@ function Example() {
}
```

## 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 (
<TabsScrollArea accessibilityLabel="Scrollable tab list" maxWidth={320} width="100%">
{({ onActiveTabElementChange }) => (
<Tabs
TabComponent={DefaultTab}
TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}
accessibilityLabel="Tabs"
activeBackground="bgPrimary"
activeTab={activeTab}
background="bg"
gap={4}
onActiveTabElementChange={onActiveTabElementChange}
onChange={setActiveTab}
tabs={tabs}
/>
)}
</TabsScrollArea>
);
}
```

## 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.
4 changes: 4 additions & 0 deletions apps/docs/docs/components/navigation/Tabs/mobileMetadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"description": "Tabs is a flexible, accessible tab list for switching between related views. Use `DefaultTab` and `DefaultTabsActiveIndicator` for a standard underline tab row without custom tab wiring, or provide your own `TabComponent` and `TabsActiveIndicatorComponent`. For pill-style selection, see SegmentedTabs.",
"figma": "https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?node-id=25128-9889&t=7bpcjquwgXNk9lnN-4",
"relatedComponents": [
{
"label": "TabsScrollArea",
"url": "/components/navigation/TabsScrollArea/"
},
{
"label": "SegmentedTabs",
"url": "/components/navigation/SegmentedTabs/"
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/docs/components/navigation/Tabs/webMetadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-tabs-tabs--all",
"description": "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.",
"relatedComponents": [
{
"label": "TabsScrollArea",
"url": "/components/navigation/TabsScrollArea/"
},
{
"label": "SegmentedTabs",
"url": "/components/navigation/SegmentedTabs"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
TabsScrollArea wraps a horizontal [Tabs](/components/navigation/Tabs/) row in a `ScrollView` and shows edge gradients when content overflows. Use a **function child** that receives `onActiveTabElementChange` and pass it through to **`Tabs`** so the active tab can scroll into view.

## Basics

Set **`width`** / **`maxWidth`** on **`TabsScrollArea`** so the tab row overflows on smaller screens; gradients appear when there is offscreen content.

```jsx
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 (
<TabsScrollArea accessibilityLabel="Scrollable tab list" maxWidth={320} width="100%">
{({ onActiveTabElementChange }) => (
<Tabs
TabComponent={DefaultTab}
TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}
accessibilityLabel="Tabs"
activeBackground="bgPrimary"
activeTab={activeTab}
background="bg"
gap={4}
onActiveTabElementChange={onActiveTabElementChange}
onChange={setActiveTab}
tabs={tabs}
/>
)}
</TabsScrollArea>
);
}
```

## Overflow indicator

On React Native, the default **`TabsScrollAreaOverflowIndicator`** renders an **`OverflowGradient`** at each edge when there is offscreen content. Scrolling is **gesture-based** (horizontal pan on the **`ScrollView`**); the default gradient is **visual-only** and does not receive press handlers from **`TabsScrollArea`**.

### Custom `OverflowIndicatorComponent`

Provide a component that satisfies **`TabsScrollAreaOverflowIndicatorProps`**: **`direction`** (`'left'` \| `'right'`), **`show`**, optional **`style`**, and **`testID`**. The default implementation maps **`direction`** to **`OverflowGradient`**’s **`pin`**.

The example below replaces the gradient with a simple edge strip (visual-only; no scroll wiring).

```tsx
import { Box } from '@coinbase/cds-mobile/layout';
import {
DefaultTab,
DefaultTabsActiveIndicator,
Tabs,
TabsScrollArea,
type TabsScrollAreaOverflowIndicatorProps,
} from '@coinbase/cds-mobile/tabs';

function CustomOverflowIndicator({
direction,
show,
style,
testID,
}: TabsScrollAreaOverflowIndicatorProps) {
if (!show) {
return null;
}
const isLeft = direction === 'left';
return (
<Box
background="fgMuted"
borderRadius={200}
bottom={8}
position="absolute"
style={[isLeft ? { left: 4 } : { right: 4 }, style]}
testID={testID}
top={8}
width={4}
/>
);
}

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 (
<TabsScrollArea
OverflowIndicatorComponent={CustomOverflowIndicator}
accessibilityLabel="Scrollable tab list"
maxWidth={320}
width="100%"
>
{({ onActiveTabElementChange }) => (
<Tabs
TabComponent={DefaultTab}
TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}
accessibilityLabel="Tabs"
activeBackground="bgPrimary"
activeTab={activeTab}
background="bg"
gap={4}
onActiveTabElementChange={onActiveTabElementChange}
onChange={setActiveTab}
tabs={tabs}
/>
)}
</TabsScrollArea>
);
}
```

## Styling

Use **`styles.root`**, **`styles.scrollContainer`**, and **`styles.overflowIndicator`** to align with layout tokens. Mobile uses a single **`overflowIndicator`** style slot for both edges (see **Styles** tab).

## Accessibility

Set **`accessibilityLabel`** on **`TabsScrollArea`** (root container) and on **`Tabs`** for screen readers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ComponentPropsTable from '@site/src/components/page/ComponentPropsTable';
import mobilePropsData from ':docgen/mobile/tabs/TabsScrollArea/data';
import { sharedParentTypes } from ':docgen/_types/sharedParentTypes';
import { sharedTypeAliases } from ':docgen/_types/sharedTypeAliases';

<ComponentPropsTable
props={mobilePropsData}
sharedTypeAliases={sharedTypeAliases}
sharedParentTypes={sharedParentTypes}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ComponentStylesTable } from '@site/src/components/page/ComponentStylesTable';

import mobileStylesData from ':docgen/mobile/tabs/TabsScrollArea/styles-data';

## Selectors

<ComponentStylesTable componentName="TabsScrollArea" styles={mobileStylesData} />
Loading
Loading