Tabs
A navigable component that organizes content into separate views where only one view is visible at a time.
Docsimport { 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 tabsManual
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
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.
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.
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 viauseTabs. Use when you need to read or control the tabs state from outside the component tree.
API Reference
See the Ark UI Tabs documentation.