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.