Skip to content
Navigation

Navigation Bar

MD3 navigation bar — the bottom bar on a phone, 3–5 destinations.

Walkthrough

1. Basic

Three to five destinations, on a phone. More than five wants a drawer.

<script lang="ts">	import { NavigationBar, NavigationBarItem } from 'omaris';​	let tab = $state('home');</script>​{#snippet house()}	...{/snippet}​{#snippet bag()}	...{/snippet}​{#snippet person()}	...{/snippet}​<div class="w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<NavigationBar label="Sections">		<NavigationBarItem label="Home" selected={tab === 'home'} onclick={() => (tab = 'home')}>			{#snippet icon()}{@render house()}{/snippet}		</NavigationBarItem>		<NavigationBarItem			label="Orders"			badge={3}			selected={tab === 'orders'}			onclick={() => (tab = 'orders')}		>			{#snippet icon()}{@render bag()}{/snippet}		</NavigationBarItem>		<NavigationBarItem			label="Profile"			selected={tab === 'profile'}			onclick={() => (tab = 'profile')}		>			{#snippet icon()}{@render person()}{/snippet}		</NavigationBarItem>	</NavigationBar></div>

2. Label modes

selected shows the label only under the current item, which MD3 allows — for a bar of five where the labels would crowd each other.

<script lang="ts">	import { NavigationBar, NavigationBarItem } from 'omaris';​	let tab = $state('a');</script>​{#snippet dot()}	...{/snippet}​<div class="flex w-full max-w-sm flex-col gap-4">	<div class="overflow-hidden rounded-shape-lg border border-border">		<NavigationBar labels="always" label="labels = always">			<NavigationBarItem label="Home" selected={tab === 'a'} onclick={() => (tab = 'a')}>				{#snippet icon()}{@render dot()}{/snippet}			</NavigationBarItem>			<NavigationBarItem label="Search" selected={tab === 'b'} onclick={() => (tab = 'b')}>				{#snippet icon()}{@render dot()}{/snippet}			</NavigationBarItem>			<NavigationBarItem label="Saved" selected={tab === 'c'} onclick={() => (tab = 'c')}>				{#snippet icon()}{@render dot()}{/snippet}			</NavigationBarItem>		</NavigationBar>	</div>​	<div class="overflow-hidden rounded-shape-lg border border-border">		<NavigationBar labels="selected" label="labels = selected">			<NavigationBarItem label="Home" selected={tab === 'a'} onclick={() => (tab = 'a')}>				{#snippet icon()}{@render dot()}{/snippet}			</NavigationBarItem>			<NavigationBarItem label="Search" selected={tab === 'b'} onclick={() => (tab = 'b')}>				{#snippet icon()}{@render dot()}{/snippet}			</NavigationBarItem>			<NavigationBarItem label="Saved" selected={tab === 'c'} onclick={() => (tab = 'c')}>				{#snippet icon()}{@render dot()}{/snippet}			</NavigationBarItem>		</NavigationBar>	</div></div>

3. Swipeable screens

Wrap the bar and its screens in Navigation, set swipeable, and the destination is one bindable value: drag a screen sideways and the next one follows the finger while the active pill slides along at the same fraction. A flick moves a screen however short it was, and the ends push back. Try it with a finger — mouse is only on here so a desktop can feel it too.

Home

<script lang="ts">	import {		Navigation,		NavigationBar,		NavigationBarItem,		NavigationView,		NavigationViews,		Text	} from 'omaris';​	let screen = $state('home');</script>​{#snippet house()}	...{/snippet}​{#snippet bag()}	...{/snippet}​{#snippet person()}	...{/snippet}​{#snippet screenBody(title: string, lines: number)}	<div class="flex flex-col gap-3 p-4">		<Text variant="title-md" as="h3">{title}</Text>		{#each { length: lines }, index (index)}			<div class="h-12 rounded-shape-md bg-surface-container-high"></div>		{/each}	</div>{/snippet}​<div class="w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<Navigation swipeable bind:value={screen} class="h-96">		<NavigationViews mouse>			<NavigationView value="home">{@render screenBody('Home', 4)}</NavigationView>			<NavigationView value="orders">{@render screenBody('Orders', 6)}</NavigationView>			<NavigationView value="profile">{@render screenBody('Profile', 3)}</NavigationView>		</NavigationViews>​		<NavigationBar label="Main" elevated>			<NavigationBarItem value="home" label="Home" icon={house} />			<NavigationBarItem value="orders" label="Orders" badge={3} icon={bag} />			<NavigationBarItem value="profile" label="Profile" icon={person} />		</NavigationBar>	</Navigation></div>

4. Right to left

The same screens under dir="rtl". Everything mirrors together: the destinations run right to left, the screens are stacked the same way, and the finger drags them the way the writing runs — a drag to the right moves forward, and the pill slides along under it. Nothing in the markup changes; the direction is read off the layout.

الرئيسية

<script lang="ts">	import {		Navigation,		NavigationBar,		NavigationBarItem,		NavigationView,		NavigationViews,		Text	} from 'omaris';​	let screen = $state('home');</script>​{#snippet house()}	...{/snippet}​{#snippet bag()}	...{/snippet}​{#snippet person()}	...{/snippet}​{#snippet screenBody(title: string, lines: number)}	<div class="flex flex-col gap-3 p-4">		<Text variant="title-md" as="h3">{title}</Text>		{#each { length: lines }, index (index)}			<div class="h-12 rounded-shape-md bg-surface-container-high"></div>		{/each}	</div>{/snippet}​<div dir="rtl" class="w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<Navigation swipeable bind:value={screen} class="h-96">		<NavigationViews mouse>			<NavigationView value="home">{@render screenBody('الرئيسية', 4)}</NavigationView>			<NavigationView value="orders">{@render screenBody('الطلبات', 6)}</NavigationView>			<NavigationView value="profile">{@render screenBody('الحساب', 3)}</NavigationView>		</NavigationViews>​		<NavigationBar label="التنقل" elevated>			<NavigationBarItem value="home" label="الرئيسية" icon={house} />			<NavigationBarItem value="orders" label="الطلبات" badge={3} icon={bag} />			<NavigationBarItem value="profile" label="الحساب" icon={person} />		</NavigationBar>	</Navigation></div>

5. Overridden

The same set with the defaults taken apart: labels="selected" so the label rides the swipe in and out, lazy so a screen is built the first time it is reached, a tighter threshold so a shorter drag counts, and classes on the pill and the label. fill={false} sizes the viewport by its content instead of filling the column.

<script lang="ts">	import {		Navigation,		NavigationBar,		NavigationBarItem,		NavigationView,		NavigationViews,		Text	} from 'omaris';​	let screen = $state('two');</script>​{#snippet dot()}	...{/snippet}​<div class="w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<Navigation swipeable bind:value={screen}>		<NavigationViews mouse lazy fill={false} threshold={0.15} class="h-40">			<NavigationView value="one" class="p-4">				<Text variant="body-md">First. Drag left.</Text>			</NavigationView>			<NavigationView value="two" class="p-4">				<Text variant="body-md">Second — built on the way past.</Text>			</NavigationView>			<NavigationView value="three" class="p-4">				<Text variant="body-md">Third, and the end pushes back.</Text>			</NavigationView>		</NavigationViews>​		<NavigationBar label="Sections" labels="selected" size="sm">			<NavigationBarItem				value="one"				label="One"				icon={dot}				classes={{ indicator: 'rounded-shape-sm', label: 'uppercase tracking-wide' }}			/>			<NavigationBarItem				value="two"				label="Two"				icon={dot}				classes={{ indicator: 'rounded-shape-sm', label: 'uppercase tracking-wide' }}			/>			<NavigationBarItem				value="three"				label="Three"				icon={dot}				classes={{ indicator: 'rounded-shape-sm', label: 'uppercase tracking-wide' }}			/>		</NavigationBar>	</Navigation></div>