Dropdown
Reusable disclosure panel: a trigger and a menu, revealed on click,
hover (opt-in), or aria-expanded="true". Hidden-checkbox
pattern by default, zero JavaScript. Compose inside a navbar, on a
card, or anywhere a panel needs to anchor to a control.
Live
Click-toggle (default)
<div class="dropdown">
<input id="dd-1" type="checkbox" class="dropdown__toggle" hidden>
<label for="dd-1" class="dropdown__trigger">Menu</label>
<ul class="dropdown__menu">
<li><a href="#">Profile</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Sign out</a></li>
</ul>
</div> @use '@adnap/krysalicss' with (
$feature-list: ('base-reset', 'base-global', 'module-dropdown')
); Hoverable modifier (hover + focus-within reveal)
<div class="dropdown is-hoverable">
<input id="dd-h" type="checkbox" class="dropdown__toggle" hidden>
<label for="dd-h" class="dropdown__trigger">Products</label>
<ul class="dropdown__menu">
<li><a href="#">Framework</a></li>
<li><a href="#">Adapters</a></li>
</ul>
</div> @use '@adnap/krysalicss' with (
$feature-list: ('base-reset', 'base-global', 'module-dropdown')
); Right-aligned panel (.is-right)
<div class="dropdown is-right">
<input id="dd-r" type="checkbox" class="dropdown__toggle" hidden>
<label for="dd-r" class="dropdown__trigger">Account</label>
<ul class="dropdown__menu">
<li><a href="#">Profile</a></li>
<li><a href="#">Sign out</a></li>
</ul>
</div> @use '@adnap/krysalicss' with (
$feature-list: ('base-reset', 'base-global', 'module-dropdown')
); Composed inside a navbar item
<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">
<div class="dropdown">
<input id="nav-more" type="checkbox" class="dropdown__toggle" hidden>
<label for="nav-more" class="dropdown__trigger navbar__link">More</label>
<ul class="dropdown__menu">
<li><a href="/explanation">Explanation</a></li>
<li><a href="/reference">Reference</a></li>
</ul>
</div>
</li>
</ul>
</nav> @use '@adnap/krysalicss' with (
$feature-list: ('base-reset', 'base-global', 'module-dropdown')
); Markup
The trigger automatically paints a CSS chevron ::after its label —
no glyph in the markup, no font dependency. The chevron flips upward
when the .is-up modifier is set. To suppress it (e.g. the label
already carries an icon), set $arrow: false at compile time.
The default JS-free markup pairs a hidden <input type="checkbox"> with
a <label for>: clicking the label toggles the checkbox, and
:has(.dropdown__toggle:checked) on the parent reveals the menu. The
input is visually clipped but stays focusable so keyboard users can Tab
to it and toggle with Space.
<div class="dropdown">
<input id="dd-1" type="checkbox" class="dropdown__toggle" hidden>
<label for="dd-1" class="dropdown__trigger">Menu</label>
<ul class="dropdown__menu">
<li><a href="#">Profile</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Sign out</a></li>
</ul>
</div> Add .is-hoverable to the root to also reveal the panel on hover and
focus-within. The toggle keeps working — the modifier is additive.
Renamed from .hoverable in this release to align with the framework's
.is-* state-prefix convention.
<div class="dropdown is-hoverable">
<input id="dd-h" type="checkbox" class="dropdown__toggle" hidden>
<label for="dd-h" class="dropdown__trigger">Products</label>
<ul class="dropdown__menu">
<li><a href="#">Framework</a></li>
<li><a href="#">Adapters</a></li>
</ul>
</div> Add .is-right to anchor the panel to the trigger's inline-end edge
instead of the inline-start (use near viewport edges). .is-up opens
the panel above the trigger.
Composing inside a navbar
module/dropdown slots cleanly into a navbar__item. The navbar
flattens the panel (drops the absolute position and shadow) below
$mobile-breakpoint so the menu stacks with the rest of the items.
<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">
<div class="dropdown">
<input id="nav-more" type="checkbox" class="dropdown__toggle" hidden>
<label for="nav-more" class="dropdown__trigger navbar__link">More</label>
<ul class="dropdown__menu">
<li><a href="/explanation">Explanation</a></li>
<li><a href="/reference">Reference</a></li>
</ul>
</div>
</li>
</ul>
</nav> Modes
The component supports two reveal modes, both emitted by default. Pick
either, both, or strip one at compile time via $controls.
CSS-only mode
Hidden <input type="checkbox" class="dropdown__toggle"> paired with a
<label for> trigger. Clicking the label toggles the checkbox, and
:has(.dropdown__toggle:checked) reveals the menu. Zero JavaScript.
The .is-hoverable modifier is part of this mode — it layers :hover /
:focus-within reveal on top.
To ship the CSS branch only and strip the JS hooks from the compiled output:
@use '@adnap/krysalicss/module/dropdown' with (
$controls: ('css'),
); JS mode
When $controls includes 'js', the framework also reveals the menu
for any .dropdown containing [aria-expanded="true"] or carrying
.is-active on the root. Two hooks, pick the one that fits your state
model:
aria-expandedon the trigger — accessibility-correct; screen readers announce the open/closed state..is-activeon the root — Bulma-compatible class-toggle pattern; cheap to wire from a framework that prefers class-state.
<div class="dropdown">
<button class="dropdown__trigger" type="button" aria-expanded="false" aria-controls="dd-aria-panel">
Menu
</button>
<ul id="dd-aria-panel" class="dropdown__menu">
<li><a href="#">Profile</a></li>
<li><a href="#">Sign out</a></li>
</ul>
</div> <div class="dropdown is-active">
<button class="dropdown__trigger" type="button">Menu</button>
<ul class="dropdown__menu">
<li><a href="#">Profile</a></li>
<li><a href="#">Sign out</a></li>
</ul>
</div> To ship the JS branch only and strip the hidden-checkbox CSS reveal:
@use '@adnap/krysalicss/module/dropdown' with (
$controls: ('js'),
); Either JS hook lets consumer code add focus management, click-outside
dismissal, and Esc-to-close — none of which the CSS-only path
provides.
Reveal matrix
| Surface | Trigger | Mechanism | Limitation |
|---|---|---|---|
| Click toggle (default) | Click on the <label> | :has(.dropdown__toggle:checked) on the parent. | No outside-click dismiss without consumer JS. |
Hoverable (.is-hoverable) | :hover + :focus-within | Layered on top of the toggle path — both work together. | Hover affordance is mouse-only; focus-within covers keyboard. |
| JS-driven (ARIA) | aria-expanded="true" on a descendant | :has([aria-expanded='true']) on the parent. Emitted only when $controls includes 'js'. | Consumer JS owns the attribute toggle, focus, and dismissal. |
| JS-driven (class) | .is-active on the root | Direct class match. Emitted only when $controls includes 'js'. | No accessibility hook by itself — pair with aria-expanded on the trigger if a screen reader needs the state. |
Keyboard accessibility
- The JS-free dropdown reveals on click and (with
.is-hoverable) hover / focus-within. Tabbing into the hidden checkbox or any link inside the panel keeps it open. - A fully keyboard-complete dropdown (arrow-key navigation,
Escto dismiss, rovingtabindex, outside-click closure) requires JS that togglesaria-expandedand manages focus. 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.
Variables
| Variable | Default | Notes |
|---|---|---|
$selector | '.dropdown' | Root. |
$toggle-selector | '.dropdown__toggle' | Visually clipped <input type="checkbox">. |
$trigger-selector | '.dropdown__trigger' | Visible <label> or <button>. |
$menu-selector | '.dropdown__menu' | Panel revealed by the toggle, hover, or aria-expanded. |
$hoverable-modifier | '.is-hoverable' | Modifier on the root that adds hover + focus-within reveal. |
$right-modifier | '.is-right' | Right-align the panel to the trigger. |
$up-modifier | '.is-up' | Open the panel above the trigger. |
$active-modifier | '.is-active' | JS-mode root toggle. Emitted only when $controls includes 'js'. |
$arrow | true | Emit the CSS chevron ::after the trigger. Set to false to strip it (use when the label already carries an icon). |
$arrow-size | 0.3em | Half-width of the chevron triangle. Em-relative so the glyph scales with the trigger's font-size. |
$arrow-gap | 0.5em | Inline gap between the label and the chevron. |
$gap | $size-extra-small (0.5rem) | Inter-item gap inside the panel. |
$padding | $size-extra-small (0.5rem) | Outer panel inset. |
$item-padding-y | $size-extra-small (0.5rem) | Vertical inset of each menu item. |
$item-padding-x | $size-small (0.75rem) | Horizontal inset of each menu item. |
$focus-outline-width | $global-focus-outline-width (2px) | Width of the :focus-visible outline on trigger and items. |
$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 trigger / item edge and the focus ring. |
$hover-bg | color-mix(in srgb, currentColor 8%, transparent) | Subtle backdrop tint on hovered items; tracks the panel fg through theme switches. |
$min-width | 12rem | Minimum panel width. |
$offset | 2px | Stack distance between trigger and panel. |
$border-radius | $global-border-radius | Panel surface radius. |
$z-index | 10 | Panel stacking order. |
$controls | $global-controls | Reveal mechanism(s) — see Wiring with consumer JS above. |
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/dropdown' with (
$min-width: 16rem,
$offset: 4px,
); Tokens consumed
| Token | Used for |
|---|---|
--kc-dropdown-bg / --kc-dropdown-fg | Open panel surface. Falls back through --kc-bg / --kc-fg so the panel tracks the active theme. |
--kc-{label}-bg / --kc-{label}-fg | Each combination modifier (.dropdown.is-primary, etc.). |
--kc-focus-ring | Trigger and menu-item focus outline. |