# Dropdown

A click-triggered menu that opens anchored to the trigger element.

## Basic usage

<DemoBlock title="Trigger on click">
  <oas-dropdown items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]' placement="bottom">
    <oas-button type="primary">Actions</oas-button>
  </oas-dropdown>
</DemoBlock>

## Trigger

The `trigger` attribute controls how the menu opens: `click` (default) / `hover` / `focus`, multi-selectable with spaces (e.g. `"click hover"`). With hover, `hover-delay` / `hover-hide-delay` debounce open/close (default 150 / 100ms — without a delay hover flickers). The hover area is the trigger plus the floating panel (moving across the gap does not close it).

<DemoBlock title="Hover trigger">
  <oas-dropdown trigger="hover" items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Hover to open</oas-button>
  </oas-dropdown>
</DemoBlock>

<DemoBlock title="Focus trigger">
  <oas-dropdown trigger="focus" items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Focus to open (Tab or click)</oas-button>
  </oas-dropdown>
</DemoBlock>

<DemoBlock title="Multiple triggers (click + hover)">
  <oas-dropdown trigger="click hover" items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Click or hover</oas-button>
  </oas-dropdown>
</DemoBlock>

<DemoBlock title="Custom hover delays">
  <oas-space size="small">
    <oas-dropdown trigger="hover" hover-delay="400" items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>hover-delay=400</oas-button>
    </oas-dropdown>
    <oas-dropdown trigger="hover" hover-hide-delay="400" items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>hover-hide-delay=400</oas-button>
    </oas-dropdown>
  </oas-space>
</DemoBlock>

### Touchscreen degradation (automatic on coarse pointers)

Touch devices (`pointer: coarse`, e.g. phones and tablets) have no stable hover semantics: a tap synthesizes both hover and click events, which would open-then-immediately-close the menu with the desktop behavior. When `trigger` includes `hover`, the hover channel is automatically disabled on coarse pointers and degrades to **tap toggling** — tap the trigger to open, tap again to close, or tap outside the menu to dismiss (outside dismiss replaces hover-out). Fine-pointer (desktop mouse) behavior is unchanged; compatible with controlled `open`.

<DemoBlock title="Tap toggling on touch (automatic on touch devices)">
  <oas-dropdown id="dd-coarse-tap" trigger="hover" items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Tap me on touch</oas-button>
  </oas-dropdown>
</DemoBlock>

## Placement

<DemoBlock title="Four directions">
  <oas-dropdown placement="top" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Up</oas-button>
  </oas-dropdown>
  <oas-dropdown placement="bottom" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Down</oas-button>
  </oas-dropdown>
  <oas-dropdown placement="left" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Left</oas-button>
  </oas-dropdown>
  <oas-dropdown placement="right" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Right</oas-button>
  </oas-dropdown>
</DemoBlock>

## 12-way placement

`placement` supports 12 directions: each of the four bases (`top / bottom / left / right`) pairs with a cross-axis `-start` / `-end` align (`bottom-start` — the panel's left edge aligns with the trigger's left edge — is the most common form). When the panel flips along the main axis due to lack of space, the align suffix is preserved (`bottom-start` → `top-start`), and the aligned position is still clamped to the viewport.

<DemoBlock title="12-way placement">
  <oas-space size="small">
    <oas-dropdown placement="bottom-start" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>bottom-start</oas-button>
    </oas-dropdown>
    <oas-dropdown placement="bottom-end" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>bottom-end</oas-button>
    </oas-dropdown>
    <oas-dropdown placement="right-start" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>right-start</oas-button>
    </oas-dropdown>
    <oas-dropdown placement="top-end" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>top-end</oas-button>
    </oas-dropdown>
  </oas-space>
</DemoBlock>

## Nested submenus

`items` entries support a `children` array for cascading submenus (any depth); hover/click expands, selecting a leaf collapses and closes. The floating menu reuses `oas-menu`, so nested submenus automatically flip left/up near viewport edges to stay fully visible.

<DemoBlock title="Nested submenus">
  <oas-dropdown items='[{"label":"File","value":"file","children":[{"label":"New","value":"new","children":[{"label":"File","value":"new-file"},{"label":"Window","value":"new-window"}]},{"label":"Open","value":"open"}]},{"label":"Edit","value":"edit"}]'>
    <oas-button>More actions</oas-button>
  </oas-dropdown>
</DemoBlock>

## Disabled items

<DemoBlock title="Disabled items">
  <oas-dropdown items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete","disabled":true}]'>
    <oas-button>Actions</oas-button>
  </oas-dropdown>
</DemoBlock>

## Disabled dropdown

`disabled` disables the whole dropdown: click / hover / focus triggers are ignored, the split arrow button is disabled, the host is desaturated (opacity .6) and syncs `aria-disabled`.

<DemoBlock title="Disabled dropdown">
  <oas-space size="small">
    <oas-dropdown disabled items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>Disabled (click)</oas-button>
    </oas-dropdown>
    <oas-dropdown disabled split items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>Disabled (split)</oas-button>
    </oas-dropdown>
  </oas-space>
</DemoBlock>

## Arrow

The menu panel shows an arrow pointing at the trigger element . The `arrow` attribute toggles it (shown by default; `arrow="false"` hides it). `arrow-point-at-center` pins the arrow to the panel center (by default the arrow follows the trigger's projection, so it still points at the trigger even after viewport-avoidance shifting). `auto-adjust-overflow` controls whether the placement auto-flips when there is not enough viewport space (enabled by default).

<DemoBlock title="With arrow">
  <oas-dropdown id="dd-arrow" placement="bottom" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
    <oas-button type="primary">With arrow</oas-button>
  </oas-dropdown>
</DemoBlock>

<DemoBlock title="Arrow visibility">
  <oas-space size="small">
    <oas-dropdown items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>Default (arrow)</oas-button>
    </oas-dropdown>
    <oas-dropdown id="dd-arrow-none" arrow="false" items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>arrow="false"</oas-button>
    </oas-dropdown>
  </oas-space>
</DemoBlock>

<DemoBlock title="Arrow at center & no overflow adjust">
  <oas-space size="small">
    <oas-dropdown arrow-point-at-center items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>arrow-point-at-center</oas-button>
    </oas-dropdown>
    <oas-dropdown auto-adjust-overflow="false" items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>auto-adjust-overflow="false"</oas-button>
    </oas-dropdown>
  </oas-space>
</DemoBlock>

## Selection event

<DemoBlock title="Selection event">
  <oas-dropdown id="dd-event" onoas-select="ddLog(event)" items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Select an action</oas-button>
  </oas-dropdown>
  <oas-tag id="dd-result" type="info">Nothing selected</oas-tag>
</DemoBlock>

## Keep open after select

`hide-on-click` controls whether selecting a menu item closes the menu (default `true` — close on select; `"false"` keeps it open, useful for multi-select / check scenarios).

<DemoBlock title="Keep open after select">
  <oas-dropdown id="dd-keep" hide-on-click="false" items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Keep open</oas-button>
  </oas-dropdown>
  <oas-tag id="dd-keep-result" type="info">Nothing selected</oas-tag>
</DemoBlock>

## Controlled display

The `open` attribute is controlled: an external button can set/remove `open` to show/hide the menu (clicking outside / pressing Esc / selecting an item still closes it).

<DemoBlock title="Controlled display (open attribute)">
  <oas-space size="small">
    <oas-button type="primary" size="small" onclick="event.stopPropagation; ddOpen(true)">Open</oas-button>
    <oas-button size="small" onclick="event.stopPropagation; ddOpen(false)">Close</oas-button>
    <oas-tag id="dd-open-status" type="info">open: false</oas-tag>
  </oas-space>
  <oas-dropdown id="dd-ctrl" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Trigger element</oas-button>
  </oas-dropdown>
</DemoBlock>

## Open-change event

The component fires `oas-open-change` (`detail: { open }`) whenever it opens or closes itself (click / hover / focus / Esc / outside click / select) — the host can sync state in controlled mode (same semantics as tooltip / popover; externally writing `open` also triggers it).

<DemoBlock title="oas-open-change event">
  <oas-space size="small">
    <oas-dropdown id="dd-open-change" trigger="click hover" onoas-open-change="ddOpenChange(event)" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>Click or hover</oas-button>
    </oas-dropdown>
    <oas-tag id="dd-open-change-status" type="info">open: false</oas-tag>
  </oas-space>
</DemoBlock>

## Offset & scroll

`offset` adjusts the gap between the panel and the trigger (default 8px). When the page scrolls, the panel repositions to follow the trigger (fixing the fixed-positioning detachment); `close-on-scroll` closes it on scroll instead.

<DemoBlock title="Offset">
  <oas-space size="small">
    <oas-dropdown placement="bottom-start" items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>Default (8px)</oas-button>
    </oas-dropdown>
    <oas-dropdown placement="bottom-start" offset="16" items='[{"label":"Edit","value":"edit"}]'>
      <oas-button>offset=16</oas-button>
    </oas-dropdown>
  </oas-space>
</DemoBlock>

<DemoBlock title="Close on scroll">
  <oas-dropdown id="dd-scroll" close-on-scroll items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Open then scroll the page</oas-button>
  </oas-dropdown>
</DemoBlock>

## Controlled selection

The `value` attribute is controlled: an external value sets the selected item (the dropdown shows no check mark, so a tag echoes the current value in real time); selecting a menu item also updates `value` and fires `oas-select`.

<DemoBlock title="Controlled selection (value attribute)">
  <oas-space size="small">
    <oas-button size="small" onclick="ddValue('edit')">Select "Edit"</oas-button>
    <oas-button size="small" onclick="ddValue('copy')">Select "Copy"</oas-button>
    <oas-button size="small" onclick="ddValue('')">Clear</oas-button>
    <oas-tag id="dd-value-status" type="info">value: -</oas-tag>
  </oas-space>
  <oas-dropdown id="dd-value" onoas-select="ddValueLog(event)" items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button>Select an action</oas-button>
  </oas-dropdown>
</DemoBlock>

## Split button

The `split` attribute turns the dropdown into a split button : a main button plus a separate arrow button. Click the arrow to open the menu; clicking the main button fires `oas-action` (bind your primary action, e.g. save); selecting a menu item still fires `oas-select`.

<DemoBlock title="Split button">
  <oas-dropdown split items='[{"label":"Edit","value":"edit"},{"label":"Copy","value":"copy"},{"label":"Delete","value":"delete"}]'>
    <oas-button type="primary">Save & submit</oas-button>
  </oas-dropdown>
</DemoBlock>

<DemoBlock title="Main button action event">
  <oas-space size="small">
    <oas-dropdown id="dd-split" split onoas-action="ddSplitAction(event)" items='[{"label":"Edit","value":"edit"},{"label":"Delete","value":"delete"}]'>
      <oas-button>More actions</oas-button>
    </oas-dropdown>
    <oas-tag id="dd-split-result" type="info">Not clicked yet</oas-tag>
  </oas-space>
</DemoBlock>

## Loading menu items

A menu item with `loading: true` enters a loading state: it shows a spinning indicator and is not selectable (click / keyboard / hover are all ignored); update `items` to remove `loading` once the async work finishes. The demo below marks "Save" as loading for ~1.5s after you select it.

<DemoBlock title="Loading menu items">
  <oas-dropdown items='[{"label":"Save","value":"save"},{"label":"Syncing…","value":"syncing","loading":true},{"label":"Delete","value":"delete"}]'>
    <oas-button>Actions</oas-button>
  </oas-dropdown>
</DemoBlock>

<DemoBlock title="Async operation demo">
  <oas-space size="small">
    <oas-dropdown id="dd-async" onoas-select="ddAsyncLog(event)" items='[{"label":"Save","value":"save"},{"label":"Save as","value":"save-as"},{"label":"Delete","value":"delete"}]'>
      <oas-button>Select an action</oas-button>
    </oas-dropdown>
    <oas-tag id="dd-async-status" type="info">Nothing selected</oas-tag>
  </oas-space>
</DemoBlock>

## Declarative child-element channel

Besides the `items` JSON, you can declare the menu with `<oas-dropdown-item>` / `<oas-dropdown-group>` / `<oas-dropdown-divider>` child elements (the `items` attribute **wins when set explicitly**; otherwise children are parsed and converge to the same rendering path). Default slot text becomes the label; attributes map to the `items` fields: `value` / `disabled` / `loading` / `icon` / `kind` / `danger` / `href` / `target` / `rel`. Nested elements inside an `<oas-dropdown-item>` become cascading submenus recursively; an `<oas-dropdown-group>`'s `label` is the group title (`value` can act as a radio-group id) and its children flatten to the same level. Adding/removing children or changing their attributes/text re-renders automatically (MutationObserver).

<DemoBlock title="Declarative children (group / divider / nested / checkbox / danger / href)">
  <oas-dropdown id="dd-decl" onoas-select="ddDeclLog(event)" placement="bottom-start">
    <oas-button type="primary">Actions</oas-button>
    <oas-dropdown-group label="Navigation">
      <oas-dropdown-item value="home">Home</oas-dropdown-item>
      <oas-dropdown-item value="docs" href="/components/" target="_blank" rel="noopener">Component docs</oas-dropdown-item>
    </oas-dropdown-group>
    <oas-dropdown-divider></oas-dropdown-divider>
    <oas-dropdown-item value="edit">Edit
      <oas-dropdown-item value="copy">Copy</oas-dropdown-item>
      <oas-dropdown-item value="cut">Cut</oas-dropdown-item>
    </oas-dropdown-item>
    <oas-dropdown-item value="grid" kind="checkbox">Show gridlines</oas-dropdown-item>
    <oas-dropdown-divider></oas-dropdown-divider>
    <oas-dropdown-item value="delete" danger>Delete</oas-dropdown-item>
  </oas-dropdown>
  <oas-tag id="dd-decl-result" type="info">Nothing selected</oas-tag>
</DemoBlock>

<DemoBlock title="Dynamic add/remove (MutationObserver auto refresh)">
  <oas-space size="small">
    <oas-button size="small" onclick="ddDeclAdd()">Append an item</oas-button>
  </oas-space>
  <oas-dropdown id="dd-decl-dyn" placement="bottom-start">
    <oas-button>Dynamic menu</oas-button>
    <oas-dropdown-item value="home">Home</oas-dropdown-item>
    <oas-dropdown-item value="settings">Settings</oas-dropdown-item>
  </oas-dropdown>
</DemoBlock>

### Leading media (slot="leading")

Menu items support a leading media slot: put a `<template slot="leading">` (or a direct child with `slot="leading"`) inside an `<oas-dropdown-item>` (shared with `<oas-menu-item>`); the content renders before the label and takes precedence over the `icon` attribute (when both are given the icon is not rendered); in the `loading` state the spinner replaces the leading media. Suited to avatars, color chips, and any rich leading content (`items` JSON does not support it — declarative children only).

<DemoBlock title="Leading avatars (avatar example)">
  <oas-dropdown id="dd-leading" placement="bottom-start">
    <oas-button>Switch account</oas-button>
    <oas-dropdown-item value="lin">Lin Xiaoyu<template slot="leading"><oas-avatar size="small">Lin</oas-avatar></template></oas-dropdown-item>
    <oas-dropdown-item value="chen">Chen Yining<template slot="leading"><oas-avatar size="small" color="success">Chen</oas-avatar></template></oas-dropdown-item>
    <oas-dropdown-item value="zhao">Zhao Qiming<template slot="leading"><oas-avatar size="small" color="warning">Zhao</oas-avatar></template></oas-dropdown-item>
    <oas-dropdown-divider></oas-dropdown-divider>
    <oas-dropdown-item value="logout" icon="close">Sign out</oas-dropdown-item>
  </oas-dropdown>
</DemoBlock>

<script setup>
import { onMounted } from 'vue'
onMounted(() => {
  window.ddLog = (e) => {
    const tag = document.getElementById('dd-result')
    if (tag) tag.textContent = `Selected: ${e.detail.value}`
  }

  // reserve-width demo: show the value and trigger width after switching (compare the two buttons)
  const reserveOut = document.getElementById('dd-reserve-out')
  const reserveEl = document.getElementById('dd-reserve')
  if (reserveEl && reserveOut) {
    reserveEl.addEventListener('oas-select', (e) => {
      reserveOut.textContent = `Selected: ${e.detail.value} (trigger width ${Math.round(reserveEl.getBoundingClientRect().width)}px, no jitter)`
    })
  }

  const ctrl = document.getElementById('dd-ctrl')
  const openStatus = document.getElementById('dd-open-status')
  if (ctrl && openStatus) {
    const syncOpen = () => {
      openStatus.textContent = `open: ${ctrl.hasAttribute('open')}`
    }
    window.ddOpen = (open) => {
      if (open) ctrl.setAttribute('open', '')
      else ctrl.removeAttribute('open')
    }
    syncOpen
    // Clicking outside / Esc / selecting makes the component remove open; keep status synced with MutationObserver
    new MutationObserver(syncOpen).observe(ctrl, { attributes: true, attributeFilter: ['open'] })
  }

  const val = document.getElementById('dd-value')
  const valueStatus = document.getElementById('dd-value-status')
  if (val && valueStatus) {
    const syncValue = () => {
      valueStatus.textContent = `value: ${val.getAttribute('value') || '-'}`
    }
    window.ddValue = (v) => {
      if (v) val.setAttribute('value', v)
      else val.removeAttribute('value')
    }
    window.ddValueLog = (e) => {
      val.setAttribute('value', e.detail.value)
    }
    syncValue
    // Selecting a menu item updates value in the component; keep status synced with MutationObserver
    new MutationObserver(syncValue).observe(val, { attributes: true, attributeFilter: ['value'] })
  }

  window.ddSplitAction = (e) => {
    const tag = document.getElementById('dd-split-result')
    if (tag) tag.textContent = `Main button clicked (${e.type})`
  }

  const keepDd = document.getElementById('dd-keep')
  const keepResult = document.getElementById('dd-keep-result')
  if (keepDd && keepResult) {
    keepDd.addEventListener('oas-select', (e) => {
      keepResult.textContent = `Selected: ${e.detail.value} (menu stays open)`
    })
  }

  const openChangeDd = document.getElementById('dd-open-change')
  const openChangeStatus = document.getElementById('dd-open-change-status')
  if (openChangeDd && openChangeStatus) {
    window.ddOpenChange = (e) => {
      openChangeStatus.textContent = `open: ${e.detail.open}`
    }
  }

  const asyncDd = document.getElementById('dd-async')
  const asyncStatus = document.getElementById('dd-async-status')
  if (asyncDd && asyncStatus) {
    window.ddAsyncLog = (e) => {
      asyncStatus.textContent = `Selected: ${e.detail.value}`
      if (e.detail.value !== 'save') return
      // Simulate async work: "Save" spins for ~1.5s, then recovers (menu stays open to observe)
      const mark = (loading) => {
        const items = JSON.parse(asyncDd.getAttribute('items') || '[]')
        const target = items.find((i) => i.value === 'save')
        if (!target) return
        if (loading) {
          target.loading = true
          asyncDd.setAttribute('items', JSON.stringify(items))
          // The component closes the menu right after forwarding the select event;
          // reopen on the next frame so the loading state is visible
          window.setTimeout(() => asyncDd.setAttribute('open', ''), 0)
        } else {
          delete target.loading
          asyncDd.setAttribute('items', JSON.stringify(items))
        }
      }
      mark(true)
      window.setTimeout(() => mark(false), 1500)
    }
  }

  // Declarative child-element channel: show selection + append an item at runtime (MutationObserver re-renders)
  window.ddDeclLog = (e) => {
    const tag = document.getElementById('dd-decl-result')
    if (tag) tag.textContent = `Selected: ${e.detail.value} (declarative child channel)`
  }
  const declDyn = document.getElementById('dd-decl-dyn')
  if (declDyn) {
    window.ddDeclAdd = () => {
      const n = declDyn.querySelectorAll('oas-dropdown-item').length
      const item = document.createElement('oas-dropdown-item')
      item.setAttribute('value', `extra-${n}`)
      item.textContent = `Dynamic item ${n}`
      declDyn.appendChild(item)
    }
  }
})
</script>

## Trigger size and type

`size` (five tiers xs / small / medium / large / xl, `sm/md/lg` aliases accepted) and `type` (aligned with the oas-button type vocabulary) are **forwarded to the trigger `oas-button`** (a direct child). Dimensions not set on the dropdown keep the trigger button's own attributes; forwarding silently no-ops when the trigger is not an `oas-button`. Invalid `size` falls back to medium with a one-time warning.

<DemoBlock title="size passthrough (five tiers)">
  <oas-space size="small">
    <oas-dropdown size="xs" items='[{"label":"Edit","value":"edit"}]'><oas-button>xs</oas-button></oas-dropdown>
    <oas-dropdown size="small" items='[{"label":"Edit","value":"edit"}]'><oas-button>small</oas-button></oas-dropdown>
    <oas-dropdown size="medium" items='[{"label":"Edit","value":"edit"}]'><oas-button>medium</oas-button></oas-dropdown>
    <oas-dropdown size="large" items='[{"label":"Edit","value":"edit"}]'><oas-button>large</oas-button></oas-dropdown>
    <oas-dropdown size="xl" items='[{"label":"Edit","value":"edit"}]'><oas-button>xl</oas-button></oas-dropdown>
  </oas-space>
</DemoBlock>

<DemoBlock title="type passthrough (aligned with button type)">
  <oas-space size="small">
    <oas-dropdown type="primary" items='[{"label":"Edit","value":"edit"}]'><oas-button>primary trigger</oas-button></oas-dropdown>
    <oas-dropdown type="danger" items='[{"label":"Edit","value":"edit"}]'><oas-button>danger trigger</oas-button></oas-dropdown>
  </oas-space>
</DemoBlock>

## Panel max height

`max-height` caps the menu panel height (numbers are px; forwarded to the inner `oas-menu` channel) — the panel scrolls internally, for long menus.

<DemoBlock title="max-height=120 (panel scrolls internally)">
  <oas-dropdown max-height="120" items='[{"label":"Home","value":"home"},{"label":"Products","value":"products"},{"label":"Solutions","value":"solutions"},{"label":"Docs","value":"docs"},{"label":"Blog","value":"blog"},{"label":"About us","value":"about"},{"label":"Join us","value":"jobs"},{"label":"Contact","value":"contact"}]'>
    <oas-button>Long menu (120px cap)</oas-button>
  </oas-dropdown>
</DemoBlock>

## Reserve Widest Option (reserve-width)

<DemoBlock title="reserve-width: no jitter when switching (open the dropdowns to compare the two buttons)">
  <div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
    <oas-dropdown id="dd-reserve" reserve-width value="a" items='[{"label":"Align left","value":"a"},{"label":"Align center","value":"b"},{"label":"Justified with auto wrap","value":"c"}]'>
      <oas-button>Alignment</oas-button>
    </oas-dropdown>
    <oas-dropdown id="dd-narrow" value="a" items='[{"label":"Align left","value":"a"},{"label":"Align center","value":"b"},{"label":"Justified with auto wrap","value":"c"}]'>
      <oas-button>Control (no reserve)</oas-button>
    </oas-dropdown>
    <span id="dd-reserve-out" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)"></span>
  </div>
</DemoBlock>

`reserve-width` (opt-in) measures the max text width of all menu item labels and writes it to the trigger `min-width`: the trigger never jitters when switching values (high-frequency toolbar/filter switching). Both the `items` attribute and the declarative child channel participate; content changes re-measure automatically; removing the attribute restores the trigger width. The measurement is an approximation (trigger chrome margin tunable via the `--oas-dropdown-reserve-pad` variable, default 40px). The first button above has the reserve on (sized by the widest of the three options), the second is the control; open the menus and switch values to compare.

## API

### oas-dropdown

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `arrow` | Whether to show the arrow pointing at the trigger (`arrow="false"` hides it; the skeleton is kept) | `string` | `true` |
| `arrow-point-at-center` | Pin the arrow to the panel center (by default the arrow follows the trigger's projection, so it still points at the trigger even after viewport-avoidance shifting) | `boolean` | — |
| `auto-adjust-overflow` | Auto-flip/avoid when there is not enough viewport space (`auto-adjust-overflow="false"` disables it; the panel may overflow the viewport) | `string` | `true` |
| `close-on-scroll` | Close the popover on page scroll (default repositions to follow; true closes on scroll) | `boolean` | — |
| `disabled` | Disable entirely: no trigger response | `boolean` | — |
| `hide-on-click` | Close after selecting an item (default true; false keeps it open for multi-select) | `string` | `true` |
| `hover-delay` | Open delay in ms on hover trigger (default 150) | — | — |
| `hover-hide-delay` | Close delay in ms on hover trigger (default 100) | — | — |
| `items` | Menu items JSON | `string` | `[]` |
| `max-height` | Caps the menu panel height with internal scrolling (numbers get px) | `string` | — |
| `offset` | Gap in px between popover and trigger (default 8) | — | — |
| `open` | Controlled display (boolean attribute; expands when present) | `boolean` | — |
| `placement` | Popup placement | `string` | `bottom` |
| `reserve-width` | Reserve the widest option width on the trigger (opt-in): measures the max text width of all menu item labels and writes it to the trigger min-width so switching values never jitters; re-measured automatically when items/children change; removing the attribute restores the trigger width | `boolean` | — |
| `size` | Forwarded to the trigger oas-button size tiers (aliases normalized, invalid falls back to medium) | `string` | — |
| `split` | Split button mode (boolean attribute): main button + arrow button; arrow opens the menu, main button fires oas-action | `boolean` | — |
| `trigger` | Trigger: `click` (default) / `hover` / `focus`; space-separated for multiple (e.g. `"click hover"`). With `hover`, touch devices (coarse pointer) automatically degrade to tap toggling: tap to open, tap again or tap outside to close; desktop hover behavior unchanged | `string` | `click` |
| `type` | Forwarded to the trigger oas-button type vocabulary | `string` | — |
| `value` | Current selected value | `string` | — |

#### Events

| Event | Description |
| --- | --- |
| `oas-action` | Main button clicked in split mode, `detail: { originalEvent }` |
| `oas-open-change` | Popover open state changed, `detail: { open: boolean }` (including external setAttribute; controlled loop) |
| `oas-select` | An item was selected, `detail: { value }` |

#### Slots

| Name | Description |
| --- | --- |
| default | Dropdown menu content (items / groups) |

#### CSS Variables

| CSS Variable | Default |
| --- | --- |
| `--oas-dropdown-arrow-height` | `12px` |
| `--oas-dropdown-arrow-radius` | `0` |
| `--oas-dropdown-arrow-width` | `12px` |
| `--oas-glass-blur` | `none` |
| `--oas-glass-ring` | `transparent` |
| `--oas-origin-x` | `center` |
| `--oas-origin-y` | `center` |

### oas-dropdown-item

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `danger` | Destructive item: red semantics (delete/exit etc.) | — | — |
| `disabled` | Disable this item | — | — |
| `href` | Link address: with href the item renders as a native `<a>` (real navigation + still emits `oas-select`) | — | — |
| `icon` | Leading icon (icon name from the `@oas-ui/icons` registry) | — | — |
| `icon-color` | Icon color: fixes the icon to this color (overrides the selected/disabled default); defaults to currentColor following the text color (only effective via the items JSON channel; the child-element channel does not parse this attribute) | — | — |
| `kind` | Leaf semantics: `radio` (default, selectable) / `action` (no checked state, does not write back value) / `checkbox` (multi-select, value is the checked-set array) | — | — |
| `loading` | Loading: renders a spinner and blocks clicks; recovers when data-driven | — | — |
| `rel` | Link rel (with href) | — | — |
| `shortcut` | Shortcut hint rendered as a trailing kbd (aligned with the menubar/menu shortcut contract) (only effective via the items JSON channel; the child-element channel does not parse this attribute) | — | — |
| `target` | Link target (with href) | — | — |
| `value` | Selected value (data-carrier field of the declarative child-element channel) | — | — |

#### Slots

| Name | Description |
| --- | --- |
| default | Dropdown item label content (default slot text); direct child `<oas-dropdown-item>` elements recursively become the submenu `children` |
| `template[slot="leading"]` | Leading media template (or a direct child with slot="leading"): avatars/color chips/any rich leading content, rendered before the label and taking precedence over the icon attribute; replaced by the spinner in the loading state; declarative child channel only (not supported by the items JSON) |

### oas-dropdown-group

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `label` | Group title (small, secondary color, not clickable) | — | — |
| `value` | Radio-group id (selecting inside the group only updates that group's value) | — | — |

#### Slots

| Name | Description |
| --- | --- |
| default | Group items: child `<oas-dropdown-item>` elements flatten to the same level |

### oas-dropdown-divider

#### Slots

| Name | Description |
| --- | --- |
| default | Divider data carrier (no attributes; the host parses it as `type: "divider"`) |

Clicking the trigger toggles visibility; clicking outside / pressing Esc / selecting an item closes it. The floating menu is an inner `oas-menu` (`role="menu"`, leaf items `menuitemradio`, items with submenus `menuitem`) supporting cascading submenus and keyboard navigation.
