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>