Skip to content
edmi-ui

Tabs

Tabs in a flat sunken track (default), standalone pills, or underlined (line). Horizontal and vertical.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
NAV 59.9998. 4 tokens, 412 holders, rebalanced 2 days ago.
NAV 59.9998. 4 tokens, 412 holders, rebalanced 2 days ago.
NAV 59.9998. 4 tokens, 412 holders, rebalanced 2 days ago.
Terminal window
npx shadcn@latest add @edmi-ui/tabs
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/tabs
Terminal window
yarn dlx shadcn@latest add @edmi-ui/tabs
Terminal window
bunx shadcn@latest add @edmi-ui/tabs

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/tabs
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/tabs
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/tabs
Terminal window
bunx shadcn-vue@latest add @edmi-ui/tabs

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/tabs.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/tabs.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/tabs.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/tabs.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
<Tabs defaultValue="overview" className="w-[420px]">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="holdings">Holdings</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
<TabsContent value="overview">
NAV 59.9998. 4 tokens, 412 holders, rebalanced 2 days ago.
</TabsContent>
<TabsContent value="holdings">Four tokens, weighted by AUM.</TabsContent>
<TabsContent value="activity">No recent trades.</TabsContent>
</Tabs>
<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
</script>
<template>
<Tabs default-value="overview" class="w-[420px]">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="holdings">Holdings</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
<TabsContent value="overview">
NAV 59.9998. 4 tokens, 412 holders, rebalanced 2 days ago.
</TabsContent>
<TabsContent value="holdings">Four tokens, weighted by AUM.</TabsContent>
<TabsContent value="activity">No recent trades.</TabsContent>
</Tabs>
</template>
<script lang="ts">
import * as Tabs from "$lib/components/ui/tabs";
</script>
<Tabs.Root value="overview" class="w-[420px]">
<Tabs.List>
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="holdings">Holdings</Tabs.Trigger>
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview">NAV 59.9998. 4 tokens, 412 holders, rebalanced 2 days ago.</Tabs.Content>
<Tabs.Content value="holdings">Four tokens, weighted by AUM.</Tabs.Content>
<Tabs.Content value="activity">No recent trades.</Tabs.Content>
</Tabs.Root>
registry/ui/tabs.tsx
"use client";
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import * as React from "react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className,
)}
{...props}
/>
);
}
// Flat by default: the active tab is a --tab-active fill + 1px border, with or without a track.
// ✦ `elevation` (on TabsList, flows to the triggers): raised/floating make ONLY the active tab rise (bevel);
// the list/track itself never gets the bevel. The `line` variant has no track and ignores elevation.
const tabsListVariants = cva(
"group/tabs-list text-muted-foreground group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default:
"inline-flex w-fit items-center justify-center gap-0.5 rounded-lg border border-border bg-muted p-[3px] shadow-[inset_0_1px_2px_rgb(0_0_0/0.04)] group-data-[orientation=vertical]/tabs:h-fit",
line: "flex gap-[22px] border-border group-data-[orientation=horizontal]/tabs:border-b group-data-[orientation=vertical]/tabs:gap-1 group-data-[orientation=vertical]/tabs:border-l",
// ✦ no track
pills:
"inline-flex w-fit items-center gap-1 group-data-[orientation=vertical]/tabs:h-fit",
},
},
defaultVariants: {
variant: "default",
},
},
);
const raisedActive =
"data-[active]:border-transparent data-[active]:[background-image:var(--r1-s-face)] data-[active]:[background-origin:border-box] data-[active]:shadow-btn-raised-neutral";
const tabsTriggerVariants = cva(
"relative inline-flex items-center justify-center gap-1.5 text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default:
"h-[30px] flex-1 rounded-[7px] border border-transparent px-3.5 data-[active]:border-border data-[active]:bg-tab-active data-[active]:text-foreground",
line: "-mb-px border-b-2 border-transparent px-0.5 pb-[11px] data-[active]:border-foreground data-[active]:text-foreground group-data-[orientation=vertical]/tabs:mb-0 group-data-[orientation=vertical]/tabs:-ml-px group-data-[orientation=vertical]/tabs:border-b-0 group-data-[orientation=vertical]/tabs:border-l-2 group-data-[orientation=vertical]/tabs:py-1 group-data-[orientation=vertical]/tabs:pr-0 group-data-[orientation=vertical]/tabs:pl-3",
pills:
"h-8 rounded-[7px] border border-transparent px-3 data-[active]:border-border data-[active]:bg-tab-active data-[active]:text-foreground",
},
// ✦ the TabsList elevation, passed down to each trigger
elevation: { flat: "", raised: "" },
},
compoundVariants: [
{ variant: "default", elevation: "raised", class: raisedActive },
{ variant: "pills", elevation: "raised", class: raisedActive },
],
defaultVariants: { variant: "default", elevation: "flat" },
},
);
const TabsListContext = React.createContext<{
variant: "default" | "line" | "pills";
elevation: "flat" | "raised";
}>({ variant: "default", elevation: "flat" });
function TabsList({
className,
variant = "default",
elevation,
activateOnFocus = true,
...props
}: TabsPrimitive.List.Props &
VariantProps<typeof tabsListVariants> & {
/** ✦ depth: raised +1 / floating +2 make the active tab rise (ignored by `line`). */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "control");
return (
<TabsListContext.Provider
value={{
variant: variant ?? "default",
elevation: level === "flat" || level === "sunken" ? "flat" : "raised",
}}
>
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
activateOnFocus={activateOnFocus}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
</TabsListContext.Provider>
);
}
function TabsTrigger({
className,
elevation,
...props
}: TabsPrimitive.Tab.Props & { elevation?: Elevation }) {
const context = React.useContext(TabsListContext);
const own =
elevation && elevation !== "auto"
? elevation === "raised" || elevation === "floating"
? "raised"
: "flat"
: undefined;
return (
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
tabsTriggerVariants({
variant: context.variant,
elevation: own ?? context.elevation,
}),
className,
)}
{...props}
/>
);
}
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
return (
<TabsPrimitive.Panel
data-slot="tabs-content"
className={cn("flex-1 text-sm outline-none", className)}
{...props}
/>
);
}
export {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
tabsListVariants,
tabsTriggerVariants,
};
registry/ui/tabs/Tabs.vue
<script setup lang="ts">
import type { TabsRootEmits, TabsRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<TabsRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<TabsRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TabsRoot
v-slot="slotProps"
data-slot="tabs"
:data-orientation="forwarded.orientation || 'horizontal'"
v-bind="forwarded"
:class="cn('group/tabs flex gap-2 data-[orientation=horizontal]:flex-col', props.class)"
>
<slot v-bind="slotProps" />
</TabsRoot>
</template>
registry/ui/tabs/TabsContent.vue
<script setup lang="ts">
import type { TabsContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsContent } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<TabsContent
data-slot="tabs-content"
:class="cn('flex-1 text-sm outline-none', props.class)"
v-bind="delegatedProps"
>
<slot />
</TabsContent>
</template>
registry/ui/tabs/TabsList.vue
<script setup lang="ts">
import type { TabsListProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { TabsListVariants } from '.'
import { reactiveOmit } from '@vueuse/core'
import { computed, provide } from 'vue'
import { TabsList } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
import { tabsListVariants } from '.'
const props = withDefaults(defineProps<TabsListProps & {
class?: HTMLAttributes['class']
variant?: TabsListVariants['variant']
/** ✦ depth: raised +1 / floating +2 make the active trigger rise (default / pills). Triggers may override. */
elevation?: Elevation
}>(), {
variant: 'default',
elevation: undefined,
})
const level = useElevation(() => props.elevation, 'control')
const raised = computed(() => level.value === 'raised' || level.value === 'floating')
provide('tabsList', {
get variant() { return props.variant },
get raised() { return raised.value },
})
const delegatedProps = reactiveOmit(props, 'class', 'variant', 'elevation')
</script>
<template>
<TabsList
data-slot="tabs-list"
:data-variant="variant"
v-bind="delegatedProps"
:class="cn(tabsListVariants({ variant }), props.class)"
>
<slot />
</TabsList>
</template>
registry/ui/tabs/TabsTrigger.vue
<script setup lang="ts">
import type { TabsTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsTrigger, useForwardProps } from 'reka-ui'
import { computed, inject } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import type { Elevation } from '@/registry/edmi/ui/elevation'
const props = withDefaults(defineProps<TabsTriggerProps & {
class?: HTMLAttributes['class']
/** ✦ overrides the TabsList `elevation` for this trigger (raised/floating = the active trigger rises) */
elevation?: Elevation
}>(), { elevation: undefined })
const context = inject<{ raised?: boolean } | null>('tabsList', null)
const delegatedProps = reactiveOmit(props, 'class', 'elevation')
const resolvedRaised = computed(() =>
props.elevation && props.elevation !== 'auto'
? props.elevation === 'raised' || props.elevation === 'floating'
: !!context?.raised,
)
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TabsTrigger
data-slot="tabs-trigger"
:class="cn(
'relative inline-flex items-center justify-center gap-1.5 text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
// default (in a track)
'group-data-[variant=default]/tabs-list:h-[30px] group-data-[variant=default]/tabs-list:flex-1 group-data-[variant=default]/tabs-list:rounded-[7px] group-data-[variant=default]/tabs-list:border group-data-[variant=default]/tabs-list:border-transparent group-data-[variant=default]/tabs-list:px-3.5 group-data-[variant=default]/tabs-list:data-[state=active]:border-border group-data-[variant=default]/tabs-list:data-[state=active]:bg-tab-active group-data-[variant=default]/tabs-list:data-[state=active]:text-foreground',
// pills (✦ no track)
'group-data-[variant=pills]/tabs-list:h-8 group-data-[variant=pills]/tabs-list:rounded-[7px] group-data-[variant=pills]/tabs-list:border group-data-[variant=pills]/tabs-list:border-transparent group-data-[variant=pills]/tabs-list:px-3 group-data-[variant=pills]/tabs-list:data-[state=active]:border-border group-data-[variant=pills]/tabs-list:data-[state=active]:bg-tab-active group-data-[variant=pills]/tabs-list:data-[state=active]:text-foreground',
// line
'group-data-[variant=line]/tabs-list:-mb-px group-data-[variant=line]/tabs-list:border-b-2 group-data-[variant=line]/tabs-list:border-transparent group-data-[variant=line]/tabs-list:px-0.5 group-data-[variant=line]/tabs-list:pb-[11px] group-data-[variant=line]/tabs-list:data-[state=active]:border-foreground group-data-[variant=line]/tabs-list:data-[state=active]:text-foreground group-data-[orientation=vertical]/tabs:group-data-[variant=line]/tabs-list:mb-0 group-data-[orientation=vertical]/tabs:group-data-[variant=line]/tabs-list:-ml-px group-data-[orientation=vertical]/tabs:group-data-[variant=line]/tabs-list:border-b-0 group-data-[orientation=vertical]/tabs:group-data-[variant=line]/tabs-list:border-l-2 group-data-[orientation=vertical]/tabs:group-data-[variant=line]/tabs-list:py-1 group-data-[orientation=vertical]/tabs:group-data-[variant=line]/tabs-list:pr-0 group-data-[orientation=vertical]/tabs:group-data-[variant=line]/tabs-list:pl-3',
// elevation ✦ raised/floating: only the active trigger rises (default + pills; line ignores it)
resolvedRaised && 'group-data-[variant=default]/tabs-list:data-[state=active]:border-transparent group-data-[variant=default]/tabs-list:data-[state=active]:[background-image:var(--r1-s-face)] group-data-[variant=default]/tabs-list:data-[state=active]:[background-origin:border-box] group-data-[variant=default]/tabs-list:data-[state=active]:shadow-btn-raised-neutral group-data-[variant=pills]/tabs-list:data-[state=active]:border-transparent group-data-[variant=pills]/tabs-list:data-[state=active]:[background-image:var(--r1-s-face)] group-data-[variant=pills]/tabs-list:data-[state=active]:[background-origin:border-box] group-data-[variant=pills]/tabs-list:data-[state=active]:shadow-btn-raised-neutral',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</TabsTrigger>
</template>
registry/ui/tabs/index.ts
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
export { default as Tabs } from "./Tabs.vue";
export { default as TabsContent } from "./TabsContent.vue";
export { default as TabsList } from "./TabsList.vue";
export { default as TabsTrigger } from "./TabsTrigger.vue";
// Flat by default: the active tab is a --tab-active fill + 1px border (with a track for `default`, without for `pills`).
// `line` has no track; it underlines the active tab. `elevation` ✦ (on TabsList or a trigger) raised/floating make ONLY the active
// `default`/`pills` trigger rise (bevel); `line` ignores it.
export const tabsListVariants = cva(
"group/tabs-list text-muted-foreground group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default:
"inline-flex w-fit items-center justify-center gap-0.5 rounded-lg border border-border bg-muted p-[3px] shadow-[inset_0_1px_2px_rgb(0_0_0/0.04)] group-data-[orientation=vertical]/tabs:h-fit",
// ✦ no track
pills:
"inline-flex w-fit items-center gap-1 group-data-[orientation=vertical]/tabs:h-fit",
line: "flex gap-[22px] border-border group-data-[orientation=horizontal]/tabs:border-b group-data-[orientation=vertical]/tabs:gap-1 group-data-[orientation=vertical]/tabs:border-l",
},
},
defaultVariants: {
variant: "default",
},
},
);
export type TabsListVariants = VariantProps<typeof tabsListVariants>;
src/lib/registry/ui/tabs/tabs.svelte
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: TabsPrimitive.RootProps = $props();
</script>
<TabsPrimitive.Root
bind:ref
bind:value
data-slot="tabs"
class={cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className)}
{...restProps}
/>
src/lib/registry/ui/tabs/tabs-list.svelte
<script lang="ts" module>
import { getContext, setContext } from "svelte";
import { tv, type VariantProps } from "tailwind-variants";
// Flat by default: the active tab is `--tab-active` + 1px border (with or without a track).
// `default` = track, `line` = underline, `pills` ✦ = no track. elevation ✦ raised/floating make ONLY the
// active tab rise (default/pills; `line` ignores it); the level is passed down to every trigger.
export const tabsListVariants = tv({
base: "group/tabs-list text-muted-foreground group-data-[orientation=vertical]/tabs:flex-col",
variants: {
variant: {
default: "inline-flex w-fit items-center justify-center gap-0.5 rounded-lg border border-border bg-muted p-[3px] shadow-[inset_0_1px_2px_rgb(0_0_0/0.04)] group-data-[orientation=vertical]/tabs:h-fit",
line: "flex gap-[22px] border-border group-data-[orientation=horizontal]/tabs:border-b group-data-[orientation=vertical]/tabs:gap-1 group-data-[orientation=vertical]/tabs:border-l",
// ✦ no track
pills: "inline-flex w-fit items-center gap-1",
},
},
defaultVariants: {
variant: "default",
},
});
export type TabsListVariant = VariantProps<typeof tabsListVariants>["variant"];
interface TabsListContext {
variant: NonNullable<TabsListVariant>;
raised: boolean;
}
export function setTabsListCtx(ctx: TabsListContext) {
setContext("tabsList", ctx);
}
export function getTabsListCtx() {
return getContext<TabsListContext | undefined>("tabsList");
}
</script>
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
let {
ref = $bindable(null),
variant = "default",
elevation = "auto",
class: className,
...restProps
}: TabsPrimitive.ListProps & {
variant?: TabsListVariant;
/** ✦ depth: raised +1 / floating +2 make the active trigger rise (default/pills). */
elevation?: Elevation;
} = $props();
const level = useElevation(() => elevation, "control");
const raised = $derived(level.current === "raised" || level.current === "floating");
setTabsListCtx({
get variant() {
return variant ?? "default";
},
get raised() {
return raised;
},
});
</script>
<TabsPrimitive.List
bind:ref
data-slot="tabs-list"
data-variant={variant}
class={cn(tabsListVariants({ variant }), className)}
{...restProps}
/>
src/lib/registry/ui/tabs/tabs-trigger.svelte
<script lang="ts" module>
import { tv } from "tailwind-variants";
// tabs.trigger elevation: only the active trigger rises (bevel).
const raisedActive =
"data-[state=active]:border-transparent data-[state=active]:[background-image:var(--r1-s-face)] data-[state=active]:[background-origin:border-box] data-[state=active]:shadow-btn-raised-neutral";
export const tabsTriggerVariants = tv({
base: "relative inline-flex items-center justify-center gap-1.5 text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: "h-[30px] flex-1 rounded-[7px] border border-transparent px-3.5 data-[state=active]:border-border data-[state=active]:bg-tab-active data-[state=active]:text-foreground",
line: "-mb-px border-b-2 border-transparent px-0.5 pb-[11px] data-[state=active]:border-foreground data-[state=active]:text-foreground group-data-[orientation=vertical]/tabs:mb-0 group-data-[orientation=vertical]/tabs:-ml-px group-data-[orientation=vertical]/tabs:border-b-0 group-data-[orientation=vertical]/tabs:border-l-2 group-data-[orientation=vertical]/tabs:py-1 group-data-[orientation=vertical]/tabs:pr-0 group-data-[orientation=vertical]/tabs:pl-3",
// ✦ no track
pills: "h-8 rounded-[7px] border border-transparent px-3 data-[state=active]:border-border data-[state=active]:bg-tab-active data-[state=active]:text-foreground",
},
elevation: { flat: "", raised: "" },
},
compoundVariants: [
{ variant: "default", elevation: "raised", class: raisedActive },
{ variant: "pills", elevation: "raised", class: raisedActive },
],
defaultVariants: { variant: "default", elevation: "flat" },
});
</script>
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import { getTabsListCtx } from "./tabs-list.svelte";
let {
ref = $bindable(null),
class: className,
elevation = "auto",
...restProps
}: TabsPrimitive.TriggerProps & {
/** ✦ overrides the TabsList `elevation` (raised/floating = the active trigger rises). */
elevation?: Elevation;
} = $props();
const ctx = getTabsListCtx();
const raised = $derived(
elevation !== "auto" ? elevation === "raised" || elevation === "floating" : (ctx?.raised ?? false)
);
</script>
<TabsPrimitive.Trigger
bind:ref
data-slot="tabs-trigger"
class={cn(
tabsTriggerVariants({ variant: ctx?.variant ?? "default", elevation: raised ? "raised" : "flat" }),
className
)}
{...restProps}
/>
src/lib/registry/ui/tabs/tabs-content.svelte
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>
<TabsPrimitive.Content
bind:ref
data-slot="tabs-content"
class={cn("flex-1 text-sm outline-none", className)}
{...restProps}
/>
src/lib/registry/ui/tabs/index.ts
import Root from "./tabs.svelte";
import Content from "./tabs-content.svelte";
import List, {
type TabsListVariant,
tabsListVariants,
} from "./tabs-list.svelte";
import Trigger, { tabsTriggerVariants } from "./tabs-trigger.svelte";
export {
Content,
Content as TabsContent,
List,
List as TabsList,
Root,
//
Root as Tabs,
type TabsListVariant,
Trigger,
Trigger as TabsTrigger,
tabsListVariants,
tabsTriggerVariants,
};
StackBlitz ↗StackBlitz ↗StackBlitz ↗
Open positions.
Open positions.
Open positions.

See the Elevation guide for the levels, roles and layered mode.

Flat by default. Set raised on TabsList: only the ACTIVE trigger rises (bevel); the list or track itself never does. It flows to the triggers (a trigger can override it) and line ignores it.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

default tabs.
pills tabs.

Raised (+1): only the active tab rises

default tabs.
pills tabs.

Flat (0)

Raised (+1): only the active tab rises

Flat (0)

Raised (+1): only the active tab rises

Prop Type Default
Tabs defaultValue / value string none
Tabs orientation horizontal | vertical horizontal
TabsList variant default | line | pills default
TabsTrigger disabled boolean false
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Parts: Tabs, TabsList, TabsTrigger, TabsContent.

  • Inside a track the active tab is flat: --tab-active fill and a 1px border, no gradient, no lip.
  • pills variant (no track) is an inline row of pills; active = --tab-active + 1px border.
  • line variant underlines the active tab with the foreground colour.