Component

Headless web components

@getdom/studio/headless

Behaviour shipped as plain custom elements — works in any HTML, in any framework, with zero JavaScript runtime beyond the controls themselves.

Why

The headless layer

Every interactive component in this library starts as a custom element — <dom-dropdown>, <dom-dialog>, <dom-popover> and so on. They carry the behavioural contract (state, ARIA wiring, keyboard handling, event emission) but no visual styling. Drop them into a plain HTML page, a Rails view, a Django template, or wrap them in any framework. The Vue components in this library are thin wrappers around exactly these controls.

  • ✓ ~2kb gzipped per control, SSR-safe (guards customElements)
  • ✓ Light DOM by default — Tailwind classes cascade in normally
  • ✓ ARIA + roving tabindex + focus management built in
  • ✓ Events use the dom:* convention with bubbling

Install

One import, all controls

<!-- Register every <dom-*> custom element. One import is enough — they
   ship as ES modules and self-register via customElements.define. -->
<script type="module">
  import '@getdom/studio/headless';
</script>

<!-- Or pick what you need: -->
<script type="module">
  import '@getdom/studio/headless/dropdown.js';
  import '@getdom/studio/headless/dialog.js';
</script>

Floating UI

Dropdowns and popovers without the usual traps

The shared floating.js engine and <dom-popover> panel solve the failure modes that usually make dropdowns expensive: clipped parents, transformed ancestors, visual viewport offsets, side collisions, scroll containers, long menus, arrow placement, and resize reflow. Use the headless popover, dropdown, combobox, autocomplete, and tooltip controls first; drop to the low-level helper only for custom surfaces.

View floating layer guide →

Reference

Every control at a glance

<dom-dropdown>

A menu that opens from a button. The menu panel uses the same native Popover API as dom-popover (top layer, light-dismiss, Esc), plus menu keyboard navigation and dom:select.

Attrs
open, placement, align, offset, collision-padding, floating-mode, lock-scroll, data-menu-id
Events
dom:open · dom:close · dom:select
<dom-dialog>

Self-contained wrapper around the native HTML <dialog>. Top-layer rendering, nested stacking, ::backdrop and Esc-to-close are all native. Trigger slot is optional — open via commandfor, the open attribute, or .open() / .close() / .toggle().

Attrs
id, open, static, no-backdrop
Events
dom:open · dom:close
<dom-popover>

Floating panel anchored to a trigger. Built on the native HTML Popover API — top layer, light-dismiss and Esc are handled by the browser.

Attrs
open, placement, align, offset, collision-padding, floating-mode, flip, lock-scroll, trigger, hover-close-delay, data-trigger-id, data-panel-id
Events
dom:open · dom:close
<dom-tabs>

Tab list with managed ARIA, roving tabindex and panel linking via data-tab / data-panel pairs.

Attrs
value
Events
dom:change
<dom-toggle>

Accessible switch with role="switch", keyboard support, and aria-checked maintained as you toggle.

Attrs
checked, disabled, aria-label
Events
dom:change
<dom-tooltip>

Lightweight tooltip wired through aria-describedby. The bubble appears on hover and focus, and is injected as a sibling element.

Attrs
text, placement, delay
Events
dom:show · dom:hide
<dom-accordion>

Disclosure-panel group containing one or more <dom-accordion-item> children.

Attrs
multiple
Events
<dom-combobox>

Select-like combobox with a text input, optional toggle button, managed activedescendant, keyboard navigation, and floating list positioning.

Attrs
value, open, placement, floating-mode, data-menu-id
Events
dom:input · dom:query · dom:select · dom:change
<dom-autocomplete>

Free-text autocomplete. Suggestions are optional; typed values are valid and Enter commits the current text.

Attrs
value, open, placement, floating-mode, data-menu-id
Events
dom:input · dom:query · dom:select · dom:custom · dom:change
<dom-drawer>

Light-DOM drawer primitive anchored to the left, right, or bottom edge. It owns open state, ARIA, focus trap, scroll lock, Esc/backdrop dismiss, and events while keeping panel/backdrop styling in editable Tailwind classes.

Attrs
open, show, side, static, enter, enter-from, enter-to, leave, leave-from, leave-to
Events
dom:open · dom:close

Cards reflect each class's static __doc. Click for the full slots / attributes / events / keyboard reference.

Events

The dom:* convention

Every control emits dom:open / dom:close when it shows or hides, plus an component-specific verb (dom:select, dom:change, dom:show) for the action it mediates. All events bubble and use the detail field for data:

document.addEventListener('dom:select', (e) => {
  console.log(e.target.tagName, '→', e.detail.value);
});

Teleport / portals

External popups

When a popup needs to escape its host (e.g. you teleport a menu to document.body to dodge an ancestor with overflow: hidden), set data-menu-id on <dom-dropdown> or <dom-combobox>, and data-panel-id on <dom-popover>. The headless re-resolves the popup from document.getElementById on every open.