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="rounded-card border-muted bg-background-alt shadow-card w-[390px] border p-3"
>
<Tabs.List
class="rounded-9px bg-dark-10 shadow-mini-inset dark:bg-background grid w-full grid-cols-2 gap-1 p-1 text-sm font-semibold leading-[0.01em] dark:border dark:border-neutral-600/30"
>
<Tabs.Trigger
value="outbound"
class="data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white"
>Outbound</Tabs.Trigger
>
<Tabs.Trigger
value="inbound"
class="data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white"
>Inbound</Tabs.Trigger
>
</Tabs.List>
<Tabs.Content value="outbound" class="select-none pt-3">
<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] font-semibold leading-none tracking-[-0.01em]"
>
Prague
</h4>
<p class="text-muted-foreground text-sm font-medium">06:05</p>
</div>
<div class="self-end text-center">
<p class="text-muted-foreground text-sm font-medium">3h 30m</p>
</div>
<div class="text-right">
<h4
class="mb-2 text-[20px] font-semibold leading-none tracking-[-0.01em]"
>
Malaga
</h4>
<p class="text-muted-foreground text-sm font-medium">06:05</p>
</div>
<div class="relative col-span-3">
<hr
class="border-border-input border-1 relative top-4 h-px border-dashed"
/>
<div class="bg-background-alt absolute left-1/2 -translate-x-1/2 p-1">
<Airplane class="text-muted-foreground size-6 rotate-90" />
</div>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="inbound" class="select-none pt-3">
<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] font-semibold leading-none tracking-[-0.01em]"
>
Malaga
</h4>
<p class="text-muted-foreground text-sm font-medium">07:25</p>
</div>
<div class="self-end text-center">
<p class="text-muted-foreground text-sm font-medium">3h 20m</p>
</div>
<div class="text-right">
<h4
class="mb-2 text-[20px] font-semibold leading-none tracking-[-0.01em]"
>
Prague
</h4>
<p class="text-muted-foreground text-sm font-medium">10:45</p>
</div>
<div class="relative col-span-3">
<hr
class="border-border-input border-1 relative top-4 h-px border-dashed"
/>
<div class="bg-background-alt absolute left-1/2 -translate-x-1/2 p-1">
<Airplane class="text-muted-foreground size-6 rotate-90" />
</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>
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 |