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')
); 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')
); 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')
); 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.
<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.
<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.
<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:
<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
| Surface | Trigger | Mechanism | Limitation |
|---|---|---|---|
| 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-within | Reveal CSS layered on top of the toggle path — both work together. | Hover affordance is mouse-only, but focus-within still covers keyboard. |
| JS-driven dropdown | aria-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-hoverableis set), and focus-within the parent.has-dropdownitem. Tabbing into the hidden checkbox or any link inside the panel keeps it open. - A fully keyboard-complete dropdown (arrow-key navigation between
items,
Escapeto dismiss, rovingtabindex, outside-click closure) requires JS that togglesaria-expandedon the trigger and manages focus inside the menu. CSS cannot move focus or listen for keys. - The framework ships both hooks.
$controlsdefaults 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
| Variable | Default | Notes |
|---|---|---|
$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-breakpoint | tablet | Breakpoint key (see Breakpoints). |
$gap | $global-gap | Inline 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-bg | color-mix(in srgb, currentColor 8%, transparent) | Subtle backdrop tint on hovered links + brand; tracks the navbar fg through theme switches. |
$dropdown-min-width | 12rem | Minimum panel width. |
$border-radius | $global-border-radius | Surface radius. |
See Combinations and light variants for $enable-light, $default-combination, and $controls — cross-cutting plumbing common to every theme-aware component.
Override example
@use '@adnap/krysalicss/module/navbar' with (
$mobile-breakpoint: desktop,
$gap: 1.5rem,
); Tokens consumed
| Token | Used for |
|---|---|
--kc-navbar-bg / --kc-navbar-fg | Plain .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-fg | Open dropdown panel. |
--kc-{label}-bg / --kc-{label}-fg | Each combination modifier. |