Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Select

Select lets someone pick one or more values from a list of options, shown in a dropdown instead of laid out on screen all at once. It can filter that list live as the user types, let several options be picked in one visit, or hand the whole dropdown over to the browser so it looks and behaves like the device's own native picker.

Dependencies

eun-icon · for the leading icon, or the checkmark on each option while multiple eun-label · if it has a label eun-loader · for the spinning indicator, while loading is set
Overview API Examples Accessibility

When to use

Reach for select when someone is picking from a closed list of options you already know, such as a country, a category, or a status, especially once that list is too long to show as plain radio buttons (past around 5 options). For a short list where seeing every choice at a glance matters more than saving space, radio (or checkbox for multiple) is usually the friendlier control.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/select.js";
import "@eunomia/elements/label.js";
<eun-select label="Country">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
</eun-select>

Importing the file registers <eun-select> as a custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component.

Alternatives

You want .. Prefers A short, fixed list where seeing every choice at a glance matters more than saving space eun-radio The same, but for picking more than one option eun-checkbox

Guidance

  • Always set label (or slot one): see the Accessibility tab
  • Use searchable once a list is long enough that typing to filter beats scanning, since for short, fixed lists the plain trigger is faster
  • Group related options with optgroup once a list mixes categories
  • Reserve readonly-available for genuinely inline-editable data, not every field in a long form
  • Reach for native on touch devices or long, plain lists where the OS's own picker (native scrolling, platform-specific gestures) beats a custom listbox
  • Set confirm-label to something clearer than the default "Confirm" when the context calls for it (e.g. "Done", "Apply"), since the button itself always shows while multiple and open: it's the only on-screen "done picking" control, and everything else (Escape, outside click, focus leaving the field) is undiscoverable without it
  • Using a select for fewer than ~5 options, since visible radio (or checkbox, once multiple) buttons remove a click and let users compare choices at a glance
  • Relying on option order alone to convey grouping: use optgroup instead
  • Overriding colors with inline styles instead of the --field-* CSS variables
  • Combining native with searchable or multiple, since a native dropdown can't filter or show this component's own checkbox-per-option UI, so native always wins and the other is silently ignored

Live testing

Properties

Attributes

NameTypeDefaultDescription
default-valuestring | string[]The default values applied when the field connects, and restored on form reset
search-placeholderstring'Search…'The fallback for placeholder when searchable is set
select-placeholderstring'Select…'The fallback for placeholder when searchable isn't set
readonly-availablebooleanfalseWhether the field can switch between a static display and an editable one
confirm-labelstring'Confirm'The label of the confirm button, shown while editing in readonlyAvailable mode or while multiple and the dropdown is open. Set to an empty string to hide the button entirely
no-animationbooleanfalseDisables the dropdown panel's open and close height transition, on top of the automatic reduced motion handling
valuestring | string[]The selected option's value, or every picked option's value while multiple
searchablebooleanfalseWhether the field filters its options as the user types. When false, the field is read only and driven by arrow keys, Enter, and type-ahead. Ignored while native is set
nativebooleanfalseRenders the field as a real select, using the browser's own dropdown instead of the listbox panel. Takes priority over searchable and multiple, neither of which a native dropdown can support
multiplebooleanfalseWhether more than one option can be picked at once. Each option then renders a checkbox, and picking one toggles it without closing the dropdown, so value becomes an array of every picked option's value. Ignored while native is set
iconEunomiaIconNameAn optional leading icon
placeholderstringThe placeholder shown when the field is empty. Defaults to searchPlaceholder when searchable, and selectPlaceholder otherwise
empty-value-labelstring'No value set'Text displayed in place of the value when readonly and empty
no-results-labelstring'No results'Text displayed in the dropdown when no option matches the search
loadingbooleanfalseWhether options are currently being fetched. Shows a spinning loading indicator in place of the dropdown arrow, and a single disabled loading row instead of the option list. Has no effect while native, beyond the arrow swap
loading-labelstring'Loading…'The accessible label announced by the loading indicator while loading is set

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
variant'outline' | 'fill' | 'underline''outline'The visual variant to apply to the field
labelstringThe title label of the field, rendered through the label component
instructionsstringInstructions displayed below the label
namestringThe field name
disabledbooleanfalseWhether the field is disabled
readonlybooleanfalseWhether the field is read only
requiredbooleanfalseWhether a selection is required
hideErrorbooleanfalseWhether the errors are hidden
validatorsArray<Validators<string | string[]>>The list of validation rules applied to the field value
readonlyAvailablebooleanfalseWhether the field can switch between a static display and an editable one

Slots

NameDescription
(default)The option and optgroup elements defining the choices
iconReplaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty
readonly-contentOptional custom content displayed instead of the raw label in the static readonly display
confirmOptional custom content for the confirm trigger, such as an icon, displayed as a link while editing in readonlyAvailable mode, or while multiple and the dropdown is open. Overrides confirmLabel when used. The button itself always shows in either of those states unless confirmLabel is set to an empty string

Events

NameTypeDescription
eunchangeChangeEventFired when an option is selected
euncommitCommitEventFired when picking an option confirms an edit in readonlyAvailable mode

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--field-backgroundSets the background color of the field
--field-border-colorSets the border color of the field
--field-border-color-hoverSets the border color on hover
--field-border-color-focusSets the border color on focus
--field-border-radiusSets the corner radius of the field
--field-paddingSets the padding of the field
--field-text-colorSets the text color of the field
--field-placeholder-colorSets the color of the placeholder
--field-icon-colorSets the color of the leading icon and the dropdown arrow
--field-error-colorSets the color of the error message
--field-hint-colorSets the color of the hint message
--field-focus-outline-colorSets the color of the focus outline
--field-match-colorSets the color of the matched search substring, highlighted while searchable
--field-match-backgroundSets the background of the matched search substring, none by default
--field-match-font-weightSets the font weight of the matched search substring
--field-match-text-decorationSets the text decoration, such as underline, of the matched search substring, none by default
--field-confirm-link-colorSets the color of the confirm slot's link, while editing in readonlyAvailable mode
--field-confirm-link-color-hoverSets the hover color of the confirm slot's link
--field-listbox-max-heightSets the maximum height of the dropdown panel, past which it scrolls
--field-listbox-transition-durationSets the duration of the dropdown panel's open and close height transition, disabled entirely by the no-animation attribute or reduced motion settings
--eun-viewport-inset-topReserves space, such as a page's sticky header height, that the dropdown panel keeps clear of at the top of the viewport. Set on the root or an ancestor rather than on the field itself
--eun-viewport-inset-bottomThe same as eun-viewport-inset-top, reserved at the bottom of the viewport, such as for a sticky footer

Basic

<eun-select label="Country" placeholder="Select a country">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
  <option value="ch">Switzerland</option>
</eun-select>

Searchable

Filters the list live as the user types, instead of only navigating it with Arrow keys and type-ahead.

<eun-select searchable label="Country" placeholder="Search a country">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
</eun-select>

Multiple

Each option renders a checkbox, and picking one toggles it without closing the dropdown, so several can be picked in one visit. value becomes a string[] of every picked option's value, and the field itself shows every picked label, joined with a comma.

<eun-select multiple label="Countries" placeholder="Select countries">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
  <option value="ch">Switzerland</option>
</eun-select>

Read the selection back as a plain array:

const select = document.querySelector("eun-select");
select.addEventListener("eunchange", () => console.log(select.value)); // e.g. ["fr", "ch"]

Pre-checked by default

default-value accepts a comma-separated list of values while multiple, since each matching option starts checked, exactly like value itself would, but only takes effect on connection and is restored on a native form reset() (value isn't). Both accept an array directly too when set from JS (select.defaultValue = ["fr", "ch"]), since the comma-separated form is only for the HTML attribute, which can't hold an array natively.

<eun-select multiple label="Countries" default-value="fr,ch">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
  <option value="ch">Switzerland</option>
</eun-select>

Closing the dropdown

Since picking an option no longer closes it, a confirm button, labeled "Confirm" by default, shows automatically once the dropdown is open, giving a discoverable, on-screen "done picking" control. Escape, an outside click, and focus leaving the field entirely (e.g. Tab) all close it too, though none of those are visible affordances on their own. Set confirm-label for wording that fits the context better:

<eun-select multiple label="Countries" confirm-label="Done">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
</eun-select>

Slot content into confirm instead for anything richer than plain text (an icon, for instance), since it overrides confirm-label when present. Set confirm-label="" (and don't slot anything) to hide the button entirely, though that leaves multiple with no on-screen way to close the dropdown at all: see the Accessibility tab.

Combined with searchable

multiple and searchable compose: typing filters the list exactly as it already does, and the field keeps whatever's been typed instead of jumping back to the picked labels while the dropdown stays open, so several options can be filtered-and-picked in a row before it closes.

Variants

variant selects the visual style: outline (default, bordered box), fill (filled background, no border), or underline (bottom border only).

<eun-select label="Fill" variant="fill">
  <option value="fr">France</option>
</eun-select>

Part of the shared vocabulary covered in Variants, alongside every other component that reuses it.

With a leading icon

<eun-select icon="public" label="Country">
  <option value="fr">France</option>
</eun-select>

Need something other than a eun-icon, or a different icon library entirely? Slot your own content into icon instead. It replaces the prop-driven icon entirely.

<eun-select label="Country">
  <svg
    slot="icon"
    width="16"
    height="16"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
  >
    <circle cx="12" cy="12" r="10"></circle>
    <path
      d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10Z"
    ></path>
  </svg>
  <option value="fr">France</option>
</eun-select>

Grouped options

<eun-select label="Country">
  <optgroup label="Europe">
    <option value="fr">France</option>
    <option value="be">Belgium</option>
  </optgroup>
  <optgroup label="Other">
    <option value="ca">Canada</option>
  </optgroup>
</eun-select>

Native

Renders a real <select> instead of the listbox, handing the dropdown over to the browser/OS. Takes precedence over searchable and multiple, since a native dropdown has no way to filter its options as the user types, and its own multi-select UI (platform-specific, e.g. ctrl/cmd-click) is entirely separate from this component's checkbox-per-option one.

<eun-select native label="Country">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
</eun-select>

States

<eun-select label="Disabled" disabled>...</eun-select>
<eun-select label="Readonly" readonly value="fr">...</eun-select>
<eun-select label="Required" required hint="This field is mandatory"
  >...</eun-select
>

readonly and disabled are not interchangeable: a readonly field still submits its value and stays focusable, a disabled one does neither. See the Accessibility tab.

Loading

Set loading while options are being fetched, for example a searchable field querying a server for matches as the user types. A spinning eun-loader replaces the dropdown arrow, and the listbox shows a single "Loading…" row instead of stale/no-results content. The field itself stays editable, so the user can keep refining the query while a previous request is still in flight.

<eun-select label="Country" searchable loading placeholder="Search a country">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
</eun-select>
import "@eunomia/elements/loader.js";

eun-loader is used internally, but its module must still be imported alongside eun-select for the spinner to render, exactly like eun-icon for the leading icon above.

Debouncing a real request

Same idea as eun-search's own "Debouncing a real request" example (its Loading section): wrap whatever triggers the fetch in debounce, exported from @eunomia/elements, so a fast typist doesn't fire one request per keystroke, and toggle loading around it.

import { debounce } from "@eunomia/elements";

const select = document.querySelector("eun-select");

const runSearch = debounce(async (query: string) => {
  select.loading = true;
  try {
    const countries = await fetchCountries(query); // your own API call
    select.replaceChildren(
      ...countries.map((country) => {
        const option = document.createElement("option");
        option.value = country.code;
        option.textContent = country.name;
        return option;
      }),
    );
  } finally {
    select.loading = false;
  }
}, 300);

eun-select doesn't (yet) expose a public event fired as the user types while searchable is set. The only event it fires is eunchange, once an option is picked. Wire runSearch to whatever your own app already uses to read the typed value (a wrapping form's own input handling, a signal/store update, etc.).

Click to edit

Set readonly-available to let a field switch between a static, clickable display of its current value and a real editable select, instead of always being one or the other. Save/Cancel step, since choosing a result from an already-open list is itself the confirming gesture. Cancelling means dismissing the dropdown (Escape or an outside click) without picking anything.

That leaves no way to confirm having kept the current value without picking a different one first, so a confirm button, labeled "Confirm" by default, shows automatically while editing for exactly that case:

<eun-select readonly-available label="Country" value="fr">
  <option value="fr">France</option>
  <option value="be">Belgium</option>
</eun-select>
document
  .querySelector("eun-select[readonly-available]")
  .addEventListener("euncommit", (event) => save(event.target.value));

Customizing or hiding the confirm button

Set confirm-label for wording that fits the context better, or to an empty string to remove the button entirely, for example when picking an option already feels like enough confirmation on its own for that particular field, and the extra button would just be clutter:

<eun-select
  readonly-available
  label="Country"
  value="fr"
  confirm-label="Confirm"
>
  <option value="fr">France</option>
  <option value="be">Belgium</option>
</eun-select>

Slot content into confirm instead for anything richer than plain text (an icon, for instance), since it overrides confirm-label when present.

Theming

Colors come from the active theme's --eun-* tokens (see Theming). For one-off overrides, target the component's own --field-* custom properties instead (full list in the API tab), shared with eun-input and future text-like field components.

<eun-select
  label="Custom"
  style="
    --field-border-color: #b45309;
    --field-border-color-hover: #92400e;
    --field-border-color-focus: #92400e;
    --field-border-radius: 2px;
  "
>
  <option value="fr">France</option>
</eun-select>

Dropdown panel height

--field-listbox-max-height (default 260px) caps the dropdown before it scrolls internally:

<eun-select label="Country" style="--field-listbox-max-height: 160px">
  ...
</eun-select>

Reserved viewport space

A page with its own sticky/fixed chrome, most commonly a header, should declare --eun-viewport-inset-top (and --eun-viewport-inset-bottom for a sticky footer) on :root, matching that chrome's height. The dropdown renders in the browser's top layer, above everything else on the page including that chrome by default, so without this it can render straight over it whenever there isn't enough room on its preferred side. With the inset declared, the panel stops short of the reserved band instead. This site's own sticky header is set up exactly this way, in main.css:

:root {
  --eun-viewport-inset-top: 70px; /* matches this site's sticky header */
}

This is read once per reposition, inherited like any custom property, so no per-field configuration is needed once it's set at the page level.

Open and close animation

The panel's height transitions smoothly open and closed by default. --field-listbox-transition-duration (default 200ms) controls how long that takes:

<eun-select label="Country" style="--field-listbox-transition-duration: 350ms">
  ...
</eun-select>

Set no-animation to disable it: the panel then shows/hides instantly, same as it already does automatically whenever the browser reports prefers-reduced-motion: reduce:

<eun-select no-animation label="Country">...</eun-select>

This audit covers every mode the field can render in: the searchable and non-searchable listbox, multiple, native, and readonly-available, since each changes what's on screen and, in some cases, what accessibility tree gets built.

Keyboard interactions

When focus is on the field (native unset):

Key Action
Tab / Shift+Tab Moves focus in/out of the field (single tab stop)
ArrowDown Opens the dropdown if closed and highlights the next option, in the same press, matching how a native <select> immediately shows a highlighted option on the first Arrow press rather than requiring an initial press just to open
ArrowUp Opens the dropdown if closed and highlights the previous option, same as ArrowDown
Home / End, while not searchable Opens the dropdown if closed and jumps the highlight to the first/last option. Left to the browser while searchable, where they're the native "move the caret" shortcuts instead
Enter Selects the highlighted option, or, while multiple, toggles it and leaves the dropdown open instead
Escape Closes the dropdown (native Popover API light-dismiss)
Any character key, while searchable Filters the list live
Any character key, while not searchable Type-ahead: jumps the highlight to the next option starting with the typed text, same as a native <select>. Several characters typed in quick succession extend the match, and a pause of around 600ms resets it

While native is set, the field is a real <select>: all keyboard interaction (Arrow keys, type-ahead, Home/End, opening/closing) comes from the browser/OS for free, not from this component, including its own native multi-select interaction, entirely unrelated to this component's own multiple (see the Examples tab's "Native" section).

On the read-only click-to-edit display (readonly-available, not editing):

Key Action
Enter Enters edit mode
Space Enters edit mode

While editing (or while multiple and open), the confirm button is a real <button> reachable by Tab like any other focusable element, activated with Enter/Space, shown by default (confirmLabel, "Confirm") unless explicitly hidden by setting confirm-label="" with no confirm slot used either.

Dismissal behavior

The dropdown closes on: Escape, a click outside it (both native Popover API light-dismiss), picking an option, and the browser window itself losing focus, for example Alt-Tabbing to another application. That last one isn't something the Popover API handles on its own: light-dismiss only reacts to clicks and Escape, neither of which fires on a window-level blur. Without explicitly listening for it, switching away and back would find the dropdown exactly as it was left, open on top of whatever's now underneath it, which reads as broken far more than it reads as "still mid-selection." Closing on blur matches how native OS menus and <select> pickers already behave.

It also closes if the field itself scrolls behind a reserved --eun-viewport-inset-top/-bottom band while open (see the next section), since once the field a screen-reader or sighted keyboard user was just interacting with is no longer visibly there, there's nothing left for the dropdown to be meaningfully anchored to, so it's closed rather than left floating disconnected from it.

While multiple, picking an option no longer closes the dropdown, since several are meant to be picked in one visit, so two more ways to close it exist specifically for that mode, on top of Escape/outside-click/window-blur above:

  • Clicking the confirm button (see the Examples tab), the only on-screen, discoverable "done picking" control while multiple, shown by default
  • Focus leaving the field, the dropdown, and the confirm link entirely, for example Tab-ing to whatever comes next in the page. The Popover API's own light-dismiss doesn't cover this on its own either: it reacts to clicks outside the popover, not to focus moving there via the keyboard alone, so without this, tabbing straight past the field would leave the dropdown open and detached from the no-longer-focused field. Checked via :focus-within a frame after every focusout, so moving focus to the confirm link itself (still inside the field's own wrapper) doesn't trigger a false close

Motion

The panel's open/close height transition is skipped automatically whenever the browser reports prefers-reduced-motion: reduce, on top of the explicit no-animation attribute/property covered in the Examples tab. Either way, the panel just shows/hides instantly instead.

Focus indicator

The field renders a :focus-visible outline (--field-focus-outline-color, falling back to --eun-color-primary-500) that switches to --field-error-color while invalid, exactly like eun-input. The confirm link gets the same outline treatment when it's present and focused.

Aria attributes

This component follows the WAI-ARIA APG combobox pattern (search: "Editable Combobox With List Autocomplete" while searchable, "Select-Only Combobox" otherwise): role="combobox" on the field, paired with role="listbox" on the dropdown panel and role="option" on each choice. The combobox-specific roles and states below don't apply while native: a real <select> already has its own complete, browser-implemented accessibility tree (role, states, and keyboard handling), so none of that is layered on top of it. aria-invalid, aria-required, and aria-describedby still are, though (see below), since those describe the field's state, not the combobox interaction pattern, and apply just as much to a native <select>.

  • role="combobox" / aria-expanded="true"\|"false": reflects isOpen
  • aria-controls="listbox": points the field at the dropdown panel
  • aria-autocomplete="list" while searchable, "none" otherwise: tells assistive technology whether typing filters the list or not
  • aria-activedescendant: set to the highlighted option's id while one is highlighted, so a screen reader announces it without moving DOM focus off the field
  • aria-multiselectable="true"\|"false" on the listbox: reflects multiple
  • role="option" / aria-selected="true"\|"false" on each choice: this alone communicates picked/not-picked while multiple, toggling on every pick without the dropdown closing, and a screen reader announces the updated selection state on each one, the same way it would for a native multi-select <select>
  • role="presentation" on group-label rows (from optgroup) and on the "no results" row: decorative/informational only, never focusable or selectable, and not valid option/group children of a listbox either
  • aria-required="true"\|"false" (listbox modes) / native required (native mode): kept in sync with the required property either way, so assistive technology announces it consistently regardless of mode
  • aria-invalid="true"\|"false": kept in sync with valid, in every mode including native
  • aria-label: set from the label property directly on the field and on the readonly-available static display. Not aria-labelledby pointing at the rendered <eun-label>: eun-label renders its text inside its own shadow root, a different shadow tree than this field, and ID references don't cross shadow-root boundaries in current browsers. Computing a plain-text aria-label from the same label property sidesteps that entirely: see eun-input's Accessibility tab for the full reasoning, identical here
  • aria-describedby="description": set on the field whenever a hint or error message is actually rendered below it, so the message is announced together with the field, in every mode including native

The option checkbox is decorative, not a second interactive widget

Each option renders a small checkbox-shaped indicator while multiple, but it's purely visual (aria-hidden, no role, no tabindex), and it isn't a eun-checkbox, deliberately so. Nesting a genuinely interactive, focusable, independently-role="checkbox" element inside a role="option" row would give assistive technology two overlapping interactive things to reconcile per row, which the WAI-ARIA Listbox pattern doesn't define and no screen reader handles consistently, since an option is meant to be one atomic, selectable unit, not a container for another widget. The row's own role="option" / aria-selected above is already the complete, correct, spec-compliant way to expose "picked or not" for a multi-selectable listbox option (mirroring how a native <select multiple> needs no nested control either), and the checkbox glyph is only there so sighted users get the same affordance visually.

Dropdown panel: rendering, positioning, and sizing

The dropdown (popover="auto") renders in the browser's top layer: it's never clipped by an ancestor's overflow: hidden or out-ranked by a stacking context, and closes automatically on an outside click or Escape with no manual document-level listener to maintain.

Its position tracks the field on every scroll and resize, flipping above the field instead of below whenever there isn't enough room underneath, same as a native <select>'s own dropdown, so the panel is never left truncated or hanging off the edge of the viewport for the sighted keyboard and screen-magnifier users who rely on that.

Two implementations produce that same result, chosen automatically per browser (AnchorPositionMixin, shared with any future dropdown-style component): CSS Anchor Positioning natively where supported, a JS fallback everywhere else. Neither changes anything described above, since it's purely an implementation/performance detail, except for one thing worth knowing when placing a field on a page with its own fixed or sticky chrome (a header, most commonly): rendering in the top layer means the panel is always visually above that chrome by default, browser z-index rules aside, which can mean covering it. --eun-viewport-inset-top (and --eun-viewport-inset-bottom for a sticky footer), set once on :root, reserve that space so the panel stops short of it instead, as covered in the Theming section. This site's own sticky header is configured exactly this way, which is what keeps the demos throughout this page from ever rendering their dropdown over the top navigation bar.

That reservation covers the panel while the field itself is still on screen. Scroll far enough that the field is now behind that reserved band too, and there's no longer a sensible position left to keep the panel in relative to it, so the dropdown closes instead (see Dismissal behavior above), the same way it does on Escape or an outside click. This is deliberately stricter than waiting for the field to leave the viewport entirely: on browsers using the native CSS Anchor Positioning path, the panel's own clamp against the reserved band is only fully re-applied when its position is computed cold (opening, a resize). During a live, continuous scroll it can otherwise visibly drift over the reserved band for the scroll's duration before catching up. Closing at the inset boundary itself avoids that window.

--field-listbox-max-height (default 260px) caps how tall the panel gets before it scrolls internally. This matters here because a page reserving a lot of top/bottom space with the custom properties above effectively shrinks how much room is left for the panel to use without covering that space, so a smaller max-height keeps it comfortably within whatever's left.

None of this applies while native: the dropdown is the browser/OS's own picker UI, positioned, sized, and kept clear of page chrome by the platform itself.

Label accessibility

Always set label (or slot content into eun-label's label slot), since a select with no accessible name is as disruptive for screen-reader users as an unlabeled text field. placeholder is never a substitute: once a value is picked, it's gone.

Disabled vs. readonly

disabled removes the field from the tab order and excludes its value from form submission entirely. readonly keeps it focusable and its value submitted, and additionally prevents the dropdown from opening at all (there being nothing to change), communicating "you can't change this right now", not "this doesn't apply".

While native, this distinction is implemented differently under the hood: a real <select> has no readonly state at all, so readonly there disables the inner <select> itself (styled to still read as distinctly "readonly", not "disabled": see --field-* theming) and excludes the host from the tab order, exactly like disabled does. From the keyboard user's perspective the outcome is the same either way: a readonly field never traps focus on an inert control, only the implementation differs from listbox mode.

Readonly-available / click-to-edit flow

The static display exposes role="button" and full keyboard access (Tab to reach it, Enter/Space to activate it). Picking an option from the resulting dropdown commits immediately and exits edit mode, so there is no separate confirm step to also activate, unlike a free-text field where a typed value needs an explicit confirmation. To let the user confirm having kept the current value without picking a different one, a confirm button, a real <button> styled as a link, labeled confirmLabel ("Confirm" by default), shows automatically while editing, reachable and activatable like any other button (see Keyboard interactions and Focus indicator above). Set confirm-label="" (with no confirm slot used either) to remove it for a field where that isn't needed.

Reference links

  • WAI-ARIA Authoring Practices: Combobox Pattern
  • WAI-ARIA Authoring Practices: Listbox Pattern
  • WAI Web Accessibility Tutorials: Labeling Controls
  • WHATWG HTML: Popover API
  • CSS Anchor Positioning: Working Draft