# Select

> A dropdown list. CSS component with one class and no JavaScript.

The same appearance and states as [Input](/components/input.md), on a real `<select>`. The browser’s own list is then used, the one that already works with the keyboard, screen readers and the phone’s built-in picker.

## How to use it

**Plain HTML**

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/select/select.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/label/label.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->
```

```html
<label class="lt-label" for="country">Country</label>
<select class="lt-select" id="country" name="country">
<option value="">Choose a country</option>
<option>Canada</option>
<option selected>Ireland</option>
<option>United Kingdom</option>
</select>


<label class="lt-label" for="city">City</label>
<select class="lt-select" id="city" data-state="invalid" aria-invalid="true">
<option value="" selected>Choose a city</option>
</select>


<label class="lt-label" for="registered">Registered country</label>
<select class="lt-select" id="registered" disabled>
<option selected>Ireland</option>
</select>
```

**TypeScript**

```ts
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/select.css"
import "@lo-tek/interfaces/label.css"
```

`lt` is used in two ways, and which one depends on whether you create the element or find it.

**If you create the markup,** you spread the attributes in. That is the route in React, Astro and anything else that renders:

```tsx
import { lt } from "@lo-tek/interfaces/react"


<select {...lt.select({ picker: "styled" })} id="country" name="country">
  <option value="">Choose a country</option>
  <option value="ireland">Ireland</option>
</select>
```

The import goes to `/react` here, since the block is JSX. From there the keys are called `className` and `htmlFor`. In Astro and anything else without React you import `@lo-tek/interfaces` and get `class` and `for`.

**If the element is already there,** you set them on it. That is the route when you enhance HTML a server or a template has sent:

```ts
import { lt, type FieldState } from "@lo-tek/interfaces"


const field = document.querySelector<HTMLSelectElement>("#country")
if (!field) throw new Error("element not found")


field.addEventListener("change", () => {
  const state: FieldState = field.value === "" ? "invalid" : "default"


  lt.setAttributes(field, lt.select({ state, picker: "styled" }))
})
```

`state` is the same type as in [Input](/components/input.md) and [Textarea](/components/textarea.md). When the field goes back to `"default"`, `lt.setAttributes` removes both `data-state` and `aria-invalid`.

Every call sets the whole state. If you leave out `picker`, `data-picker` is removed in the same way as `data-state`, so pass the options the field should keep.

| `data-state` | Used for |
| - | - |
| *(omitted)* | Normal state |
| `invalid` | Validation has failed |
| `success` | The choice is confirmed valid. Use sparingly. |

The first `<option>` should have an empty `value` when the field is required. Then `required` actually stops the submission until the user has chosen something.

## The list drawn in the page

The list that pops up is normally a window from the operating system, and no CSS reaches into it. With `data-picker="styled"` the field asks the browser to draw the list inside the page instead, so it gets the system’s colours, spacing and shadow.

**Plain HTML**

```html
<label class="lt-label" for="case">Application type</label>
<select class="lt-select" data-picker="styled" id="case" name="case">
<option value="">Choose an application type</option>
<option value="building" selected>Building permit</option>
<option value="subdivision">Subdivision of property</option>
<option value="discharge">Discharge permit</option>
</select>
```

This is the same `<select>` as before. It is submitted with the form, read by screen readers and works with the keyboard in exactly the same way, and the markup is unchanged apart from the attribute.

An option longer than the field then wraps over several lines, and the field gets taller. If it should be clipped with ”…” instead, put `<button><selectedcontent></selectedcontent></button>` first in the `<select>`. The browser does not create `<selectedcontent>` by itself, but fills it with the selected option when it is there, and LO-TEK clips it. Browsers without support skip the button.

Groups made with `<optgroup label="…">` get their heading in smaller, muted text, so it stands apart from the options.

Support differs between browsers, which is why this has to be switched on:

| Browser | What the user sees |
| - | - |
| Chromium and Safari that support `appearance: base-select` | The list is drawn in the page, with LO-TEK’s colours |
| Firefox | The browser’s own list, as without the attribute |

That is the one condition the stylesheet asks about, so every version that has `appearance: base-select` draws the list in the page. LO-TEK is tested in Chromium 148 and WebKit 26.4. Firefox gets no new functionality, but nothing that looks broken either. If all users should see the same thing, leave the attribute out.

In high contrast mode the field falls back to the browser’s own list in all browsers. The system colours are the only ones guaranteed to be readable there.

```ts
import { lt } from "@lo-tek/interfaces"


lt.select({ picker: "styled" })
// { class: "lt-select", "data-picker": "styled" }
```

## Classes you can style

| Class | What it is |
| - | - |
| `.lt-select` | The dropdown list. The state comes from `data-state`, the list from `data-picker`. |

| Variable | Default | Applies to |
| - | - | - |
| `--lt-select-width` | `100%` | The width of the field |
| `--lt-select-padding` | `var(--lt-spacing-2) var(--lt-spacing-3)` | The space inside the field |
| `--lt-select-radius` | `var(--lt-spacing-1)` | The field, and the list drawn in the page |
| `--lt-select-option-padding` | `var(--lt-spacing-2) var(--lt-spacing-3)` | One option in the list drawn in the page |
| `--lt-select-viewport-margin` | `var(--lt-spacing-2)` | Space between the list and the edge of the window |

The arrow is two gradients in the background, drawn in `--lt-color-neutral-text-subtle`, so it follows the theme. To remove it, set `background-image: none`. With `data-picker="styled"` the browser draws the arrow itself, and it is styled with `::picker-icon`.

In the default appearance the space for the arrow at the end of the field is fixed, since the arrow is drawn a fixed distance from the edge, and `--lt-select-padding` applies to the other three sides. With `data-picker="styled"` the arrow takes its own space, and the variable applies to all four.

A compact field, such as “Items per page”, becomes as wide as its content with `--lt-select-width: auto`. If the label should sit beside the field, the layout around them decides that, not LO-TEK. `.lt-label` is a block, and a row with `display: flex` around the two puts them on one line.

See [Customising](/customising.md) for how the variables and the layer work.

## Accessibility

- An `<option>` that says “Choose a country” is a label in disguise, not a replacement for `<label>`.
- If the list has more than about seven options that belong together logically, group them with `<optgroup label="…">`.
- Do not submit the form automatically on change. Keyboard users move through the options with the arrow keys and would submit the form on the first one they pass.
- If you need search, multiple selection or icons in the list, `<select>` is the wrong tool. Build it as a standalone component instead of restyling this element.
