---
outline: deep
---

# Select

Build a select two ways. For a simple single select, style a native `<select>` with `class="l-select"` — zero-JS and the recommended default. For in-popover search, multi-select with chips, or async options, reach for the `<l-select>` custom element.

**`<select>`** — Native HTML Element

```html
<select class="l-select">
  <option>France</option>
  <option>Germany</option>
</select>
```

## Basic

Add `class="l-select"` to a native `<select>` — its `<option>`s are styled automatically. Built on the [Customizable Select API](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select) (`appearance: base-select`), degrading to a native `<select>` where unsupported — so always keep meaningful text in each `<option>`.

```html
<select
  class="l-select"
  aria-label="Plan"
>
  <option value="free">Free</option>
  <option value="pro">Pro</option>
  <option value="team">Team</option>
</select>
```

### Disabled

Native `disabled` attribute, on the `<select>` for the whole control or on an individual `<option>`. To disable a group of fields at once, wrap them in `<fieldset disabled>` — see [Form field](./form-field.md#disabled-group).

```html
<select
  class="l-select"
  disabled
>
  <option>France</option>
  <option disabled>Germany</option>
</select>
```

### Rich options

Put markup inside each `<option>` — wrap a `.l-select-item-title` over a `.l-select-item-description` in `.l-select-item-text` (add a `.l-select-item-media` image or icon before it). A `<button><selectedcontent></button>` trigger mirrors the chosen option.

```html
<select
  class="l-select"
  aria-label="Merge method"
>
  <button>
    <selectedcontent></selectedcontent>
  </button>

  <option
    value="squash"
    selected
  >
    <span class="l-select-item-text">
      <span class="l-select-item-title">Squash and merge</span>
      <span class="l-select-item-description">
        The 1 commit from this branch will be added to the base branch.
      </span>
    </span>
  </option>

  <option value="rebase">
    <span class="l-select-item-text">
      <span class="l-select-item-title">Rebase and merge</span>
      <span class="l-select-item-description">
        The 1 commit from this branch will be rebased and added to the base branch.
      </span>
    </span>
  </option>
</select>
```

## Enhanced: `<l-select>`

When the native tier isn't enough, the `<l-select>` custom element adds in-popover search, multi-select with removable chips, and async options. It needs its JS module (`import 'luxen-ui/select'`). Options are authored as a native `<datalist>` of `<option>` — the same surface as [`<l-combobox>`](/elements/combobox) — with `<option selected>` for pre-selection.

**`<l-select>`** — Custom Element · Shadow DOM

### Searchable

Add `searchable` for a filter box inside the popover — useful for long lists. Matching is accent/case-insensitive.

```html
<div class="w-72">
  <l-select
    searchable
    label="Country"
    name="country"
    placeholder="Select a country…"
  >
    <datalist>
      <option value="us">United States</option>
      <option value="fr">France</option>
      <option value="de">Germany</option>
      <option value="es">Spain</option>
      <option value="it">Italy</option>
      <option value="jp">Japan</option>
      <option value="br">Brazil</option>
    </datalist>
  </l-select>
</div>
```

### Multiple

Add `multiple` to select several values. The trigger shows a removable [`<l-tag>`](/elements/tag) chip per value and the form submits one entry per value under `name`.

```html
<div class="w-72">
  <l-select
    multiple
    label="Tags"
    name="tags"
    placeholder="Select tags…"
  >
    <datalist>
      <option
        value="design"
        selected
      >
        Design
      </option>
      <option value="dev">Development</option>
      <option value="qa">QA</option>
      <option value="ops">Ops</option>
    </datalist>
  </l-select>
</div>
```

### Rich options

The same `.l-select-item-*` classes as the native tier work inside each `<option>`. Set a `label` (or `.l-select-item-title`) so filtering and the trigger display use the title.

```html
<div class="w-72">
  <l-select
    label="Country"
    placeholder="Select a country…"
  >
    <datalist>
      <option
        value="us"
        label="United States"
      >
        <span class="l-select-item-media"><l-icon name="circle-flags:us"></l-icon></span>
        <span class="l-select-item-text">
          <span class="l-select-item-title">United States</span>
          <span class="l-select-item-description">North America</span>
        </span>
      </option>
      <option
        value="fr"
        label="France"
      >
        <span class="l-select-item-media"><l-icon name="circle-flags:fr"></l-icon></span>
        <span class="l-select-item-text">
          <span class="l-select-item-title">France</span>
          <span class="l-select-item-description">Europe</span>
        </span>
      </option>
      <option
        value="jp"
        label="Japan"
      >
        <span class="l-select-item-media"><l-icon name="circle-flags:jp"></l-icon></span>
        <span class="l-select-item-text">
          <span class="l-select-item-title">Japan</span>
          <span class="l-select-item-description">Asia</span>
        </span>
      </option>
    </datalist>
  </l-select>
</div>
```

### Clearable

Add `with-clear` for a button that resets the value.

### Custom filter

Options are matched case- and accent-insensitively (every space-separated keyword must appear). Override the `filter` property — `(item, query) => boolean` — for `startsWith`, fuzzy, or remote-driven filtering.

```js
const select = document.querySelector('l-select');

// Match from the start of the label instead of anywhere in it.
select.filter = (item, query) => item.label.toLowerCase().startsWith(query.toLowerCase());
```

## Accessibility

The native tier inherits the platform's `<select>` semantics — pair it with a `<label>`. The criteria below cover the `<l-select>` custom element.

### Criteria

- **Role** — The trigger exposes `aria-haspopup="listbox"` / `aria-expanded` / `aria-controls`; options are `role="option"` in a `role="listbox"` (with `aria-multiselectable` when `multiple`)
- **Accessible name** — Set `label` (or wrap with `<l-form-field>`)
- **Focus management** — Opening moves focus into the search box (or the listbox when not searchable); closing returns it to the trigger
- **Target size** — The trigger and chip remove buttons keep a minimum 24×24px hit target

### Rules

- Always set `label` so the trigger has an accessible name

### Keyboard interactions

- `Enter / Space / ArrowDown` — Opens the listbox (focus moves to the search box when searchable, else the list)
- `ArrowDown / ArrowUp` — Moves the active option
- `Enter` — Selects the active option (single) or toggles it (multiple)
- `Escape` — Closes the listbox and returns focus to the trigger
- `Backspace / Delete` — Removes a focused chip (multiple)

## API reference

The native tier ships its styles via `luxen-ui/css/select`; the reference below is for the `<l-select>` custom element.

### Importing

```js
import 'luxen-ui/select';
```

```css
@import 'luxen-ui/css/select';
```

### Attributes & Properties

- **placeholder**: `string` — Placeholder shown in the trigger when nothing is selected.
- **label**: `string` — Accessible label for the trigger.
- **size**: `SelectSize` (default: `'md'`) — Control size.
- **searchable**: `boolean` (default: `false`) — Show a filter box inside the popover (opt-in, for long lists).
- **with-clear**: `boolean` (default: `false`) — Show a button to clear the value.
- **placement**: `Placement` (default: `'bottom-start'`) — Panel placement relative to the trigger.
- **multiple**: `boolean` (default: `false`) — Allow selecting multiple values — renders chips and submits one entry per value.
- **filter**: `SelectFilter` (default: `defaultFilter`) — Override the option filter. `(item, query) => boolean`.
- **value**: `string | string[]` — The selected value (single mode) or array of values (multiple mode).
- **validationTarget**

### Events

- **change** — Fired when the selection changes. Bubbles. Not cancelable. Properties: `value: string | string[]`.
- **input** — Fired as the user types in the search box. Bubbles. Not cancelable. Properties: `value: string` (the query).
- **show** (cancelable) — Fired before the listbox opens. Cancelable.
- **hide** (cancelable) — Fired before the listbox closes. Cancelable.

### CSS parts

- `base` — The host wrapper.
- `trigger` — The button that opens the listbox.
- `value` — The selection display inside the trigger.
- `chevron` — The trigger chevron.
- `clear` — The clear button.
- `panel` — The floating popover panel.
- `search` — The search input.
- `listbox` — The options container.
- `option` — Each option row.
- `empty` — The "no results" message.

### CSS custom properties

- `--height` — Control height. Defaults to the form-control height.
- `--border-radius` — Trigger + panel radius.
- `--background` — Panel background.
