SolutionsPackagesComponentsCLI — Coming soonResourcesDocumentationStorybookChangelogBlogAboutSign up freeGet early access

Documentation

Select

Form-associated single-value select with a keyboard-navigable listbox, label, hint, error message, and native validation.

Open this component in Storybook

The <dsgn-select> component picks one value from a list. It behaves like a native <select> inside a <form> — its value reaches FormData, required blocks submission, and form reset and <fieldset disabled> work — while rendering a styled listbox that follows the WAI-ARIA select-only combobox pattern.

Multi-select, search inside the list, option groups, and async options are not supported yet.

Usage

<dsgn-select
  label="Framework adapter"
  name="adapter"
  placeholder="Choose an adapter"
></dsgn-select>
import '@ds-gn/ui/select';

document.querySelector('dsgn-select').options = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'angular', label: 'Angular', disabled: true, tag: 'soon' },
];

In plain HTML, options can also be set as a JSON attribute:

<dsgn-select
  label="Framework adapter"
  options='[{"value":"react","label":"React"},{"value":"vue","label":"Vue"}]'
></dsgn-select>

Playground

Props

PropTypeDefaultDescription
optionsSelectOption[][]The choices, in order. A property, or a JSON attribute in plain HTML. See Options.
valuestring''The selected option’s value, or '' for none. Not reflected; the value at first render is the reset value.
namestring''Field name submitted with the form. Reflected, so a value set only as a property still lands in FormData.
placeholderstring''Shown in the trigger while no option is selected.
labelstring''Label text rendered above the trigger. Omitted when empty.
hintstring''Supporting text below the trigger. Stays visible alongside an error. Omitted when empty.
errorstring''Error message shown below the hint, with an icon. A non-empty value also marks the select invalid.
invalidbooleanfalseMarks the select invalid without a message. Reflected. Set by dsgn-field from its error.
aria-labelstring''Accessible name for the trigger and the listbox, for selects with no visible label.
size'sm' | 'md' | 'lg''md'Control size, shared with dsgn-input: 32, 40, and 48px tall.
disabledbooleanfalseDisables the select.
requiredbooleanfalseReports valueMissing while no option is chosen, and adds a * to the label.
surface'light' | 'dark''light'Semantic token surface to use.

aria-label is the attribute name; the matching JavaScript property is ariaLabel. It is ignored when label is set.

Inside a <dsgn-field>, Field supplies label, hint, error, required, invalid, and size, and overwrites values set on the select directly.

Options

interface SelectOption {
  value: string; // submitted with the form and reported in dsgn-change
  label: string; // visible text, also used for typeahead
  disabled?: boolean; // shown, but not selectable; skipped by the keyboard
  tag?: string; // short trailing note, such as why it is unavailable
}

The type is exported from @ds-gn/ui/select/props.

Slots

The component renders its own label, trigger, listbox, hint, and error message, and takes no slotted content.

CSS parts

PartDescription
labelThe visible <label>.
required-indicatorThe * appended to the label when required. Hidden from assistive technology.
triggerThe combobox button.
listboxThe floating option list.
optionEach option in the list.
hintHint text below the trigger.
errorThe error message, icon and text, below the hint.

Events

EventDetailDescription
dsgn-change{ value, name }Fired when the user picks a different option. Not fired when the same option is picked again or when value is set from code. Bubbles and crosses the shadow boundary.

In React, the generated wrapper exposes this as onDsgnChange.

Methods

MethodDescription
formResetCallback()Called by the platform when the owning form resets. Restores the first-rendered value and clears error.
formDisabledCallback(disabled)Called by the platform when a <fieldset disabled> or form owner toggles the inherited disabled state.
formStateRestoreCallback(state)Called by the platform to restore the value after navigation or autofill.

These are form-association lifecycle callbacks. The browser calls them — you do not.

Keyboard

Focus stays on the trigger the whole time; the highlighted option is tracked with aria-activedescendant.

KeyClosedOpen
↓ / ↑Open on the selected option, or the first enabled one.Move the highlight; stops at the ends and skips disabled options.
Enter / SpaceOpen, as above.Pick the highlighted option and close.
Alt + ↑—Pick the highlighted option and close.
Home / EndOpen on the first / last enabled option.Highlight the first / last enabled option.
A characterOpen and jump to the first match.Jump to the next match. Characters typed within 500ms combine.
Escape—Close without changing the value.
TabMove focus on.Pick the highlighted option, then move focus on.

The pointer works the same way: hovering an option highlights it, clicking picks it, and clicking outside or moving focus away closes the list.

Forms

<form>
  <dsgn-select name="adapter" label="Framework adapter" required></dsgn-select>
  <dsgn-button type="submit">Generate</dsgn-button>
</form>
form.addEventListener('submit', (event) => {
  event.preventDefault();
  const adapter = new FormData(form).get('adapter');
});
  • A required select with no value reports valueMissing, so it blocks submission and the browser’s message points at the trigger.
  • Inherited disabled state (for example from a <fieldset disabled>) is tracked separately from the disabled property, so re-enabling the fieldset restores the select.
  • A form reset returns to the value the select was first rendered with and clears any error message you set.

Positioning

The listbox opens in the browser’s top layer (a popover="manual" element), so it is never clipped by an ancestor’s overflow and never covered by a stacking context — including inside a <dsgn-modal>, where Escape closes the listbox first and the dialog on the next press.

It opens below the trigger, at least as wide as it. When there is not enough room below, it flips above; when it would spill past the right edge of the viewport, it shifts left to keep a 16px gutter. It follows the trigger on scroll and resize while open.

In a browser without the Popover API, the listbox falls back to absolute positioning inside the component, where an ancestor with overflow: hidden can clip it.

Examples

Sizes

<dsgn-select label="Small" size="sm"></dsgn-select>
<dsgn-select label="Medium" size="md"></dsgn-select>
<dsgn-select label="Large" size="lg"></dsgn-select>

Hint and error message

<dsgn-select
  label="Framework adapter"
  hint="Used for generated wrappers."
  error="Choose an adapter."
  required
></dsgn-select>

Listening for changes

document
  .querySelector('dsgn-select')
  .addEventListener('dsgn-change', (event) => {
    const { value, name } = event.detail;
  });

Pill shape

The trigger’s corner radius comes from the --radius-control token (default 0.375rem), which dsgn-input follows too. Override it on :root to round every form control:

:root {
  --radius-control: 9999px;
}

Dark surface

<dsgn-select label="Framework adapter" surface="dark"></dsgn-select>

Accessibility

  • The trigger is a <button role="combobox"> with aria-haspopup="listbox", aria-expanded, and aria-controls; the list is role="listbox" and each option is role="option" with aria-selected and, when unavailable, aria-disabled.
  • Prefer a visible label. When the design has no room for one, set aria-label — a placeholder is not a label.
  • The error and hint are linked through aria-describedby, error first, and required sets aria-required. The * is hidden from assistive technology.
  • The selected option carries a check mark and medium weight, and the highlighted option a fill, so neither relies on color alone.
  • Opening and closing animate with a short scale and fade, and not at all under prefers-reduced-motion: reduce.
© 2026 LightLeap Labs LLC. All rights reserved. · Terms of Service · Privacy Policy · Cookies · Contact · Support