Fylgja Navbar
A navbar is the header at the top of a site, with the brand on one side and the
main navigation links on the other. On small screens the links move into a drawer
behind a menu button. The drawer is the native <dialog> from the
Offcanvas, opened with an invoker command, so the mobile
menu traps focus, closes on Escape, and returns focus to the button, all without
JavaScript.
Preview
The links show inline from the md breakpoint up. Make the window narrow to see the
menu button and open the drawer.
<header class="flex align gap" style="--py: var(--size-3)">
<a href="/" class="lead" style="text-decoration: none">Acme</a>
<nav aria-label="Main" class="hidden md:block">
<ul class="flex gap-sm" style="--btn-stroke: transparent">
<li><a href="#" class="btn" aria-current="page">Home</a></li>
<li><a href="#" class="btn">Products</a></li>
<li><a href="#" class="btn">Pricing</a></li>
<li><a href="#" class="btn">About</a></li>
</ul>
</nav>
<button type="button" class="md:hidden" commandfor="navbar-menu" command="show-modal" aria-label="Open menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/></svg>
</button>
<dialog id="navbar-menu" class="offcanvas open:flex flex-col clip" closedby="any" aria-labelledby="navbar-menu-title" style="--p: 0">
<div class="flex align gap flow-none" style="--py: var(--size-6); --px: var(--size-6)">
<h2 id="navbar-menu-title">Menu</h2>
<button type="button" commandfor="navbar-menu" command="close" aria-label="Close menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6 18 18"/><path d="M18 6 6 18"/></svg>
</button>
</div>
<nav aria-label="Main" class="flex-auto scroll-y scroll-mask" style="--py: var(--size-2) var(--size-6); --px: var(--size-2)">
<ul style="--btn-stroke: transparent">
<li><a href="#" class="btn w-full align" aria-current="page">Home</a></li>
<li><a href="#" class="btn w-full align">Products</a></li>
<li><a href="#" class="btn w-full align">Pricing</a></li>
<li><a href="#" class="btn w-full align">About</a></li>
</ul>
</nav>
</dialog>
</header>Usage
- Wrap the bar in a
<header>withflex align gap, which keeps the brand at the start and pushes the navigation to the end. - Put the inline links in a
<nav aria-label="Main">with a<ul>, each link an<a class="btn">. Hide this nav on small screens withhidden md:block. - Mark the current page
aria-current="page". Fylgja’s button active style highlights it, so no extra class is needed. - Add a menu
<button>withmd:hidden,commandfor, andcommand="show-modal"that opens a<dialog class="offcanvas">holding the same links. - Give the drawer
closedby="any"and a close button, so Escape, a backdrop click, and the button all dismiss it.
Anatomy
| Part | Responsibility |
|---|---|
<header> |
The bar. Lays out the brand, links, and menu button. |
Brand <a href="/"> |
Links home. Text or a logo with an accessible name. |
<nav aria-label="Main"> |
The inline links, shown from the md breakpoint up. |
Menu <button command> |
Opens the drawer on small screens, hidden from md up. |
<dialog class="offcanvas"> |
The drawer with the same links, in the top layer. |
Accessibility
- Both navs are labeled “Main”, and only one is exposed at a time, so a screen reader always finds one main navigation landmark.
aria-current="page"marks the current page in both lists.- The icon only menu and close buttons need an
aria-label. - Opened as a modal, the drawer traps focus and makes the page inert, and focus returns to the menu button when it closes.
Variants
Always collapsed
For a site with many sections, or a header that is short on room, skip the inline links and always use the menu button and drawer.
<header class="flex align gap" style="--py: var(--size-3)">
<a href="/" class="lead" style="text-decoration: none">Acme</a>
<button type="button" commandfor="navbar-drawer" command="show-modal" aria-label="Open menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/></svg>
</button>
<dialog id="navbar-drawer" class="offcanvas open:flex flex-col clip" closedby="any" aria-labelledby="navbar-drawer-title" style="--p: 0">
<div class="flex align gap flow-none" style="--py: var(--size-6); --px: var(--size-6)">
<h2 id="navbar-drawer-title">Menu</h2>
<button type="button" commandfor="navbar-drawer" command="close" aria-label="Close menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6 18 18"/><path d="M18 6 6 18"/></svg>
</button>
</div>
<nav aria-label="Main" class="flex-auto scroll-y scroll-mask" style="--py: var(--size-2) var(--size-6); --px: var(--size-2)">
<ul style="--btn-stroke: transparent">
<li><a href="#" class="btn w-full align" aria-current="page">Home</a></li>
<li><a href="#" class="btn w-full align">Products</a></li>
<li><a href="#" class="btn w-full align">Pricing</a></li>
<li><a href="#" class="btn w-full align">About</a></li>
</ul>
</nav>
</dialog>
</header>Related: Offcanvas, Classic Footer, Breadcrumbs.
FAQ
Why are the links in the markup twice?
The inline links and the drawer links live in different places, one in the header and one in a <dialog> in the top layer, so they are two lists. Only one is ever exposed at a time, since the inline nav is hidden on small screens and the drawer is closed on large ones. In a template or component framework you render both from the same data, so there is still one source of truth.
Do I need JavaScript for the mobile menu?
No. The menu button opens the drawer with an invoker command, and the native <dialog> handles focus, the backdrop, Escape, and closing on a backdrop click through closedby="any".
How do I change the breakpoint?
Swap the md prefix on both md:block and md:hidden for another breakpoint prefix, such as lg, so the links appear on larger screens only. Keep the two prefixes the same, or there will be a width where both or neither show.
Where does the .offcanvas class come from?
It is the small rule from the Offcanvas component, which pins the dialog to the screen edge. Ship that rule with the navbar, rather than relying on Fylgja's theme.css being loaded.
How do I make the header stick to the top?
Add the sticky class to the <header>, and give it a background color so content does not show through while scrolling.