Navigation drawer
A navigation drawer is the top level navigation for wide screens, with room for labels, groups and
counts. There are two kinds. Without modal it is a standard drawer: part of the
layout, always visible next to the content. With modal it is a popover that slides in
over the page and can be dismissed. Use it on narrow screens. It moves like the modal navigation
rail: it springs open on --np-motion-expressive-default-spatial and slides back on --np-motion-standard-fast-spatial.
The examples below show both. The first is modal and opens from a button, the second is a standard drawer with its height capped so it fits on this page.
Usage
<script lang="ts">
import { Button, NavigationDrawer, NavigationDrawerItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
let selection = $state(1)
</script>
<Button command="show-modal" commandfor="demo-drawer">Open nav</Button>
<NavigationDrawer id="demo-drawer" modal>
<NavigationDrawerItem
selected={selection === 1}
onclick={() => {
selection = 1
}}
label="Videos"
badgeLabel="+100"
>
{#snippet icon()}<Icon>videocam</Icon>{/snippet}
</NavigationDrawerItem>
<NavigationDrawerItem
selected={selection === 2}
onclick={() => {
selection = 2
}}
label="Styles"
>
{#snippet icon()}<Icon>palette</Icon>{/snippet}
</NavigationDrawerItem>
<NavigationDrawerItem
selected={selection === 3}
onclick={() => {
selection = 3
}}
label="Settings"
>
{#snippet icon()}<Icon>settings</Icon>{/snippet}
</NavigationDrawerItem>
</NavigationDrawer>
<script lang="ts">
import { NavigationDrawer, NavigationDrawerItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
let selection = $state(1)
</script>
<NavigationDrawer --np-navigation-drawer-height="200px">
<NavigationDrawerItem
selected={selection === 1}
onclick={() => {
selection = 1
}}
label="Videos"
badgeLabel="+100"
>
{#snippet icon()}<Icon>videocam</Icon>{/snippet}
</NavigationDrawerItem>
<NavigationDrawerItem
selected={selection === 2}
onclick={() => {
selection = 2
}}
label="Styles"
>
{#snippet icon()}<Icon>palette</Icon>{/snippet}
</NavigationDrawerItem>
<NavigationDrawerItem
selected={selection === 3}
onclick={() => {
selection = 3
}}
label="Settings"
>
{#snippet icon()}<Icon>settings</Icon>{/snippet}
</NavigationDrawerItem>
</NavigationDrawer>
Opening a modal drawer
A modal drawer is a native <dialog>. To open it, set command="show-modal" and commandfor on the trigger, as in the example above.
To open or close it from code, bind the element and call the popover methods on it.
<script lang="ts">
let drawer = $state<HTMLElement>()
</script>
<Button onclick={() => drawer?.show()}>Menu</Button>
<NavigationDrawer bind:element={drawer} modal>
<NavigationDrawerItem label="Videos" onclick={() => drawer?.close()} />
</NavigationDrawer>A scrim dims the page behind the drawer, and a click on it closes the drawer. backdrop=false leaves the page undimmed. Set direction="rtl" to make the drawer slide in from the other edge. Use this when it sits
at the end of the layout. In a right-to-left page it slides in from the right by default.
Dismissible standard drawer
A standard drawer is always visible until you set open. Then it can be dismissed: open=false slides it out to the start and gives its width back to the content. true brings it back. Toggle it from a menu button that stays visible, as Material 3 asks.
The content takes the space the drawer gives back.
<script lang="ts">
import { IconButton, NavigationDrawer, NavigationDrawerItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const destinations = [
{ value: 'videos', label: 'Videos', icon: 'videocam' },
{ value: 'styles', label: 'Styles', icon: 'palette' },
{ value: 'settings', label: 'Settings', icon: 'settings' },
]
let open = $state(true)
let selected = $state('videos')
</script>
<div style="display:flex;width:40rem;max-width:100%;height:16rem">
<NavigationDrawer {open} aria-label="Main, dismissible" --np-navigation-drawer-height="100%">
{#each destinations as destination (destination.value)}
<NavigationDrawerItem
label={destination.label}
selected={selected === destination.value}
onclick={() => (selected = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationDrawerItem>
{/each}
</NavigationDrawer>
<div style="flex:1;padding:0.5rem 1rem">
<IconButton aria-label="Navigation" aria-expanded={open} onclick={() => (open = !open)}>
<Icon>{open ? 'menu_open' : 'menu'}</Icon>
</IconButton>
<p>The content takes the space the drawer gives back.</p>
</div>
</div>
Headline and sections
headline puts a title above the destinations. It names the navigation unless you pass
an aria-label. NavigationDrawerSection groups related destinations under a
divider and a label. A screen reader announces the group by that label.
<script lang="ts">
import { NavigationDrawer, NavigationDrawerItem, NavigationDrawerSection } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const mail = [
{ value: 'inbox', label: 'Inbox', icon: 'mail', badgeLabel: 24 },
{ value: 'sent', label: 'Sent', icon: 'send' },
{ value: 'favorites', label: 'Favorites', icon: 'favorite' },
{ value: 'trash', label: 'Trash', icon: 'delete' },
]
const labels = [
{ value: 'work', label: 'Work', icon: 'work' },
{ value: 'saved', label: 'Saved', icon: 'bookmark' },
]
let selected = $state('inbox')
</script>
{#snippet item(destination: { value: string; label: string; icon: string; badgeLabel?: number })}
<NavigationDrawerItem
label={destination.label}
badgeLabel={destination.badgeLabel}
selected={selected === destination.value}
onclick={() => (selected = destination.value)}
>
{#snippet icon()}<Icon>{destination.icon}</Icon>{/snippet}
</NavigationDrawerItem>
{/snippet}
<NavigationDrawer headline="Mail" --np-navigation-drawer-height="34rem">
{#each mail as destination (destination.value)}
{@render item(destination)}
{/each}
<NavigationDrawerSection label="Labels">
{#each labels as destination (destination.value)}
{@render item(destination)}
{/each}
</NavigationDrawerSection>
</NavigationDrawer>
Accessibility
The drawer renders a <nav>, and the selected item gets aria-current="page". Give the <nav> an aria-label when the page has more than one navigation landmark.
The items share one tab stop. Tab moves into and out of the drawer. ↑ and ↓ move between the destinations and wrap at the ends. Home and End jump to the first and the last one.
While a modal drawer is open, the rest of the page is inert. This keeps the keyboard
and the screen reader cursor inside the drawer. Focus moves to the first item on open and back to
the trigger on close. Escape closes the drawer.
Theming
| Token | Default value |
|---|---|
--np-navigation-drawer-background | --np-color-surface-container-low |
--np-navigation-drawer-width | 22.5rem |
--np-navigation-drawer-height | 100dvh |
--np-navigation-drawer-padding | 0.75rem |
--np-navigation-drawer-item-container-shape | --np-shape-corner-full |
--np-navigation-drawer-item-font-size | 0.875rem |
--np-navigation-drawer-item-font-weight | 500 |
--np-navigation-drawer-item-selected-font-weight | 500 |
Use --np-navigation-drawer-height when the drawer does not fill the full viewport
height, as in the standard example above. The selected item uses secondary-container and on-secondary-container, the rest with on-surface-variant.
Example
<script lang="ts">
import { NavigationDrawer, NavigationDrawerItem } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<NavigationDrawer
--np-navigation-drawer-height="200px"
--np-navigation-drawer-width="16rem"
--np-navigation-drawer-background="var(--np-color-surface-container-highest)"
--np-navigation-drawer-item-container-shape="0.5rem"
>
<NavigationDrawerItem selected label="Videos">
{#snippet icon()}<Icon>videocam</Icon>{/snippet}
</NavigationDrawerItem>
<NavigationDrawerItem label="Styles">
{#snippet icon()}<Icon>palette</Icon>{/snippet}
</NavigationDrawerItem>
</NavigationDrawer>
API
NavigationDrawer attributes
All other attributes go to the root element, including id, class, style and ontoggle. The root is the <nav> for a
standard drawer and the <dialog> for a modal one. aria-label and aria-labelledby always go to the <nav>, so they name the
navigation landmark.
| Attribute | Type | Default | Description |
|---|---|---|---|
modal | boolean | false | Turns the drawer into a popover that slides in over the page and traps focus. |
backdrop | boolean | true | Dims the page behind a modal drawer. Clicking the scrim closes it. |
open | boolean | undefined | undefined | Bindable. Whether a modal drawer is open. On a standard drawer, left out means always visible, and a value makes it dismissible. |
headline | string | undefined | undefined | A title above the destinations, naming the navigation unless it has an aria-label. |
direction | 'ltr' | 'rtl' | undefined | undefined | Edge the drawer slides in from: ltr from the left, rtl from the right.
Left out, it follows the writing direction. |
element | HTMLElement | undefined | undefined | Bindable reference to the <nav>. Use it to call show() and close(). |
NavigationDrawerItem attributes
All other attributes go to the underlying <button> or <a>, including onclick and href.
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | Required. Text of the destination. | |
icon | Snippet | undefined | undefined | Leading icon. It is filled while the item is selected. |
selected | boolean | undefined | undefined | Marks the current destination and sets aria-current="page". |
badgeLabel | string | number | undefined | undefined | Trailing text, for a count such as "+100". |
href | string | undefined | undefined | Renders the item as a link instead of a button. |
type | 'submit' | 'reset' | 'button' | null | undefined | Button type, for items inside a form. |
NavigationDrawerSection attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | The section label, shown under the divider and naming the group. |