A set of layered sections of content, known as tab panels, that are displayed one at a time.
Installation
Install the dependencies
pnpm add cn class-variance-authority github:sveltery/baseCopy the component
Copy the following files into src/lib/components/ui/tabs.
<script lang="ts">
import type { ComponentProps } from 'svelte';
import { Tabs as Primitive } from '@sveltery/base/tabs';
import { cn } from 'cn';
let { children, class: classProp, value = $bindable(), orientation = 'horizontal', ...props }: ComponentProps<typeof Primitive.Root> = $props();
</script>
<Primitive.Root data-slot="tabs" data-orientation={orientation} {orientation} {...props} class={cn('gap-2 group/tabs flex data-horizontal:flex-col', classProp)} bind:value>
{@render children?.()}
</Primitive.Root><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Tabs as Primitive } from '@sveltery/base/tabs';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Panel> = $props();
</script>
<Primitive.Panel data-slot="tabs-content" {...props} class={cn('text-sm flex-1 outline-none', classProp)}>
{@render children?.()}
</Primitive.Panel><script lang="ts">
import { Tabs as Primitive } from '@sveltery/base/tabs';
import { cn } from 'cn';
import type { TabsListProps } from './types.js';
import { tabsListVariants } from './variants.js';
let { children, class: classProp, variant = 'default', ...props }: TabsListProps = $props();
</script>
<Primitive.List data-slot="tabs-list" data-variant={variant} {...props} class={cn(tabsListVariants({ variant }), classProp)}>
{@render children?.()}
</Primitive.List><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Tabs as Primitive } from '@sveltery/base/tabs';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Tab> = $props();
</script>
<Primitive.Tab
data-slot="tabs-trigger"
{...props}
class={cn(
"gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
classProp,
)}
>
{@render children?.()}
</Primitive.Tab>import type { ComponentProps } from 'svelte';
import type { Tabs as Primitive } from '@sveltery/base/tabs';
import type { TabsListVariants } from './variants.js';
export type TabsListProps = ComponentProps<typeof Primitive.List> & TabsListVariants;// Adapted from shadcn-ui/ui d75a96ab; MIT: packages/ui/THIRD_PARTY_NOTICES.md.
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
export const tabsListVariants = cva('rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list inline-flex w-fit items-center justify-center text-muted-foreground group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col', {
variants: {
variant: {
default: 'bg-muted',
line: 'gap-1 bg-transparent',
},
},
defaultVariants: { variant: 'default' },
});
export type TabsListVariants = VariantProps<typeof tabsListVariants>;export { default as Tabs } from './Tabs.svelte';
export { default as TabsList } from './TabsList.svelte';
export { default as TabsTrigger } from './TabsTrigger.svelte';
export { default as TabsContent } from './TabsContent.svelte';
export { tabsListVariants } from './variants.js';
export type { TabsListVariants } from './variants.js';
export type { TabsListProps } from './types.js';Update the import paths
Change the imports to match your project setup.
Usage
<script lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/components/ui/tabs';
let tab = $state('account');
</script><Tabs bind:value={tab}>
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Make changes to your account here.</TabsContent>
<TabsContent value="password">Change your password here.</TabsContent>
</Tabs>Pass value without bind: to start on a tab and let the user move from there. TabsList takes variant="line" for the underlined style, and Tabs takes orientation="vertical" for a vertical list.
API reference
Tabs is styled on top of the Sveltery Base Tabs primitive, which owns its behavior, focus handling and accessibility. See the Base UI Tabs documentation for the behavior it ports.