Tabs

A navigable component that organizes content into separate views where only one view is visible at a time.

Docs
Make changes to your account here. You can update your profile information.
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/tabs";

export default function TabsBasicDemo() {
  return (
    <div>
      <Tabs defaultValue="account">
        <TabsList>
          <TabsTrigger value="account">Account</TabsTrigger>
          <TabsTrigger value="password">Password</TabsTrigger>
        </TabsList>
        <TabsContent value="account">
          <div class="text-sm text-foreground">
            Make changes to your account here. You can update your profile information.
          </div>
        </TabsContent>
        <TabsContent value="password">
          <div class="text-sm text-foreground">
            Change your password here. After saving, you'll be logged out.
          </div>
        </TabsContent>
      </Tabs>
    </div>
  );
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add tabs

Manual

Create the recipe file at src/components/recipes/tabs.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const tabsVariants = tv({
  slots: {
    root: "w-full",
    list: "relative inline-flex h-8 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
    trigger:
      "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-0.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-selected:bg-background data-selected:text-foreground data-selected:shadow-sm",
    content:
      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
    indicator:
      "absolute bottom-0 left-0 z-10 h-0.5 bg-foreground transition-[left,top,width,height] duration-200",
  },
});

export type TabsVariants = VariantProps<typeof tabsVariants>;

Create the component directory and files.

src/components/tabs/tabs.base.tsx:

import { Tabs as ArkTabs } from "@ark-ui/solid/tabs";
import { splitProps, type Component } from "solid-js";
import { tabsVariants } from "../recipes/tabs";

const styles = tabsVariants();

const Root: Component<ArkTabs.RootProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTabs.Root class={styles.root({ class: local.class })} {...others} />;
};

const RootProvider: Component<ArkTabs.RootProviderProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTabs.RootProvider class={styles.root({ class: local.class })} {...others} />;
};

const List: Component<ArkTabs.ListProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTabs.List class={styles.list({ class: local.class })} {...others} />;
};

const Trigger: Component<ArkTabs.TriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTabs.Trigger class={styles.trigger({ class: local.class })} {...others} />;
};

const Content: Component<ArkTabs.ContentProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTabs.Content class={styles.content({ class: local.class })} {...others} />;
};

const Indicator: Component<ArkTabs.IndicatorProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTabs.Indicator class={styles.indicator({ class: local.class })} {...others} />;
};

export const Tabs = {
  Root,
  RootProvider,
  List,
  Trigger,
  Content,
  Indicator,
};

src/components/tabs/index.tsx:

import { splitProps, type Component } from "solid-js";
import { Tabs as TabsBase } from "./tabs.base";
import { Tabs as ArkTabs } from "@ark-ui/solid/tabs";

const Tabs = TabsBase.Root;

const TabsList: Component<ArkTabs.ListProps> = (props) => {
  const [local, others] = splitProps(props, ["class", "children"]);
  return (
    <TabsBase.List class={local.class} {...others}>
      <TabsBase.Indicator />
      {local.children}
    </TabsBase.List>
  );
};

const TabsTrigger = TabsBase.Trigger;
const TabsContent = TabsBase.Content;

export { Tabs, TabsContent, TabsTrigger, TabsBase, TabsList };

export { tabsVariants, type TabsVariants } from "../recipes/tabs";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Usage

Basic Usage

Make changes to your account here. You can update your profile information.
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/tabs";

export default function TabsBasicDemo() {
  return (
    <div>
      <Tabs defaultValue="account">
        <TabsList>
          <TabsTrigger value="account">Account</TabsTrigger>
          <TabsTrigger value="password">Password</TabsTrigger>
        </TabsList>
        <TabsContent value="account">
          <div class="text-sm text-foreground">
            Make changes to your account here. You can update your profile information.
          </div>
        </TabsContent>
        <TabsContent value="password">
          <div class="text-sm text-foreground">
            Change your password here. After saving, you'll be logged out.
          </div>
        </TabsContent>
      </Tabs>
    </div>
  );
}

Disabled Tab

Use the disabled prop on a TabsTrigger to disable that specific tab.

This tab is enabled and functional.
import { Index } from "solid-js";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/tabs";

const tabs = [
  { value: "active", label: "Active", content: "This tab is enabled and functional." },
  { value: "settings", label: "Settings", content: "Configure your preferences here." },
  {
    value: "disabled",
    label: "Disabled",
    content: "This content is not accessible.",
    disabled: true,
  },
];

export default function TabsDisabledDemo() {
  return (
    <div>
      <Tabs defaultValue="active">
        <TabsList>
          <Index each={tabs}>
            {(tab) => (
              <TabsTrigger value={tab().value} disabled={tab().disabled}>
                {tab().label}
              </TabsTrigger>
            )}
          </Index>
        </TabsList>
        <Index each={tabs}>
          {(tab) => (
            <TabsContent value={tab().value}>
              <div class="text-sm text-foreground">{tab().content}</div>
            </TabsContent>
          )}
        </Index>
      </Tabs>
    </div>
  );
}

Advanced Usage

When the composite Tabs doesn’t provide enough control, import the raw primitive parts from TabsBase:

import { TabsBase, TabsTrigger, TabsContent } from "~/components/tabs";

Access Raw Parts

Access raw parts via TabsBase.Root, TabsBase.RootProvider, TabsBase.List, TabsBase.Trigger, TabsBase.Content, and TabsBase.Indicator:

<TabsBase.Root defaultValue="account">
  <TabsBase.List>
    <TabsBase.Indicator />
    <TabsBase.Trigger value="account">Account</TabsBase.Trigger>
    <TabsBase.Trigger value="password">Password</TabsBase.Trigger>
  </TabsBase.List>
  <TabsBase.Content value="account">...</TabsBase.Content>
  <TabsBase.Content value="password">...</TabsBase.Content>
</TabsBase.Root>

Root Provider

Use TabsBase.RootProvider when you need to access the tabs state outside of the tabs tree. This pattern uses the useTabs hook from Ark UI to create a shared context that both the tabs and external elements can reference.

Value: "overview"
Tabs organize content into separate views where only one view is visible at a time.
import { Index, createMemo } from "solid-js";
import { TabsBase, TabsList, TabsTrigger, TabsContent } from "~/components/tabs";
import { useTabs } from "@ark-ui/solid/tabs";

const tabData = [
  {
    value: "overview",
    label: "Overview",
    content: "Tabs organize content into separate views where only one view is visible at a time.",
  },
  {
    value: "usage",
    label: "Usage",
    content: "Use Tabs to switch between different sections of content without navigating away.",
  },
];

export default function TabsRootProviderDemo() {
  const tabs = useTabs({ defaultValue: "overview" });
  const value = createMemo(() => tabs().value);

  return (
    <div class="space-y-4">
      <output class="block text-sm text-muted-foreground">Value: {JSON.stringify(value())}</output>
      <TabsBase.RootProvider value={tabs}>
        <TabsList>
          <Index each={tabData}>
            {(tab) => <TabsTrigger value={tab().value}>{tab().label}</TabsTrigger>}
          </Index>
        </TabsList>
        <Index each={tabData}>
          {(tab) => <TabsContent value={tab().value}>{tab().content}</TabsContent>}
        </Index>
      </TabsBase.RootProvider>
    </div>
  );
}

The key difference:

  • Tabs (Root) — manages its own state internally. Use for simple, self-contained tabs.
  • TabsBase.RootProvider — accepts a pre-created tabs context via useTabs. Use when you need to read or control the tabs state from outside the component tree.

API Reference

See the Ark UI Tabs documentation.