Tabs
Tabs is Density’s controlled tab component. Give an item content to render its panel, or omit it for document-style navigation without panels.
import { Tabs } from "density-base-ui";
import { useState } from "react";
export default function TabsExample() {
const [value, setValue] = useState("foo");
return (
<Tabs
aria-label="Example tabs"
value={value}
onValueChange={setValue}
items={[
{ id: "foo", label: "Foo" },
{ id: "bar", label: "Bar" },
{ id: "baz", label: "Baz" },
]}
/>
);
}Content
Give an item content to render its panel. Omit it for document navigation.
import { Skeleton, Tabs, View } from "density-base-ui";
import { useState } from "react";
export default function TabsContentExample() {
const [value, setValue] = useState("foo");
return (
<Tabs
aria-label="Example tabs"
items={[
{
id: "foo",
label: "Foo",
content: (
<View align="start" gap="2xs" padding={false}>
<Skeleton style={{ width: "100%" }} />
<Skeleton style={{ width: "84%" }} />
<Skeleton style={{ width: "92%" }} />
<Skeleton style={{ width: "68%" }} />
<Skeleton style={{ width: "76%" }} />
</View>
),
},
{
id: "bar",
label: "Bar",
content: (
<View align="start" gap="2xs" padding={false}>
<Skeleton style={{ width: "100%" }} />
<Skeleton style={{ width: "84%" }} />
<Skeleton style={{ width: "92%" }} />
<Skeleton style={{ width: "68%" }} />
<Skeleton style={{ width: "76%" }} />
</View>
),
},
{
id: "baz",
label: "Baz",
content: (
<View align="start" gap="2xs" padding={false}>
<Skeleton style={{ width: "100%" }} />
<Skeleton style={{ width: "84%" }} />
<Skeleton style={{ width: "92%" }} />
<Skeleton style={{ width: "68%" }} />
<Skeleton style={{ width: "76%" }} />
</View>
),
},
]}
onValueChange={setValue}
value={value}
/>
);
}Dismiss control
Set an item’s dismissable flag to show its Close control. Use onItemDismiss to remove that item from the consumer-owned list.
import { Tabs } from "density-base-ui";
import { useState } from "react";
const initialItems = [
{ id: "readme", label: "README.md", dismissable: true },
{ id: "package", label: "package.json", dismissable: true },
{ id: "config", label: "config.ts", dismissable: true },
];
export default function TabsDismissableExample() {
const [items, setItems] = useState(initialItems);
const [value, setValue] = useState(initialItems[0].id);
const dismissTab = (dismissedItemId: string) => {
const dismissedItemIndex = items.findIndex(
(item) => item.id === dismissedItemId,
);
const remainingItems = items.filter((item) => item.id !== dismissedItemId);
setItems(remainingItems);
if (dismissedItemId === value && remainingItems.length > 0) {
setValue(remainingItems[Math.max(0, dismissedItemIndex - 1)].id);
}
};
return (
<Tabs
aria-label="Open files"
items={items}
onItemDismiss={dismissTab}
onValueChange={setValue}
value={value}
/>
);
}Start content
Use childrenStart for content before a tab label, such as a status Indicator or icon.
import { Indicator, Tabs } from "density-base-ui";
import { FileText } from "lucide-react";
import { useState } from "react";
export default function TabsChildrenStartExample() {
const [value, setValue] = useState("activity");
return (
<Tabs
aria-label="Tabs with start content"
items={[
{
id: "activity",
label: "Activity",
childrenStart: <Indicator />,
},
{
id: "files",
label: "Files",
childrenStart: <FileText aria-hidden="true" size="1em" />,
},
{ id: "settings", label: "Settings" },
]}
onValueChange={setValue}
value={value}
/>
);
}Orientation
Vertical tabs place the tab list beside the selected panel.
import { useState, type ComponentProps } from "react";
import { Separator, Skeleton, Tabs, View } from "density-base-ui";
const items = [
{
id: "foo",
label: "Foo",
content: (
<View align="start" gap="2xs" padding={false}>
<Skeleton style={{ width: "100%" }} />
<Skeleton style={{ width: "84%" }} />
<Skeleton style={{ width: "92%" }} />
<Skeleton style={{ width: "68%" }} />
<Skeleton style={{ width: "76%" }} />
</View>
),
},
{
id: "bar",
label: "Bar",
content: (
<View align="start" gap="2xs" padding={false}>
<Skeleton style={{ width: "100%" }} />
<Skeleton style={{ width: "84%" }} />
<Skeleton style={{ width: "92%" }} />
<Skeleton style={{ width: "68%" }} />
<Skeleton style={{ width: "76%" }} />
</View>
),
},
{
id: "baz",
label: "Baz",
content: (
<View align="start" gap="2xs" padding={false}>
<Skeleton style={{ width: "100%" }} />
<Skeleton style={{ width: "84%" }} />
<Skeleton style={{ width: "92%" }} />
<Skeleton style={{ width: "68%" }} />
<Skeleton style={{ width: "76%" }} />
</View>
),
},
];
function OrientationTabs(
props: Omit<ComponentProps<typeof Tabs>, "onValueChange" | "value">,
) {
const [value, setValue] = useState("foo");
return <Tabs {...props} onValueChange={setValue} value={value} />;
}
export default function TabsOrientationExample() {
return (
<View align="stretch" gap="md" padding={false}>
<OrientationTabs items={items} orientation="horizontal" />
<Separator orientation="horizontal" />
<OrientationTabs items={items} orientation="vertical" />
</View>
);
}Tab style
Use button for independent controls, block for tabs that include block-like seams, and underline for document-like navigation. Block and underline tabs keep their structural seams in Tabs’ local styles.
import { Tabs, View } from "density-base-ui";
import { useState } from "react";
const items = [
{ id: "overview", label: "Overview" },
{ id: "activity", label: "Activity" },
{ id: "settings", label: "Settings" },
];
function StyledTabs({
tabStyle,
}: {
tabStyle: "button" | "block" | "underline";
}) {
const [value, setValue] = useState("overview");
return (
<Tabs
aria-label={`${tabStyle} tabs`}
items={items}
onValueChange={setValue}
tabStyle={tabStyle}
value={value}
/>
);
}
export default function TabsTabStyleExample() {
return (
<View align="stretch" gap="md" padding={false}>
<StyledTabs tabStyle="button" />
<StyledTabs tabStyle="block" />
<StyledTabs tabStyle="underline" />
</View>
);
}Gap
import { useState, type ComponentProps } from "react";
import { Tabs, View } from "density-base-ui";
const items = [
{ id: "foo", label: "Foo" },
{ id: "bar", label: "Bar" },
{ id: "baz", label: "Baz" },
];
function GapTabs(
props: Omit<ComponentProps<typeof Tabs>, "onValueChange" | "value">,
) {
const [value, setValue] = useState("foo");
return <Tabs {...props} onValueChange={setValue} value={value} />;
}
export default function TabsGapExample() {
return (
<View align="stretch" gap="md" padding={false}>
<GapTabs gap="none" items={items} />
<GapTabs gap="2xs" items={items} />
<GapTabs gap="xs" items={items} />
<GapTabs gap="sm" items={items} />
<GapTabs gap="md" items={items} />
<GapTabs gap="lg" items={items} />
<GapTabs gap="xl" items={items} />
<GapTabs gap="2xl" items={items} />
<GapTabs gap="3xl" items={items} />
</View>
);
}Padding
padding controls the outer tab-list wrapper. It defaults to true; set it to false for a flush tab list. Individual tabs keep their own built-in hit-area padding.
import { useState, type ComponentProps } from "react";
import { Tabs, View } from "density-base-ui";
const items = [
{ id: "foo", label: "Foo" },
{ id: "bar", label: "Bar" },
{ id: "baz", label: "Baz" },
];
function PaddingTabs(
props: Omit<ComponentProps<typeof Tabs>, "onValueChange" | "value">,
) {
const [value, setValue] = useState("foo");
return <Tabs {...props} onValueChange={setValue} value={value} />;
}
export default function TabsPaddingExample() {
return (
<View align="stretch" gap="md" padding={false}>
<PaddingTabs items={items} padding={false} />
<PaddingTabs items={items} />
</View>
);
}Emphasis
import { useState, type ComponentProps } from "react";
import { Tabs, View } from "density-base-ui";
const items = [
{ id: "foo", label: "Foo" },
{ id: "bar", label: "Bar" },
{ id: "baz", label: "Baz" },
];
function EmphasisTabs(
props: Omit<ComponentProps<typeof Tabs>, "onValueChange" | "value">,
) {
const [value, setValue] = useState("foo");
return <Tabs {...props} onValueChange={setValue} value={value} />;
}
export default function TabsEmphasisExample() {
return (
<View align="stretch" gap="md" padding={false}>
<EmphasisTabs emphasis="primary" items={items} />
<EmphasisTabs emphasis="secondary" items={items} />
<EmphasisTabs emphasis="outline" items={items} />
<EmphasisTabs emphasis="ghost-primary" items={items} />
<EmphasisTabs emphasis="ghost-secondary" items={items} />
<EmphasisTabs emphasis="ghost-tertiary" items={items} />
</View>
);
}Size
import { useState, type ComponentProps } from "react";
import { Tabs, View } from "density-base-ui";
const items = [
{ id: "foo", label: "Foo" },
{ id: "bar", label: "Bar" },
{ id: "baz", label: "Baz" },
];
function SizeTabs(
props: Omit<ComponentProps<typeof Tabs>, "onValueChange" | "value">,
) {
const [value, setValue] = useState("foo");
return <Tabs {...props} onValueChange={setValue} value={value} />;
}
export default function TabsSizeExample() {
return (
<View align="stretch" gap="md" padding={false}>
<SizeTabs items={items} size="2xs" />
<SizeTabs items={items} size="xs" />
<SizeTabs items={items} size="sm" />
<SizeTabs items={items} size="md" />
<SizeTabs items={items} size="lg" />
<SizeTabs items={items} size="xl" />
<SizeTabs items={items} size="2xl" />
<SizeTabs items={items} size="3xl" />
</View>
);
}| Prop | Values | Required | Default | Behavior | Description |
|---|---|---|---|---|---|
depth | 1, 2, 3, 4, 5, 6, 7 | Not required | "1" | — | — |
emphasis | primary, secondary, outline, ghost-primary, ghost-secondary, ghost-tertiary | Not required | — | Attribute: data-dn-emphasis | — |
gap | 2xs, xs, sm, md, lg, xl, 2xl, 3xl, none | Not required | — | Attribute: data-dn-gap | — |
items | TabsItem[] | Required | — | — | — |
listProps | Omit<React.HTMLAttributes<HTMLDivElement>, "children"> | Not required | — | — | — |
listRef | React.Ref<HTMLDivElement> | Not required | — | — | — |
onItemDismiss | (id: string) => void | Not required | — | — | — |
onValueChange | (value: string, eventDetails: Parameters<NonNullable<BaseTabsRootProps["onValueChange"]>>[1]) => void | Not required | — | — | — |
orientation | vertical, horizontal | Not required | "horizontal" | Attribute: data-dn-orientation | — |
padding | false, true | Not required | true | Attribute: data-dn-padding | — |
size | 2xs, xs, sm, md, lg, xl, 2xl, 3xl | Not required | — | Attribute: data-dn-size | — |
tabStyle | button, block, underline | Not required | "button" | — | — |
value | string | Required | — | — | — |
This component also accepts props from Base UI APIs. They are passed through and intentionally kept out of this focused Density API table.