Forms
NativeSelect
The platform’s own picker, restyled where it can be.
Usage
When to reach for it
import { NativeSelect } from '@misoto22/design'Notes
A native <select>, restyled.
The escape hatch, not the default — Select is the styled one. Reach for this where the platform's own picker is genuinely better: a very long list on a phone, a form that must work without JavaScript, a page where the last kilobyte matters. The browser gives typeahead and the mobile wheel for free, and those are real.
What it cannot do is look like the rest of the system once open. The option list is drawn by the operating system, so it carries none of these tokens — which is exactly why it stopped being the default.
className sizes the WRAPPER, not the <select> inside it. This is the one control here that renders two elements, and the chevron is pinned to the wrapper's end edge: a width on the select alone narrowed the box and left the arrow floating at the far side of the row. The select fills whatever the wrapper is.
Anatomy
| Element | Description |
|---|---|
| Wrapperrequired | A relative <div> around the pair, and the element className lands on. It is the only control in this group where className does not go to the field itself, because the chevron is pinned to this box: a width set anywhere else strands the arrow at the far edge of the row. |
| Control boxrequired | The <select> on CONTROL_BASE, appearance-none so the platform’s own arrow is gone, with pe-9 of end padding so the longest option clears the drawn one. It fills the wrapper, so the wrapper’s width is the field’s width. |
| Chevronrequired | A pointer-events-none icon pinned to the wrapper’s end edge. It is ours, not the platform’s, so it does not flip or move when the picker opens. |
| Option list | children, drawn by the operating system on open. <option> and <optgroup> are the only things in it, and neither takes these tokens. |
Best practices
Do
- Give it an explicit empty first option, or a defaultValue. A <select> nobody touches has its first option selected, so an untouched form submits the top of the list as though someone chose it.
- Group with <optgroup>: it is the one piece of structure the OS picker actually renders, and there is no styled equivalent to fall back on the way SelectLabel gives Select one.
- Set the width with className. It lands on the wrapper the chevron is pinned to and the select fills it, so the arrow travels with the edge of the field rather than staying where the row ends.
Don’t
- multiple and size do not survive the styling: appearance-none plus a chevron pinned to the middle of the wrapper turns a list box into a scrolling column with an arrow drawn across it. Use checkboxes, or a multiple Combobox.
- Do not use the first option as the label. “Select a country” is announced as a choosable value and it is the value an untouched form submits — put the name in a Field and give that option value="" and disabled.
- Do not send the control’s own ink or border through className: it dresses the wrapper, and the <select> inside keeps CONTROL_BASE whatever the box around it says.
Examples
default
The platform's own picker, restyled as far as it can be: appearance-none drops the operating system's arrow and the chevron beside it is ours, so it does not flip or move when the list opens. What it cannot do is look like the system once open — the options are drawn by the OS and carry none of these tokens, which is why this is the escape hatch and Select is the default. Reach for it where the platform genuinely wins: a very long list on a phone, a form that must work without JavaScript, a page counting its last kilobyte.
The platform's own picker — better on a phone, and it works without JavaScript.
a placeholder option
An empty first option, disabled, with defaultValue pointing at it. A select nobody touches has its first option selected already, so a list that opens on Australia submits Australia on behalf of a reader who never saw the field. Naming that option “Select a country” only moves the problem: it is announced as a choosable value and it is still what an untouched form sends. The name belongs in the Field. Note that required is on the select as well — Field's own required is the asterisk and aria-required, and never reaches the control's attribute, so it is the one here that makes the browser refuse the submit.
Where the invoice is issued from.
option groups
optgroup is the one piece of structure the operating system's picker actually renders, and there is no styled fallback the way SelectLabel gives Select one — so it is how a long native list stays scannable. The width comes from the parent, not from className: className lands on the select itself while the chevron is positioned against the wrapper around it, so a field narrowed that way leaves its own arrow stranded at the far edge of the row.
Keyboard
| Key | Does |
|---|---|
| Space↓ | Opens the platform picker. |
| a–z | Typeahead, from the browser’s own implementation. |
Accessibility
- Typeahead and the mobile wheel come free, from the browser.
- What it cannot do is look like the rest of the system once open — the option list is drawn by the operating system and carries none of these tokens.