Appearance
Dropdown
A trigger + popup menu, built as a single component with slots for each part (root, trigger, menu, item, divider). Open/closed state lives on the menu as a data-state attribute; per-item variants (e.g. a destructive "Delete") live on items as data-variant attributes. Runtime state lives on data attributes, not varia variants, so it can pair directly with the JavaScript that toggles them.
Authoring
ts
// recipes/dropdown.config.ts
import { defineComponent } from 'varia'
export default defineComponent('dropdown', {
slots: {
root: 'relative inline-block',
trigger: [
'inline-flex items-center justify-between gap-2 px-3 py-2',
'rounded-md border border-gray-300 bg-white text-sm font-medium text-gray-700',
'shadow-sm hover:bg-gray-50',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
],
menu: [
'absolute z-10 mt-2 min-w-40 origin-top-right',
'rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5',
// Hidden by default; data-state=open reveals it (higher specificity wins).
'hidden data-[state=open]:block',
],
item: [
'block w-full px-4 py-2 text-left text-sm text-gray-700',
'hover:bg-gray-100 focus:bg-gray-100 focus:outline-none',
'disabled:text-gray-400 disabled:cursor-not-allowed',
// Per-item destructive variant via data-variant.
'data-[variant=danger]:text-red-700 data-[variant=danger]:hover:bg-red-50',
'data-[variant=danger]:focus:bg-red-50',
],
divider: 'my-1 border-t border-gray-200',
},
variants: {
align: {
start: { menu: 'left-0' },
end: { menu: 'right-0' },
},
},
})Three things to call out:
alignis a varia variant rather than a data-attr because it's a build-time configuration choice (which side of the trigger the menu opens from), not a runtime state. The variant class goes on the root (<div class="dropdown dropdown-align-end">) because it's slot-keyed: it emits.dropdown-align-end .dropdown__menu { right: 0 }, a descendant rule that needs the alignment class on an ancestor.Open/closed is a
data-stateattribute on the menu. The base expansion includeshidden data-[state=open]:block, so the menu is hidden by default and revealed whendata-state="open"is present. JS sets or clears that attribute. No varia variant exists for "open" because it's runtime state, not configuration.Per-item destructive styling is a
data-variantattribute, not a separate slot or boolean variant. Adding more item modes later means moredata-[variant=...]:rules in theitemslot expansion, not new slots, so the slot list stays small.
Live preview
A dropdown rendered statically with data-state="open" so you can see all the slots at once:
Consumption
html
<div class="dropdown dropdown-align-end">
<button class="dropdown__trigger" aria-haspopup="menu" aria-expanded="false">
Options
<svg>...chevron...</svg>
</button>
<div class="dropdown__menu" data-state="closed" role="menu">
<button class="dropdown__item" role="menuitem">Edit</button>
<button class="dropdown__item" role="menuitem">Duplicate</button>
<hr class="dropdown__divider" />
<button class="dropdown__item" data-variant="danger" role="menuitem">Delete</button>
</div>
</div>A minimal JS toggle that pairs with this markup:
ts
const trigger = document.querySelector('.dropdown__trigger')
const menu = document.querySelector('.dropdown__menu')
function setOpen(open: boolean) {
menu.setAttribute('data-state', open ? 'open' : 'closed')
trigger.setAttribute('aria-expanded', String(open))
}
trigger.addEventListener('click', () => {
setOpen(menu.getAttribute('data-state') !== 'open')
})
document.addEventListener('click', (e) => {
if (!menu.contains(e.target) && !trigger.contains(e.target))
setOpen(false)
})Generated class names
| Class | Element |
|---|---|
dropdown | The root wrapper (position: relative) |
dropdown__trigger | The button that opens the menu |
dropdown__menu | The popup container (hidden until data-state="open") |
dropdown__item | A clickable menu row |
dropdown__divider | A horizontal separator |
dropdown-align-start / dropdown-align-end | Variant on the root that anchors the menu's left or right edge |
Six classes (plus the two data-attrs you set in markup) for trigger, menu, items, divider, and alignment.