I9kNavMenu
Mobile navigation: a bars toggle that opens a full-screen panel with the brand, the link list, and a footer slot. Owns the modal behavior — teleport, scroll lock, focus trap, Escape, and closing itself when the viewport passes the desktop breakpoint.
ts
import { I9kNavMenu } from '@9klabs/design';Examples
Panel composition
vue
<I9kNavMenu
preview
:links="[
{ id: 'blog', label: 'Blog', href: '/blog' },
{ id: 'projects', label: 'Projects', href: '/projects' },
{ id: 'talks', label: 'Talks', href: '/talks' },
]"
>
<template #brand><I9kBrandWordmark /></template>
<template #footer>
<I9kButton variant="primary" href="/contact">Work with me</I9kButton>
</template>
</I9kNavMenu>Toggle and panel
vue
<I9kNavMenu :links="links" open-label="Open menu" close-label="Close menu">
<template #brand><I9kBrandWordmark /></template>
</I9kNavMenu>Watch out
- The component renders at every width — it has no breakpoint of its own. Hide it above the breakpoint from the caller, and keep
desktopQueryone pixel above that width so a resize closes an open panel. - The panel is teleported to
<body>, so no CSS scoped to the header that mounts this component can reach it; style the panel through thebrandandfooterslots instead. - Without
linkComponentthe menu renders plain<a href>, so in a router app every click is a full page load — pass the router link component (RouterLink, NuxtLink) as an imported component, not a string name. - The panel closes itself on a link click, but it cannot see a navigation started from
footerslot content (a language switcher, a call to action) or from the browser back button — bindv-model:openand close it from the consumer for those. previewis for specimen pages only: it strips the teleport, the focus trap, the scroll lock, and both buttons, leaving a bounded box that documents the composition.
API
Props
| Prop | Type | Default | Required |
|---|---|---|---|
links | I9kNavigationLink[] | — | yes |
open | boolean | false | no |
compact | boolean | false | no |
linkComponent | string | object | null | null | no |
preview | boolean | false | no |
openLabel | string | 'Open menu' | no |
closeLabel | string | 'Close menu' | no |
menuLabel | string | 'Site menu' | no |
desktopQuery | string | '(min-width: 769px)' | no |
Emits
| Event | Payload |
|---|---|
update:open | [open: boolean] |
navigate | [link: I9kNavigationLink, event: MouseEvent] |
Slots
brandfooter
Agent prompt
Paste this into an agent to have it use the component correctly. It is self-contained.
text
Use I9kNavMenu from @9klabs/design for the small-screen counterpart to I9kNavigation, whose own menu hides at 768px.
import { I9kNavMenu } from '@9klabs/design';
Props:
- links: I9kNavigationLink[] (required) — the same { id: string; label: string; href: string } shape I9kNavigation takes, so both can be fed from one array. The type is not exported from the package; inline the object shape or declare your own local type.
- open?: boolean (default false) — optional external control. The panel drives itself, so this can be left unbound; bind it with v-model:open when something outside the panel has to close it, such as a router navigation the panel cannot see.
- compact?: boolean (default false) — forwarded to the `brand` slot, so the wordmark in the panel head can match whatever the header was showing when the toggle was tapped.
- linkComponent?: string | object | null (default null) — render the menu links with this component instead of a plain <a>, receiving the destination as `to` rather than `href`. Pass an imported component (RouterLink, NuxtLink), not its name as a string: a string is resolved through resolveDynamicComponent, which does not see locally or compile-time registered components.
- openLabel?: string (default 'Open menu') / closeLabel?: string (default 'Close menu') — accessible names for the toggle and the close button. closeLabel is also the toggle's name while the panel is open.
- menuLabel?: string (default 'Site menu') — accessible name for the panel dialog.
- desktopQuery?: string (default '(min-width: 769px)') — media query that closes the panel when the viewport grows past the breakpoint. Keep it one pixel above the width at which the caller stops showing this component.
- preview?: boolean (default false) — documentation mode. Renders the panel open and in normal flow: no teleport, no fixed positioning, no scroll lock, no focus trap, no toggle, no close button. For specimen pages only; never use it in a real header.
Emits:
- navigate — [link: I9kNavigationLink, event: MouseEvent], fired when a menu link is clicked, just before the panel closes itself.
- update:open — [open: boolean], fired on every open and close, including Escape and the desktop-breakpoint close.
Slots:
- brand — the logo/wordmark in the panel head. Receives one slot prop: compact: boolean, mirroring the compact prop.
- footer — the block pinned to the bottom of the panel, above the safe-area padding: theme and language controls, a primary call to action. Rendered in a grid; supply the rows.
Active links: with a router link component the panel styles the active route itself, from the `router-link-active` class and `aria-current="page"` that vue-router sets — the active link takes the primary color and reveals its trailing caret.
IMPORTANT: this component only renders the toggle and the panel — it has no breakpoint of its own. Hide it above the breakpoint from the caller, the way I9kNavigation hides its own menu below it.
Usage:
<I9kNavMenu :links="links" :link-component="NuxtLink" :compact="compact" @navigate="onNavigate">
<template #brand="{ compact }"><I9kBrandWordmark :compact="compact" /></template>
<template #footer><I9kButton variant="primary" href="/contact">Work with me</I9kButton></template>
</I9kNavMenu>