Skip to content
Navigation

Tabs

MD3 tabs — a TabList of Tabs over one TabPanel per tab.

Walkthrough

1. Basic

A TabList of Tabs over one TabPanel per tab, matched by value — views of one thing on one page, not a way between pages.

Three services, all green.

<script lang="ts">	import { Tabs, TabList, Tab, TabPanel } from 'omaris';​	let active = $state('overview');</script>​<Tabs bind:value={active} class="w-full">	<TabList label="Project sections">		<Tab value="overview" label="Overview" />		<Tab value="activity" label="Activity" />		<Tab value="settings" label="Settings" />	</TabList>​	<TabPanel value="overview" class="pt-4">		<p class="text-body-md text-muted-foreground">Three services, all green.</p>	</TabPanel>	<TabPanel value="activity" class="pt-4">		<p class="text-body-md text-muted-foreground">Amina deployed storefront 3 minutes ago.</p>	</TabPanel>	<TabPanel value="settings" class="pt-4">		<p class="text-body-md text-muted-foreground">Region, domains, environment variables.</p>	</TabPanel></Tabs>

2. Icons and badges

stacked puts the icon above the label, MD3's two-line primary tab — for a top-level set where the icon is worth the extra height.

12 unread.

<script lang="ts">	import { Badge, Tabs, TabList, Tab, TabPanel } from 'omaris';​	let active = $state('inbox');</script>​<Tabs bind:value={active} class="w-full">	<TabList label="Mail">		<Tab value="inbox" label="Inbox" stacked>			{#snippet icon()}				...			{/snippet}			<!--				A count, not a pill: `value` gives the badge MD3's circular count				shape and `size="sm"` keeps it under the label it rides beside.				A default-size `<Badge>` with children is a status pill — twice				the height of the tab's own text, and it drags the label off				centre under the icon.			-->			{#snippet badge()}				<Badge tone="destructive" size="sm" value={12} label="12 unread" />			{/snippet}		</Tab>		<Tab value="sent" label="Sent" stacked>			{#snippet icon()}				...			{/snippet}		</Tab>	</TabList>​	<TabPanel value="inbox" class="pt-4">		<p class="text-body-md text-muted-foreground">12 unread.</p>	</TabPanel>	<TabPanel value="sent" class="pt-4">		<p class="text-body-md text-muted-foreground">Nothing sent today.</p>	</TabPanel></Tabs>

3. Secondary and fitted

secondary is for a set nested inside a primary tab's content. fitted shares the width evenly instead of hugging the labels.

Showing the last day.

<script lang="ts">	import { Tabs, TabList, Tab, TabPanel } from 'omaris';​	let active = $state('day');</script>​<Tabs bind:value={active} variant="secondary" fitted class="w-full max-w-md">	<TabList label="Range">		<Tab value="day" label="Day" />		<Tab value="week" label="Week" />		<Tab value="month" label="Month" />	</TabList>	<TabPanel value={active} class="pt-4">		<p class="text-body-md text-muted-foreground">Showing the last {active}.</p>	</TabPanel></Tabs>

4. Scrollable

scrollable is the answer to a set too wide for the screen: one line that swipes, rather than tabs squeezed until their labels truncate.

Deliveries in Baghdad.

<script lang="ts">	import { Tabs, TabList, Tab, TabPanel } from 'omaris';​	let active = $state('Baghdad');</script>​<Tabs bind:value={active} class="w-full max-w-lg">	<TabList label="Cities" scrollable>		<Tab value="Baghdad" label="Baghdad" />		<Tab value="Erbil" label="Erbil" />		<Tab value="Basra" label="Basra" />		<Tab value="Mosul" label="Mosul" />		<Tab value="Duhok" label="Duhok" />		<Tab value="Najaf" label="Najaf" />		<Tab value="Karbala" label="Karbala" />		<Tab value="Kirkuk" label="Kirkuk" />	</TabList>	<TabPanel value={active} class="pt-4">		<p class="text-body-md text-muted-foreground">Deliveries in {active}.</p>	</TabPanel></Tabs>

5. Swipeable

Swipeable panels, for a phone. Wrap them in TabPanels and set swipeable: the track follows the finger, a flick moves a panel however short it was, and the indicator slides along at the same fraction instead of catching up afterwards. Vertical scrolling is untouched.

Twelve unread. Drag this panel sideways.

<script lang="ts">	import { Tabs, TabList, Tab, TabPanel, TabPanels } from 'omaris';​	let value = $state('inbox');</script>​<div class="w-80 max-w-full rounded-shape-md border border-border p-2">	<Tabs swipeable fitted bind:value>		<TabList label="Mail">			<Tab value="inbox" label="Inbox" />			<Tab value="sent" label="Sent" />			<Tab value="drafts" label="Drafts" />		</TabList>​		<TabPanels>			<TabPanel value="inbox">				<p class="text-sm">Twelve unread. Drag this panel sideways.</p>			</TabPanel>			<TabPanel value="sent">				<p class="text-sm">Nothing sent today.</p>			</TabPanel>			<TabPanel value="drafts">				<p class="text-sm">Two drafts waiting.</p>			</TabPanel>		</TabPanels>	</Tabs></div>

6. Vertical

orientation="vertical" stacks the tabs down the start edge, beside the panels: a settings screen's sections. ↑ and ↓ move between them.

Name, photo and bio.

<script lang="ts">	import { Tabs, TabList, Tab, TabPanel, Text } from 'omaris';​	let section = $state('profile');</script>​<Tabs bind:value={section} orientation="vertical" class="w-full max-w-lg">	<TabList label="Settings">		<Tab value="profile" label="Profile" />		<Tab value="notifications" label="Notifications" />		<Tab value="security" label="Security" />	</TabList>	<TabPanel value="profile"><Text>Name, photo and bio.</Text></TabPanel>	<TabPanel value="notifications"><Text>What reaches your phone, and when.</Text></TabPanel>	<TabPanel value="security"><Text>Passkeys and signed-in devices.</Text></TabPanel></Tabs>