Skip to content
omaris

Navigation

Navigation Drawer

MD3 navigation drawer — the sidebar of a dashboard, in all three of the shapes a dashboard needs it in.

import { NavigationDrawer } from 'omaris'
Learn

Examples

Standard

Sections group items and badge rides on the end of a row — the sidebar of a dashboard with more pages than one flat list can hold.

<script lang="ts">	import { NavigationDrawer, NavigationDrawerItem, NavigationDrawerSection } from 'omaris';​	let selected = $state('inbox');</script>​<div class="h-80 w-72 max-w-full overflow-hidden rounded-shape-lg border border-border">	<NavigationDrawer label="Mail">		<NavigationDrawerSection label="Mail">			<NavigationDrawerItem				label="Inbox"				badge={12}				selected={selected === 'inbox'}				onclick={() => (selected = 'inbox')}			/>			<NavigationDrawerItem				label="Sent"				selected={selected === 'sent'}				onclick={() => (selected = 'sent')}			/>		</NavigationDrawerSection>​		<NavigationDrawerSection label="Commerce" divider>			<NavigationDrawerItem label="Orders" badge="New">				{#snippet submenu()}					<NavigationDrawerItem label="Open" badge={4} />					<NavigationDrawerItem label="Shipped" />				{/snippet}			</NavigationDrawerItem>		</NavigationDrawerSection>	</NavigationDrawer></div>

Collapsed rail

Collapsed, it becomes MD3's rail: icons only, labels as tooltips. What a laptop wants once the content needs the width more than the labels.

<script lang="ts">	import { Button, NavigationDrawer, NavigationDrawerItem } from 'omaris';​	let collapsed = $state(true);</script>​{#snippet overview()}	...{/snippet}​{#snippet orders()}	...{/snippet}​{#snippet team()}	...{/snippet}​<div class="flex w-full flex-col items-start gap-3">	<Button size="xs" variant="outlined" onclick={() => (collapsed = !collapsed)}>		{collapsed ? 'Expand' : 'Collapse'}	</Button>​	<div class="h-64 overflow-hidden rounded-shape-lg border border-border">		<NavigationDrawer bind:collapsed label="Sections">			<NavigationDrawerItem label="Overview" selected>				{#snippet icon()}{@render overview()}{/snippet}			</NavigationDrawerItem>			<NavigationDrawerItem label="Orders">				{#snippet icon()}{@render orders()}{/snippet}			</NavigationDrawerItem>			<NavigationDrawerItem label="Team">				{#snippet icon()}{@render team()}{/snippet}			</NavigationDrawerItem>		</NavigationDrawer>	</div></div>

Modal

mode="modal" slides it over the page on a scrim. In an app, responsive is the one to reach for: a sidebar above the breakpoint, this modal below it, so you write one drawer instead of two.

<script lang="ts">	import { Button, NavigationDrawer, NavigationDrawerItem } from 'omaris';​	let open = $state(false);</script>​<Button variant="tonal" onclick={() => (open = true)}>Open the drawer</Button>​<NavigationDrawer bind:open mode="modal" label="Sections">	{#snippet header()}		<span class="px-2 py-1 text-title-md">omaris</span>	{/snippet}	<NavigationDrawerItem label="Overview" selected />	<NavigationDrawerItem label="Orders" badge={12} />	<NavigationDrawerItem label="Settings" /></NavigationDrawer>

When to use it

Use it for

  • The sidebar of a dashboard, inside an AppShellSidebar: sections, nested groups via submenu, a logo in header, the account row in footer.
  • The rail. collapsed narrows it to icons with labels as tooltips. There is no NavigationRail component; this is it. It is bindable, so a button in the app bar can toggle it.
  • A phone or tablet. mode="modal" slides it over the page on a scrim; responsive picks between the two at breakpoint, so one drawer serves every screen.
  • More destinations than a bar can hold, or destinations that nest.

Not for

  • Three to five destinations on a phone → Navigation Bar.
  • Views within one page → Tabs. The drawer maps the app, not a page.
  • A filter panel, an inspector or a form sliding in from an edge → Sheet.
  • A list of actions off a button → Menu.
  • Where the current page sits in a hierarchy → Breadcrumb.

Do

  • Pair responsive with breakpoint={false} on the AppShellSidebar, or the shell drops the column before the drawer can go modal.
  • Add bordered when the drawer sits on the same colour as the page. It draws no border on its own.
  • Give every item an icon. Collapsed, a row without one is an empty pill.
  • When modal, bind open to a menu button in the leading slot of the Top App Bar and leave closeOnNavigate on.
  • Put "Sign out" and the account in footer, not among the destinations.

Don't

  • Leave a phone with nothing. AppShellSidebar hides below md; without responsive here or a Navigation Bar, the phone has no navigation.
  • Nest deeper than one submenu. A second level means the section wants a page of its own with Tabs.
  • Turn on resizable for the look. It is for labels that do not fit at 280. Set minWidth so the rail cannot be dragged into.
  • Rebuild the modal drawer from a Sheet. mode="modal" already has the scrim, the drag and close-on-navigate.

API

NavigationDrawer

MD3 navigation drawer — the sidebar of a dashboard, in all three of the shapes a dashboard needs it in.

- standard is the always-there sidebar of a desktop layout. - collapsed narrows it to its icons, which is what most dashboards actually want on a laptop. Every row keeps its label as a tooltip, so nothing is lost by narrowing it. - modal is the temporary drawer of a phone or a tablet: it slides over the page on a scrim, closes on Escape, on a press outside, on choosing a destination — and follows a finger dragged toward the edge, springing back if the drag was half-hearted.

responsive picks between the last two on the viewport, so one drawer is right on every screen without the layout knowing which it got.

resizable lets the sidebar be dragged to a width that suits the content — the edge is a real separator, so the arrow keys move it too.

import { NavigationDrawer } from 'omaris'

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
body
Scrolls; the header and footer don't.
header
No description in the source yet.
footer
No description in the source yet.
dialog
The modal shell: a dialog on a scrim, sliding in from the edge.
grip
The drag target on the trailing edge of a resizable drawer.

Props

collapsed bindable

Defaults to false

boolean

Narrow the drawer to its icons. Bindable.

bordered

Defaults to false

boolean
label
string

Accessible name.

mode

Defaults to 'standard'

NavigationDrawerMode

Beside the content, or over it. responsive overrides this.

open bindable

Defaults to false

boolean

Whether a modal drawer is showing. Bindable.

responsive

Defaults to false

boolean

Be a modal drawer on a narrow viewport and a standard one on a wide one. true switches at breakpoint.

breakpoint

Defaults to 900

number

Viewport width, in px, at or above which the drawer stays standard.

closeOnNavigate

Defaults to true

boolean

Close a modal drawer when a destination inside it is chosen.

resizable

Defaults to false

boolean

Let the trailing edge be dragged to set the width.

width bindable

Defaults to 280

number

Width in px while expanded. Bindable; only used when resizable.

minWidth

Defaults to 220

number
maxWidth

Defaults to 420

number
onclose
() => void
class
string
classes
NavigationDrawerClasses

Per-part Tailwind overrides. class still covers the root.

header
Snippet

Pinned to the top — a logo, a compose button.

footer
Snippet

Pinned to the bottom — the account row, usually.

children
Snippet

NavigationDrawerItem

A destination in a NavigationDrawer.

MD3's drawer item is a 56dp pill: the whole row is the active indicator, unlike the bar item where only the icon gets one. When the drawer collapses, the row becomes a centred icon — the label survives both as its accessible name and as a tooltip, so a narrowed sidebar is still readable rather than a column of guesses.

Give it a submenu and it becomes a group: the row grows a chevron and the nested destinations open under it against a guide line, which is how a dashboard's navigation actually nests.

The row collapses; it is not rebuilt. Every element here is in the DOM in both states — the label and the trailing content included — and the narrowing is a transition on the properties that differ. Nothing is added, removed or reparented on the way, which is what lets the row travel with the sidebar's own width instead of snapping to the new shape on the first frame and leaving the panel to catch up.

import { NavigationDrawerItem } from 'omaris'

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
tail
Everything after the icon, collapsed as one piece.
label
No description in the source yet.
trailing
Trailing count or badge.
chevron
The chevron on a group row.
submenu
Animates the nested rows open without anyone measuring them.
nest
The guide line the nested rows hang off.

Props

label
string

Row text — and the accessible name once the drawer collapses.

selected

Defaults to false

boolean
badge
number | string

Trailing count.

ripple

Defaults to true

boolean
expanded bindable

Defaults to false

boolean

Whether the nested rows are showing. Bindable.

class
string
classes
NavigationDrawerItemClasses

Per-part Tailwind overrides. class still covers the root.

icon
Snippet
selectedIcon
Snippet

Icon shown only while selected.

trailing
Snippet

Trailing slot, in place of badge.

submenu
Snippet

Nested destinations. Their presence turns the row into a group.

children
Snippet
variant
ButtonVariant

Fill style. MD3's five, plus link for inline navigation.

tone
ButtonTone

Color role the fill style paints with.

size
ButtonSize

MD3 Expressive heights: 32 / 40 / 56 / 96 / 136dp.

shape
ButtonShape

Pill (round) or MD3 rounded rectangle (square).

confetti
boolean | ConfettiOptions

Confetti on press. true for the default burst, or any ConfettiOptions — confetti={{ preset: 'fireworks' }}.

loading
boolean

Grows a spinner in at the start of the button and blocks interaction. The label stays put; only an icon button swaps its icon out.

toggle
boolean

Turns the button into a two-state toggle driven by pressed. Ignored when href is set — a link has no pressed state.

pressed bindable
boolean

Selected state of a toggle button. Bindable.

NavigationDrawerSection

A labelled group of items inside a NavigationDrawer.

The label folds away when the drawer is collapsed to a rail, so a rail does not show headings with nothing under them — folds rather than disappears, on the drawer's own curve and duration, and it stays in the accessibility tree the whole time so the group keeps its name.

import { NavigationDrawerSection } from 'omaris'

Props

label
string

Group heading. Hidden (but still announced) when collapsed.

divider

Defaults to false

boolean

Hairline above the group.

class
string
children
Snippet