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> with flex 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 with hidden 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> with md:hidden, commandfor, and command="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.