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>
  );
}