Navbar blocks

Top bars for a site or an app: plain links with a call to action, a storefront bar with search and a cart, and a bar with dropdown and mega menus. Each block is one .svelte file built from SvButton, SvMenu, SvBadge and SvTextInput, themed from the --sg-* tokens, and drawn over a short mock page so you can see it in place.

Every navbar is a <header> with a labelled <nav>, the current link carries aria-current="page", and the phone menu button has aria-expanded and aria-controls and closes on Escape or a link click.

Installation

Add the whole group, or one block by its name (shown under each demo):

See them first with npx @svgrid/ui try blocks/navbars. No project needed.

Simple navbar

Brand, four links and a Start free trial button. The mock page heading follows the current link. Below 768 px the links and the button fold behind a menu button that opens an in-flow panel under the bar, so the page moves down instead of being covered.

Open the live example: Simple navbar block (Blocks)

npx @svgrid/ui add navbars-simple

A storefront header: brand, a search field, an account menu and a cart button with a count, over a row of category links. Submitting the search shows the query in a toast; Add to cart on a product tile raises the count. On phones the search takes its own row under the bar and the categories move behind a menu button.

Open the live example: Navbar with search block (Blocks)

npx @svgrid/ui add navbars-search

Products opens a mega menu with two columns of icon, title and description links; Resources opens an SvMenu; Pricing is a plain link. Enter, Space or ArrowDown opens the mega menu on its first link, ArrowUp and ArrowDown move between links, and Escape closes it and puts focus back on Products. A click outside or tabbing out also closes it. Below 768 px every link is listed in a grouped phone menu.

Open the live example: Navbar with dropdown menus block (Blocks)

npx @svgrid/ui add navbars-dropdowns

Using a navbar in a SvelteKit app

A navbar belongs in the root layout, so it renders once and stays mounted while the pages under it change. Copy the block, delete the mock <main> below the <header>, and render it above the page content:

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import NavbarsSimple from '$lib/components/ui/navbars-simple.svelte'

  let { children } = $props()
</script>

<NavbarsSimple />
{@render children()}

Inside the block, the demo stops each link click with onNavigate: (_href, e) => e.preventDefault() so the gallery stays put. Remove that line and the links navigate through the SvelteKit router. Then drive the active link from the URL instead of from clicks, and close the phone menu after every navigation, including back and forward and goto() calls:

<script lang="ts">
  import { page } from '$app/state'
  import { afterNavigate } from '$app/navigation'

  const nav = createSimpleNavbar({
    links: [
      { label: 'Product', href: '/product' },
      { label: 'Pricing', href: '/pricing' },
    ],
  })

  // aria-current follows the URL, so a deep link or a redirect marks the right item.
  $effect(() => { nav.current = page.url.pathname })

  // Close the phone menu once the new page is in.
  afterNavigate(() => { nav.menuOpen = false })
</script>

The other blocks work the same way: each core has a writable current (the search block stores a category name, so map the path to one), and the menu state closes through menuOpen (simple), escape() (search) or closeMenu() and closeMega() (dropdowns). For nested routes, compare with page.url.pathname.startsWith(href) in your own linkProps if a section link should stay marked on its child pages.

Headless

Every block here is a styled render of the core in its <script module>: createSimpleNavbar, createSearchNavbar and createDropdownNavbar. To draw your own markup, take the core alone with npx @svgrid/ui add navbars-simple --headless (it writes navbars-simple.svelte.ts) and spread its prop-getters on plain elements:

<script lang="ts">
  import { createSimpleNavbar } from './navbars-simple.svelte.js'

  const nav = createSimpleNavbar()
  let menuBtn = $state<HTMLButtonElement | null>(null)
</script>

<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && nav.escape()) menuBtn?.focus() }} />

<header>
  <button bind:this={menuBtn} {...nav.menuButtonProps()}>Menu</button>
  <nav aria-label="Main" {...nav.panelProps()}>
    {#each nav.links as l (l.href)}<a {...nav.linkProps(l.href)}>{l.label}</a>{/each}
  </nav>
</header>

See also

Live examples

  • Simple navbar block - Brand, four links with aria-current and a Start free trial SvButton over a mock page whose heading follows the current link. Below 768 px a menu button opens an in-flow panel that Escape or a link click closes.
  • Navbar with search block - Storefront header with an SvTextInput search that toasts the query, an account SvMenu and a cart button with an SvBadge count that Add to cart raises. On phones the search moves under the bar.
  • Navbar with dropdown menus block - Products opens a two-column mega menu of icon, title and description links; Resources opens an SvMenu. Enter, Space or ArrowDown opens, arrow keys move between links, and Escape closes and returns focus.