Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Frosted dropdowns

Shared frosted-glass dropdowns with site colors, spacing, fonts, keyboard controls, and native form integration.

Use this shared dropdown with the custom cursor and paired-logo footer. Its options stay on the page, so the cursor remains visible through the open menu.

Preview

Selected: studio

Install

Keep a normal, labelled single-select control. The script enhances it and later selects added to the page. It includes its own styles; no framework or separate stylesheet is needed.

<label for="workspace">Workspace</label>
<select id="workspace" name="workspace">
  <option value="studio">Design studio</option>
  <option value="personal">Personal projects</option>
</select>

<shaughv-cursor color="sage"></shaughv-cursor>
<script defer src="https://cdn.shaughv.com/js/shaughv-cursor.js?v=2026-10-21.4"></script>
<script defer data-auto src="https://cdn.shaughv.com/js/shaughv-dropdown.js?v=2026-10-21.2"></script>

Complete the page with the footer recipe. If JavaScript fails to load, the original select remains usable. Multiple-select controls and native listboxes (size greater than one) keep their native presentation. A control that needs an OS-specific experience may use data-dropdown="native"; its native popup will use the system cursor.

Customize

Set these properties on the select or an ancestor. The trigger and menu inherit the select’s font, direction, and color scheme. Explicit color tokens make the result predictable in a custom theme. Dark color schemes use a softer edge sheen on the closed trigger; the open menu keeps its frosted treatment.

.project-form {
  --dropdown-accent: #8359c7;
  --dropdown-foreground: #293235;
  --dropdown-surface: #ffffff;
  --dropdown-space: 13px;
  --dropdown-radius: 12px;
  --dropdown-width: 100%;
  --dropdown-max-height: 320px;
  --dropdown-font: "Makira", system-ui, sans-serif;
  --dropdown-font-size: 15px;
}
[data-theme="dark"] .project-form {
  --dropdown-foreground: #f0f3f2;
  --dropdown-surface: #292e2d;
  color-scheme: dark;
}
Property Default Purpose
--dropdown-accent #5b8a5b Check and keyboard focus color
--dropdown-foreground CanvasText Trigger and option text
--dropdown-surface Canvas Base color for the translucent material
--dropdown-space 13px Vertical trigger padding; rows retain a 44px minimum target
--dropdown-radius 12px Trigger adds 4px; menu adds 8px
--dropdown-width auto Control width, limited to its container
--dropdown-max-height 320px Menu height in pixels, also limited by viewport space
--dropdown-font Inherited Site typeface
--dropdown-font-size Inherited Site type size

Behavior

The menu opens in the browser’s top layer where available, flips above the trigger near the bottom edge, and scrolls long option lists. It follows page scrolling, resizing, and the visual viewport. Without popover support it uses a fixed-position menu; avoid transformed ancestors on modal containers in that fallback. Without backdrop blur it uses an opaque surface. Forced-colors mode retains system colors and visible focus.

Focus stays on a labelled combobox trigger, with the active option exposed through aria-activedescendant. Arrow keys open and move through enabled options; Home and End jump to the ends. Typing matches option labels. Enter or Space commits; Escape closes without changing the selection. Tab commits the active option and continues through the page. Pointer selection commits immediately; clicking elsewhere closes the menu.

Touch uses the same page-rendered controls with 44px minimum targets. The custom cursor stays off for touch. Reduced motion removes reveal and chevron animation; the cursor’s reduced-motion behavior applies separately. Keyboard focus remains visible. Browser interaction checks cover these mechanics; dedicated assistive-technology acceptance is still a separate check for a consuming site.

Lifecycle

The native select remains the form’s source of truth: name, selected value, FormData, labels, disabled state, required validation, option groups, and form resets remain connected. A committed change emits bubbling input and change events. Option text is inserted as text, never interpreted as HTML.

For a framework, load the script once without data-auto, then own its lifecycle:

const dropdowns = window.ShaughvDropdown.mount({ root: formElement });
// After direct value assignments or stylesheet-only theme changes:
dropdowns.refresh();
// On component teardown:
dropdowns.destroy();

For one select, use ShaughvDropdown.enhance(selectElement). It returns refresh() and destroy(); unsupported native controls return null. Repeated enhancement returns the existing instance. Mounting, enhancing, or refreshing restores the shared stylesheet if a page router replaced the document head. mount() owns only the instances it creates, observes additions/removals, and removes its menus and listeners on teardown. Avoid overlapping lifecycle owners.

useEffect(() => {
  const dropdown = window.ShaughvDropdown.enhance(selectRef.current);
  return () => dropdown?.destroy();
}, []);
useEffect(() => {
  window.ShaughvDropdown.enhance(selectRef.current)?.refresh();
}, [value, theme]);

Keep the select in the framework’s markup. Event-driven controlled updates are read again after the handler. Direct assignments such as select.value = "personal" do not emit a browser event: call refresh() afterward. Ancestor class, style, theme-attribute, and system color-scheme changes update the trigger and open menu automatically. Call refresh() after replacing a stylesheet or changing inherited tokens without one of those signals. If a field is invalid, the trigger receives focus and an associated message displays the browser’s validation text.

Reference

Read usage.site_defaults and usage.components.dropdown in the live catalog, or fetch dropdown-info.json. The same guide is available as /dropdowns.md, /dropdowns.mdx, and /api/docs/pages/dropdowns.json, and through the documentation catalogs.

Was this page helpful?