Select
The same appearance and states as Input, 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
Section titled “How to use it”<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 --><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>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:
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:
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 and Textarea. 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
Section titled “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.
<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.
import { lt } from "@lo-tek/interfaces"
lt.select({ picker: "styled" })// { class: "lt-select", "data-picker": "styled" }Classes you can style
Section titled “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 for how the variables and the layer work.
Accessibility
Section titled “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.