Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .changeset/add-tabs-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@tailor-platform/app-shell": minor
---

Add `Tabs` compound component for tab-based navigation, backed by Base UI's Tabs primitive.

```tsx
import { Tabs } from "@tailor-platform/app-shell";

<Tabs.Root defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="projects">Projects</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="projects">Projects content</Tabs.Panel>
</Tabs.Root>;
```
128 changes: 128 additions & 0 deletions docs/components/tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: Tabs
description: Tab navigation with a compound component API
---

# Tabs

The `Tabs` component provides tab-based navigation for toggling between related panels on the same page. It is backed by Base UI's Tabs primitive.

## Import

```tsx
import { Tabs } from "@tailor-platform/app-shell";
```

## Basic Usage

```tsx
<Tabs.Root defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="projects">Projects</Tabs.Tab>
<Tabs.Tab value="account">Account</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="projects">Projects content</Tabs.Panel>
<Tabs.Panel value="account">Account content</Tabs.Panel>
</Tabs.Root>
```

## Sub-components

| Sub-component | Description |
| ------------- | -------------------------------------------------------------- |
| `Tabs.Root` | Manages tab selection state |
| `Tabs.List` | Groups the individual tab buttons |
| `Tabs.Tab` | An interactive tab button that toggles the corresponding panel |
| `Tabs.Panel` | A panel displayed when the corresponding tab is active |

## Props

### Tabs.Root Props

| Prop | Type | Default | Description |
| --------------- | ---------------------------------- | ----------- | --------------------------------------- |
| `defaultValue` | `Tabs.Tab.Value` | `0` | Initial active tab value (uncontrolled) |
| `value` | `Tabs.Tab.Value` | - | Controlled active tab value |
| `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes |
| `variant` | `'default' \| 'line' \| 'capsule'` | `'default'` | Visual style of the tabs |
| `className` | `string` | - | Additional CSS classes for root |
| `children` | `React.ReactNode` | - | Tabs sub-components |

### Tabs.List Props

Accepts `className` and all standard HTML `<div>` props.

### Tabs.Tab Props

| Prop | Type | Default | Description |
| ---------- | ---------------- | ------- | ---------------------------------- |
| `value` | `Tabs.Tab.Value` | - | **Required.** The value of the tab |
| `disabled` | `boolean` | - | Whether the tab is disabled |

Also accepts `className` and all standard HTML `<button>` props.

### Tabs.Panel Props

| Prop | Type | Default | Description |
| ------------- | ---------------- | ------- | ------------------------------------------------------ |
| `value` | `Tabs.Tab.Value` | - | **Required.** The value matching the corresponding tab |
| `keepMounted` | `boolean` | `false` | Whether to keep the panel in the DOM when hidden |

Also accepts `className` and all standard HTML `<div>` props.

## Controlled Usage

```tsx
const [activeTab, setActiveTab] = useState("overview");

<Tabs.Root value={activeTab} onValueChange={setActiveTab}>
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="details">Details</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="details">Details content</Tabs.Panel>
</Tabs.Root>;
```

## Examples

### With Disabled Tab

```tsx
<Tabs.Root defaultValue="active">
<Tabs.List>
<Tabs.Tab value="active">Active</Tabs.Tab>
<Tabs.Tab value="pending">Pending</Tabs.Tab>
<Tabs.Tab value="archived" disabled>
Archived
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="active">Active items</Tabs.Panel>
<Tabs.Panel value="pending">Pending items</Tabs.Panel>
<Tabs.Panel value="archived">Archived items</Tabs.Panel>
</Tabs.Root>
```

### Use Icons in Tab

```tsx
<Tabs.Root defaultValue="overview" variant="capsule">
<Tabs.List>
<Tabs.Tab value="overview">
<LayoutDashboardIcon />
</Tabs.Tab>
<Tabs.Tab value="projects">
<FolderKanbanIcon />
</Tabs.Tab>
<Tabs.Tab value="settings">
<SettingsIcon />
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="projects">Projects content</Tabs.Panel>
<Tabs.Panel value="settings">Settings content</Tabs.Panel>
</Tabs.Root>
```
154 changes: 154 additions & 0 deletions examples/nextjs-app/src/modules/pages/primitives-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,65 @@ import {
Sheet,
Menu,
Table,
Tabs,
} from "@tailor-platform/app-shell";
import * as React from "react";

const LayoutDashboardIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="7" height="9" x="3" y="3" rx="1" />
<rect width="7" height="5" x="14" y="3" rx="1" />
<rect width="7" height="5" x="3" y="16" rx="1" />
<rect width="7" height="9" x="14" y="12" rx="1" />
</svg>
);

const FolderKanbanIcon = () => (
Copy link
Copy Markdown
Contributor

@erickteowarang erickteowarang May 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor, but you can probably just pull the example icons from lucide instead for a more "realistic" example

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right. Let me quickly updated that before merging this PR. It won't need the extra review again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found the other examples are using SVG icons too, so will work in a different PR to eradicate those things with lucide-react all at once.

<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" />
<path d="M8 10v4" />
<path d="M12 10v2" />
<path d="M16 10v6" />
</svg>
);

const SettingsIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
</svg>
);

export const primitiveComponentsDemoResource = defineResource({
path: "primitives-demo",
meta: {
Expand Down Expand Up @@ -332,6 +388,104 @@ export const primitiveComponentsDemoResource = defineResource({
</Card.Content>
</Card.Root>

{/* Tabs */}
<Card.Root>
<Card.Header title="Tabs" />
<Card.Content>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "2rem",
}}
>
<div style={{ display: "flex", gap: "2rem" }}>
<div>
<div style={labelStyle}>Default</div>
<Tabs.Root defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="projects">Projects</Tabs.Tab>
<Tabs.Tab value="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="projects">Projects content</Tabs.Panel>
<Tabs.Panel value="settings">Settings content</Tabs.Panel>
</Tabs.Root>
</div>
<div>
<div style={labelStyle}>Line</div>
<Tabs.Root defaultValue="activity" variant="line">
<Tabs.List>
<Tabs.Tab value="activity">Activity</Tabs.Tab>
<Tabs.Tab value="members">Members</Tabs.Tab>
<Tabs.Tab value="billing">Billing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="activity">Activity content</Tabs.Panel>
<Tabs.Panel value="members">Members content</Tabs.Panel>
<Tabs.Panel value="billing">Billing content</Tabs.Panel>
</Tabs.Root>
</div>
<div>
<div style={labelStyle}>Capsule</div>
<Tabs.Root defaultValue="all" variant="capsule">
<Tabs.List>
<Tabs.Tab value="all">All</Tabs.Tab>
<Tabs.Tab value="open">Open</Tabs.Tab>
<Tabs.Tab value="received">Received</Tabs.Tab>
<Tabs.Tab value="closed">Closed</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="all">All content</Tabs.Panel>
<Tabs.Panel value="open">Open content</Tabs.Panel>
<Tabs.Panel value="received">Received content</Tabs.Panel>
<Tabs.Panel value="closed">Closed content</Tabs.Panel>
</Tabs.Root>
</div>
</div>
<div style={{ display: "flex", gap: "2rem" }}>
<div>
<div style={labelStyle}>With Badge</div>
<Tabs.Root defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview">
Overview
<Badge variant="success" style={{ marginLeft: "0.5rem" }}>
New
</Badge>
</Tabs.Tab>
<Tabs.Tab value="projects">
Projects
<Badge style={{ marginLeft: "0.5rem" }}>10+</Badge>
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="projects">Projects content</Tabs.Panel>
</Tabs.Root>
</div>
<div>
<div style={labelStyle}>Icons</div>
<Tabs.Root defaultValue="overview" variant="capsule">
<Tabs.List>
<Tabs.Tab value="overview">
<LayoutDashboardIcon />
</Tabs.Tab>
<Tabs.Tab value="projects">
<FolderKanbanIcon />
</Tabs.Tab>
<Tabs.Tab value="settings">
<SettingsIcon />
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="projects">Projects content</Tabs.Panel>
<Tabs.Panel value="settings">Settings content</Tabs.Panel>
</Tabs.Root>
</div>
</div>
</div>
</Card.Content>
</Card.Root>

{/* Table */}
<Card.Root>
<Card.Header title="Table" />
Expand Down
Loading
Loading