Navbar

Horizontal site navigation with a click-toggle dropdown by default and a <details>-driven mobile toggle. Zero JavaScript.

Live

Click-toggle dropdown (default)

<nav class="navbar" aria-label="Primary">
  <a class="navbar__brand" href="/">Krysalicss</a>
  <ul class="navbar__menu">
    <li class="navbar__item">
      <a class="navbar__link" href="/docs">Docs</a>
    </li>
    <li class="navbar__item has-dropdown">
      <input id="nav-more" type="checkbox" class="navbar__dropdown-toggle" hidden>
      <label for="nav-more" class="navbar__link">More ▾</label>
      <ul class="navbar__dropdown">
        <li><a class="navbar__link" href="/explanation">Explanation</a></li>
        <li><a class="navbar__link" href="/reference">Reference</a></li>
      </ul>
    </li>
  </ul>
</nav>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-navbar')
);
Playground

Hoverable modifier (hover + focus-within reveal)

<nav class="navbar" aria-label="Primary">
  <a class="navbar__brand" href="/">Krysalicss</a>
  <ul class="navbar__menu">
    <li class="navbar__item has-dropdown is-hoverable">
      <input id="nav-products" type="checkbox" class="navbar__dropdown-toggle" hidden>
      <label for="nav-products" class="navbar__link">Products ▾</label>
      <ul class="navbar__dropdown">
        <li><a class="navbar__link" href="#">Framework</a></li>
        <li><a class="navbar__link" href="#">Adapters</a></li>
      </ul>
    </li>
  </ul>
</nav>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-navbar')
);
Playground

Primary combination

<nav class="navbar is-primary" aria-label="Primary demo (primary)">
  <a class="navbar__brand" href="#">Krysalicss</a>
  <ul class="navbar__menu">
    <li class="navbar__item"><a class="navbar__link" href="#">Docs</a></li>
    <li class="navbar__item"><a class="navbar__link" href="#">Source</a></li>
  </ul>
</nav>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-navbar')
);
Playground

Markup

A .has-dropdown item pairs a hidden <input type="checkbox"> with a <label for>: clicking the label toggles the checkbox, and :has(input:checked) on the parent reveals the panel. The input is visually clipped but stays focusable so keyboard users can Tab to it and toggle with Space.

markup
<nav class="navbar" aria-label="Primary">
  <a class="navbar__brand" href="/">Krysalicss</a>
  <ul class="navbar__menu">
    <li class="navbar__item">
      <a class="navbar__link" href="/docs">Docs</a>
    </li>
    <li class="navbar__item has-dropdown">
      <input id="nav-more" type="checkbox" class="navbar__dropdown-toggle" hidden>
      <label for="nav-more" class="navbar__link">More ▾</label>
      <ul class="navbar__dropdown">
        <li><a class="navbar__link" href="/explanation">Explanation</a></li>
        <li><a class="navbar__link" href="/reference">Reference</a></li>
      </ul>
    </li>
  </ul>
</nav>

Add class="is-hoverable" next to has-dropdown to also reveal the panel on hover and focus-within (the legacy desktop behaviour). The toggle keeps working — hoverable is additive.

Renamed from .hoverable in this release to align with the framework's .is-* state-prefix convention.

hoverable markup
<nav class="navbar" aria-label="Primary">
  <a class="navbar__brand" href="/">Krysalicss</a>
  <ul class="navbar__menu">
    <li class="navbar__item has-dropdown is-hoverable">
      <input id="nav-products" type="checkbox" class="navbar__dropdown-toggle" hidden>
      <label for="nav-products" class="navbar__link">Products ▾</label>
      <ul class="navbar__dropdown">
        <li><a class="navbar__link" href="#">Framework</a></li>
        <li><a class="navbar__link" href="#">Adapters</a></li>
      </ul>
    </li>
  </ul>
</nav>

Mobile pattern

Below $mobile-breakpoint, the framework hides the inline menu and exposes a .navbar__toggle slot. Wrap the menu in <details> so the <summary> acts as a JS-free open/close trigger.

mobile markup
<nav class="navbar" aria-label="Primary">
  <a class="navbar__brand" href="/">Krysalicss</a>
  <details class="navbar__toggle">
    <summary>Menu</summary>
    <ul class="navbar__menu">
      <li class="navbar__item"><a class="navbar__link" href="/docs">Docs</a></li>
      <li class="navbar__item"><a class="navbar__link" href="/about">About</a></li>
    </ul>
  </details>
</nav>

Wiring with consumer JS

When $controls includes 'js' (the default), the framework also reveals the dropdown for any .has-dropdown item containing [aria-expanded="true"]. Swap the hidden-checkbox markup for a <button aria-expanded> trigger and have your JS flip the attribute:

aria-driven markup
<nav class="navbar" aria-label="Primary">
  <ul class="navbar__menu">
    <li class="navbar__item has-dropdown">
      <button class="navbar__link" type="button" aria-expanded="false" aria-controls="nav-more-panel">
        More ▾
      </button>
      <ul id="nav-more-panel" class="navbar__dropdown">
        <li><a class="navbar__link" href="#">Explanation</a></li>
      </ul>
    </li>
  </ul>
</nav>

This path lets you add focus management, click-outside dismissal, and Esc-to-close — none of which the JS-free path provides.

Behaviour matrix

SurfaceTriggerMechanismLimitation
Desktop dropdown (default)Click on the <label>:has(.navbar__dropdown-toggle:checked) on the parent .has-dropdown item.No outside-click dismiss without consumer JS — the panel only closes when the trigger label is clicked again.
Desktop dropdown (.is-hoverable):hover + :focus-withinReveal CSS layered on top of the toggle path — both work together.Hover affordance is mouse-only, but focus-within still covers keyboard.
JS-driven dropdownaria-expanded="true" on a descendant trigger:has([aria-expanded='true']) on the parent. Emitted only when $controls includes 'js'.Consumer JS owns the attribute toggle, focus management, and dismissal logic.
Mobile toggle<summary> activation (Enter / Space / click)Native <details> open attribute.Browsers that disable <details> lose the open/close affordance.

See Design philosophy for the JS-free rationale.

Keyboard accessibility

  • The CSS-only dropdown reveals on click, hover (when .is-hoverable is set), and focus-within the parent .has-dropdown item. Tabbing into the hidden checkbox or any link inside the panel keeps it open.
  • A fully keyboard-complete dropdown (arrow-key navigation between items, Escape to dismiss, roving tabindex, outside-click closure) requires JS that toggles aria-expanded on the trigger and manages focus inside the menu. CSS cannot move focus or listen for keys.
  • The framework ships both hooks. $controls defaults to ('css', 'js') so the same SCSS emits a CSS-only reveal and a :has([aria-expanded='true']) reveal driven by your script. Wiring the JS — attribute toggle, focus management, dismissal — is the consumer's responsibility; see the Wiring with consumer JS snippet above.

Variables

VariableDefaultNotes
$selector'.navbar'Root.
$brand-selector'.navbar__brand'Logo / wordmark slot.
$menu-selector'.navbar__menu'Top-level <ul>.
$item-selector'.navbar__item'List item; add .has-dropdown to opt into a dropdown.
$link-selector'.navbar__link'Anchor, button, or label inside an item.
$dropdown-selector'.navbar__dropdown'Panel revealed by the toggle, hover (when .is-hoverable), or aria-expanded="true".
$dropdown-toggle-selector'.navbar__dropdown-toggle'Hidden <input type="checkbox"> driving the JS-free toggle.
$hoverable-modifier'.is-hoverable'Modifier on .has-dropdown that adds hover + focus-within reveal alongside the toggle.
$toggle-selector'.navbar__toggle'Mobile toggle host (<details>).
$has-dropdown-modifier'.has-dropdown'Item-level modifier that opts a row into the dropdown styles; absent rows render inline only.
$mobile-breakpointtabletBreakpoint key (see Breakpoints).
$gap$global-gapInline gap between items.
$padding-y$size-small (0.75rem)Vertical inset of the navbar bar.
$padding-x$size-normal (1rem)Horizontal inset of the navbar bar.
$item-padding-y$size-extra-small (0.5rem)Vertical inset of each link / item.
$item-padding-x$size-small (0.75rem)Horizontal inset of each link / item.
$dropdown-padding$size-extra-small (0.5rem)Inset of the open dropdown panel.
$focus-outline-width$global-focus-outline-width (2px)Width of the :focus-visible outline on links and triggers.
$focus-outline-style$global-focus-outline-style (solid)Stroke style of the focus outline.
$focus-outline-offset$global-focus-outline-offset (2px)Gap between the link edge and the focus ring.
$hover-bgcolor-mix(in srgb, currentColor 8%, transparent)Subtle backdrop tint on hovered links + brand; tracks the navbar fg through theme switches.
$dropdown-min-width12remMinimum panel width.
$border-radius$global-border-radiusSurface radius.

See Combinations and light variants for $enable-light, $default-combination, and $controls — cross-cutting plumbing common to every theme-aware component.

Override example

app.scss
@use '@adnap/krysalicss/module/navbar' with (
  $mobile-breakpoint: desktop,
  $gap: 1.5rem,
);

Tokens consumed

TokenUsed for
--kc-navbar-bg / --kc-navbar-fgPlain .navbar. Cascade falls back to --kc-bg / --kc-fg so the navbar tracks the active theme by default. To pin to a brand surface across themes, either use a combination modifier (.navbar.is-primary) or set --kc-navbar-bg / --kc-navbar-fg at consumer level.
--kc-navbar-dropdown-bg / --kc-navbar-dropdown-fgOpen dropdown panel.
--kc-{label}-bg / --kc-{label}-fgEach combination modifier.