A set of layered sections of content, known as tab panels, that are displayed one at a time.

Basic
Line
Variants Alignment
Disabled
With Icons
Icon Only
Multiple
With Content
Manage your account preferences and profile information.
Line With Content
Manage your account preferences and profile information.
Line Disabled
With Dropdown
View your dashboard metrics and key performance indicators.
Vertical
Manage your account preferences and profile information.
With Input and Button
View your dashboard metrics and key performance indicators.

Installation

Install the dependencies

pnpm add cn class-variance-authority github:sveltery/base

Copy the component

Copy the following files into src/lib/components/ui/tabs.

src/lib/components/ui/tabs/Tabs.svelte
<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>
src/lib/components/ui/tabs/TabsContent.svelte
<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>
src/lib/components/ui/tabs/TabsList.svelte
<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>
src/lib/components/ui/tabs/TabsTrigger.svelte
<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>
src/lib/components/ui/tabs/types.ts
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;
src/lib/components/ui/tabs/variants.ts
// 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>;
src/lib/components/ui/tabs/index.ts
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.