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')
);
Playground

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')
);
Playground

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')
);
Playground

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')
);
Playground

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.

markup
<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.

hoverable markup
<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.

navbar 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">
      <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:

css-only build
@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-expanded on the trigger — accessibility-correct; screen readers announce the open/closed state.
  • .is-active on the root — Bulma-compatible class-toggle pattern; cheap to wire from a framework that prefers class-state.
aria-expanded hook
<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>
.is-active hook
<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:

js-only build
@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

SurfaceTriggerMechanismLimitation
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-withinLayered 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 rootDirect 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, Esc to dismiss, roving tabindex, outside-click closure) requires JS that toggles aria-expanded and manages focus. 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.

Variables

VariableDefaultNotes
$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'.
$arrowtrueEmit the CSS chevron ::after the trigger. Set to false to strip it (use when the label already carries an icon).
$arrow-size0.3emHalf-width of the chevron triangle. Em-relative so the glyph scales with the trigger's font-size.
$arrow-gap0.5emInline 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-bgcolor-mix(in srgb, currentColor 8%, transparent)Subtle backdrop tint on hovered items; tracks the panel fg through theme switches.
$min-width12remMinimum panel width.
$offset2pxStack distance between trigger and panel.
$border-radius$global-border-radiusPanel surface radius.
$z-index10Panel stacking order.
$controls$global-controlsReveal 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

app.scss
@use '@adnap/krysalicss/module/dropdown' with (
  $min-width: 16rem,
  $offset: 4px,
);

Tokens consumed

TokenUsed for
--kc-dropdown-bg / --kc-dropdown-fgOpen panel surface. Falls back through --kc-bg / --kc-fg so the panel tracks the active theme.
--kc-{label}-bg / --kc-{label}-fgEach combination modifier (.dropdown.is-primary, etc.).
--kc-focus-ringTrigger and menu-item focus outline.