Tabs
Organizes content into tabbed sections.
Prague
06:05
3h 30m
Malaga
06:05
Malaga
07:25
3h 20m
Prague
10:45
<script lang="ts">
import { Tabs } from "bits-ui";
import Airplane from "phosphor-svelte/lib/Airplane";
</script>
<div class="pt-6">
<Tabs.Root
value="outbound"
class="w-[390px] rounded-card border border-muted bg-background-alt p-3 shadow-card"
>
<Tabs.List
class="grid w-full grid-cols-2 gap-1 rounded-9px bg-dark-10 p-1 text-sm leading-[0.01em] font-semibold shadow-mini-inset dark:border dark:border-neutral-600/30 dark:bg-background"
>
<Tabs.Trigger
value="outbound"
class="h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted"
>Outbound</Tabs.Trigger
>
<Tabs.Trigger
value="inbound"
class="h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted"
>Inbound</Tabs.Trigger
>
</Tabs.List>
<Tabs.Content value="outbound" class="pt-3 select-none">
<div class="grid grid-cols-3 grid-rows-2 gap-0 p-4 pb-1">
<div class="text-left">
<h4
class="mb-2 text-[20px] leading-none font-semibold tracking-[-0.01em]"
>
Prague
</h4>
<p class="text-sm font-medium text-muted-foreground">06:05</p>
</div>
<div class="self-end text-center">
<p class="text-sm font-medium text-muted-foreground">3h 30m</p>
</div>
<div class="text-right">
<h4
class="mb-2 text-[20px] leading-none font-semibold tracking-[-0.01em]"
>
Malaga
</h4>
<p class="text-sm font-medium text-muted-foreground">06:05</p>
</div>
<div class="relative col-span-3">
<hr
class="relative top-4 h-px border-1 border-dashed border-border-input"
/>
<div class="absolute left-1/2 -translate-x-1/2 bg-background-alt p-1">
<Airplane class="size-6 rotate-90 text-muted-foreground" />
</div>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="inbound" class="pt-3 select-none">
<div class="grid grid-cols-3 grid-rows-2 gap-0 p-4 pb-1">
<div class="text-left">
<h4
class="mb-2 text-[20px] leading-none font-semibold tracking-[-0.01em]"
>
Malaga
</h4>
<p class="text-sm font-medium text-muted-foreground">07:25</p>
</div>
<div class="self-end text-center">
<p class="text-sm font-medium text-muted-foreground">3h 20m</p>
</div>
<div class="text-right">
<h4
class="mb-2 text-[20px] leading-none font-semibold tracking-[-0.01em]"
>
Prague
</h4>
<p class="text-sm font-medium text-muted-foreground">10:45</p>
</div>
<div class="relative col-span-3">
<hr
class="relative top-4 h-px border-1 border-dashed border-border-input"
/>
<div class="absolute left-1/2 -translate-x-1/2 bg-background-alt p-1">
<Airplane class="size-6 rotate-90 text-muted-foreground" />
</div>
</div>
</div>
</Tabs.Content>
</Tabs.Root>
</div>
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(0 0% 0%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(0 0% 100%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Structure
<script lang="ts">
import { Tabs } from "bits-ui";
</script>
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger />
</Tabs.List>
<Tabs.Content />
</Tabs.Root>
Managing Value State
This section covers how to manage the value state of the component.
Two-Way Binding
Use bind:value for simple, automatic state synchronization:
<script lang="ts">
import { Tabs } from "bits-ui";
let myValue = $state("");
</script>
<button onclick={() => (myValue = "tab-1")}> Activate tab 1 </button>
<Tabs.Root bind:value={myValue}>
<!-- -->
</Tabs.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { Tabs } from "bits-ui";
let myValue = $state("");
function getValue() {
return myValue;
}
function setValue(newValue: string) {
myValue = newValue;
}
</script>
<Tabs.Root bind:value={getValue, setValue}>
<!-- ... -->
</Tabs.Root>
Orientation
The orientation prop is used to determine the orientation of the Tabs component, which influences how keyboard navigation will work.
When the orientation is set to 'horizontal', the ArrowLeft and ArrowRight keys will move the focus to the previous and next tab, respectively. When the orientation is set to 'vertical', the ArrowUp and ArrowDown keys will move the focus to the previous and next tab, respectively.
<Tabs.Root orientation="horizontal">
<!-- ... -->
</Tabs.Root>
<Tabs.Root orientation="vertical">
<!-- ... -->
</Tabs.Root>
Activation Mode
By default, the Tabs component will automatically activate the tab associated with a trigger when that trigger is focused. This behavior can be disabled by setting the activationMode prop to 'manual'.
When set to 'manual', the user will need to activate the tab by pressing the trigger.
<Tabs.Root activationMode="manual">
<!-- ... -->
</Tabs.Root>
Panel Focus
Tabs.Content defaults to tabindex="0", so keyboard users can focus the panel. Keep this default when the panel has no focusable elements or its first meaningful content is not focusable, even if a button or link appears later.
When the first meaningful content is focusable, set tabindex={-1} to let Tab move directly to that element. Choose this based on your content, following the WAI-ARIA tabs pattern.
<Tabs.Content value="settings" tabindex={-1}>
<button>Update settings</button>
</Tabs.Content>
API Reference
The root tabs component which contains the other tab components.
| Property | Details |
|---|---|
value | |
onValueChange | |
activationMode | |
disabled | |
loop | |
orientation | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-orientation | |
data-tabs-root |
The component containing the tab triggers.
| Property | Details |
|---|---|
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-orientation | |
data-tabs-list |
The trigger for a tab.
| Property | Details |
|---|---|
value | |
disabled | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-value | |
data-orientation | |
data-disabled | |
data-tabs-trigger |
The panel containing the contents of a tab.
| Property | Details |
|---|---|
value | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-tabs-content |