Primitives
Tabs
A set of panels in one view, with one panel in view at a time.
import { Card, CardContent, Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'
export default function TabsHero() {
return (
<Tabs defaultValue="overview" className="w-full max-w-lg">
<TabsList aria-label="Project sections">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="members">Members</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<Card>
<CardContent className="text-[13px] text-foreground-light">
The project has 14 open tasks. The next release is on October 18.
</CardContent>
</Card>
</TabsContent>
<TabsContent value="activity">
<Card>
<CardContent className="text-[13px] text-foreground-light">
Maya Chen moved the task "Pricing page" to the review column.
</CardContent>
</Card>
</TabsContent>
<TabsContent value="members">
<Card>
<CardContent className="text-[13px] text-foreground-light">
The project has 6 members and 2 invitations with no answer.
</CardContent>
</Card>
</TabsContent>
</Tabs>
)
}TabsList an aria-label.Import the parts and put them together. Give the same value to a TabsTrigger and to its TabsContent.
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'
<Tabs>
<TabsList>
<TabsTrigger />
</TabsList>
<TabsContent />
</Tabs>| Part | Role |
|---|---|
Tabs | Holds the value of the active tab. |
TabsList | The row of triggers. It sets the look. |
TabsTrigger | The button of one tab. |
TabsContent | The panel of one tab. |
The variant prop of TabsList sets the look. The triggers follow the look of their list.
import { Card, CardContent, CardHeader, CardTitle, Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'
export default function TabsPills() {
return (
<Tabs defaultValue="monthly" className="w-full max-w-sm">
<Card>
<CardHeader>
<CardTitle>Pro plan</CardTitle>
<TabsList variant="pills" aria-label="Billing period">
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="yearly">Yearly</TabsTrigger>
</TabsList>
</CardHeader>
<CardContent className="text-[13px] text-foreground-light">
<TabsContent value="monthly">$24 for each seat, with one invoice each month.</TabsContent>
<TabsContent value="yearly">$20 for each seat, with one invoice each year.</TabsContent>
</CardContent>
</Card>
</Tabs>
)
}| Variant | Look | Use |
|---|---|---|
underline (default) | Text on a line that takes the full width. | The sections of a page. |
pills | A compact control with a raised chip. | A card, a panel or a toolbar. |
A trigger can hold an icon and a count. Put the count in a Badge.
import { Badge, Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'
import { CircleCheck, CircleDot, GitPullRequest } from 'lucide-react'
export default function TabsIcons() {
return (
<Tabs defaultValue="open" className="w-full max-w-lg">
<TabsList aria-label="Tasks">
<TabsTrigger value="open">
<CircleDot /> Open <Badge>24</Badge>
</TabsTrigger>
<TabsTrigger value="review">
<GitPullRequest /> In review <Badge>3</Badge>
</TabsTrigger>
<TabsTrigger value="closed">
<CircleCheck /> Closed
</TabsTrigger>
</TabsList>
<TabsContent value="open" className="text-[13px] text-foreground-light">
The 24 tasks that are open.
</TabsContent>
<TabsContent value="review" className="text-[13px] text-foreground-light">
The 3 tasks that wait for a review.
</TabsContent>
<TabsContent value="closed" className="text-[13px] text-foreground-light">
The tasks that are done.
</TabsContent>
</Tabs>
)
}Set disabled on the trigger of a tab that is not available.
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'
export default function TabsDisabled() {
return (
<Tabs defaultValue="overview" className="w-full max-w-lg">
<TabsList aria-label="Project sections">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="invoices">Invoices</TabsTrigger>
<TabsTrigger value="audit" disabled>
Audit log
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-[13px] text-foreground-light">
The overview of the project.
</TabsContent>
<TabsContent value="invoices" className="text-[13px] text-foreground-light">
The invoices of the project.
</TabsContent>
</Tabs>
)
}Tabs holds its value by default. Use defaultValue to set the first active tab.
To control the value, pass value and onValueChange. Use this to change the tab from another control.
import * as React from 'react'
import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'
const STEPS = ['details', 'members', 'review']
export default function TabsControlled() {
const [step, setStep] = React.useState('details')
const index = STEPS.indexOf(step)
return (
<Tabs value={step} onValueChange={setStep} className="w-full max-w-md">
<TabsList variant="pills" aria-label="New project">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="members">Members</TabsTrigger>
<TabsTrigger value="review">Review</TabsTrigger>
</TabsList>
<TabsContent value="details" className="text-[13px] text-foreground-light">
The name and the description of the project.
</TabsContent>
<TabsContent value="members" className="text-[13px] text-foreground-light">
The members who get an invitation.
</TabsContent>
<TabsContent value="review" className="text-[13px] text-foreground-light">
A summary of the project before you create it.
</TabsContent>
<div className="flex justify-between">
<Button size="tiny" disabled={index === 0} onClick={() => setStep(STEPS[index - 1] ?? step)}>
Back
</Button>
<Button size="tiny" disabled={index === STEPS.length - 1} onClick={() => setStep(STEPS[index + 1] ?? step)}>
Next
</Button>
</div>
</Tabs>
)
}A panel unmounts when its tab is not active. Its fields lose their content.
To keep a panel mounted, pass forceMount to TabsContent. Then add the class data-[state=inactive]:hidden to hide the panel that is not active.
import { Field, Input, Tabs, TabsContent, TabsList, TabsTrigger, Textarea } from 'ferry-ui'
export default function TabsForceMount() {
return (
<Tabs defaultValue="details" className="w-full max-w-md">
<TabsList aria-label="Invoice form">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="notes">Notes</TabsTrigger>
</TabsList>
{/* Each panel stays mounted. The class hides the panel that is not active. */}
<TabsContent value="details" forceMount className="data-[state=inactive]:hidden">
<Field label="Customer">
<Input placeholder="Acme" />
</Field>
</TabsContent>
<TabsContent value="notes" forceMount className="data-[state=inactive]:hidden">
<Field label="Notes for the customer">
<Textarea placeholder="Thank you for your order." />
</Field>
</TabsContent>
</Tabs>
)
}If the triggers are wider than the underline list, the list scrolls horizontally. It shows no scrollbar.
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ferry-ui'
// A value has no space: the tab and its panel use it in their `id`.
const SECTIONS = [
{ value: 'overview', label: 'Overview' },
{ value: 'profile', label: 'Profile' },
{ value: 'notifications', label: 'Notifications' },
{ value: 'security', label: 'Security' },
{ value: 'billing', label: 'Billing' },
{ value: 'members', label: 'Members' },
{ value: 'api-keys', label: 'API keys' },
{ value: 'audit-log', label: 'Audit log' },
]
export default function TabsOverflow() {
return (
<Tabs defaultValue="overview" className="w-full max-w-xs">
<TabsList aria-label="Account settings">
{SECTIONS.map((section) => (
<TabsTrigger key={section.value} value={section.value}>
{section.label}
</TabsTrigger>
))}
</TabsList>
{SECTIONS.map((section) => (
<TabsContent key={section.value} value={section.value} className="text-[13px] text-foreground-light">
The panel of the tab "{section.label}".
</TabsContent>
))}
</Tabs>
)
}activationMode="manual", the arrow keys only move the focus. Enter or Space activates the tab.Each part also accepts the props of its Radix UI primitive and the attributes of its element.
| Prop | Type | Default |
|---|---|---|
value | string | - |
The value for the selected tab, if controlled | ||
defaultValue | string | - |
The value of the tab to select by default, if uncontrolled | ||
onValueChange | ((value: string) => void) | - |
A function called when a new tab is selected | ||
orientation | "horizontal" | "vertical" | horizontal |
The orientation the tabs are layed out. Mainly so arrow navigation is done accordingly (left & right vs. up & down) | ||
dir | "ltr" | "rtl" | - |
The direction of navigation between toolbar items. | ||
activationMode | "manual" | "automatic" | automatic |
Whether a tab is activated automatically or manually. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
variant | "underline" | "pills" | underline |
| ||
loop | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
value is required.
| Prop | Type | Default |
|---|---|---|
valueRequired | string | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
value is required.
| Prop | Type | Default |
|---|---|---|
valueRequired | string | - |
| See the element or the Radix UI primitive this part renders. | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
tabsListVariants({ variant }) returns the classes of TabsList. Use it to give the look of the list to another element.
| Option | Type | Role |
|---|---|---|
variant | 'underline' or 'pills' | The look of the list. The default is underline. |