Color input
Colour input — a swatch that opens the operating system's colour picker.
Examples
<div class="field field-inline">
<input class="input color-input" id="accent" name="accent" type="color" value="#3b5bdb" />
<label class="field-label" for="accent">Accent colour</label>
</div>
<div class="field field-inline">
<input class="input color-input" id="surface" name="surface" type="color" value="#f8f7f4" />
<label class="field-label" for="surface">Page background</label>
</div>A swatch is a control the size of a checkbox, so it takes a checkbox’s layout: .field-inline, control first, label beside it.
Stacking a one-line label over a 40px square leaves a column of two small things with a lot of empty page beside them. What opens on click is the operating system’s own picker — its wheel, its saved swatches, its eyedropper — and nothing can style, position or read it.
Pick it, or type it.
<div class="field">
<label class="field-label" for="brand">Brand colour</label>
<div class="color-pair">
<input class="input color-input" id="brand" name="brand" type="color" value="#3b5bdb"
aria-describedby="brand-help"
data-ui="color-input" data-color-text="#brand-hex" />
<label class="sr-only" for="brand-hex">Brand colour, as a hex code</label>
<input class="input color-pair-hex" id="brand-hex" type="text"
maxlength="7" value="#3B5BDB" spellcheck="false" autocomplete="off" />
</div>
<p class="field-help" id="brand-help">Pick it, or type it.</p>
</div>The pairing is not a nicety. A colour chosen by eye alone is unusable for anyone who cannot distinguish it, and there is a blunter reason as well: measured 2026-08-08, WebKit leaves a colour input out of the Tab order entirely, so in that engine the swatch is reachable only by pointer and the text field is the only keyboard route to the value.
Keeping the two in step used to be left to you, and every consumer wrote the same twelve lines: data-ui="color-input" does the echoing now, with data-color-text naming the field it belongs to — there is no proximity magic, because a swatch and a text field that merely happen to be siblings are not necessarily a pair. .color-pair is the row they sit in, and .color-pair-hex fixes the field at the width of the seven characters it holds — an elastic text input beside a 40px square is a proportion you notice immediately, and three sites wrote that same rule by hand before it shipped here. Drag around the picker and the hex updates as you go. Typing a half-finished value does not move the swatch and does not rewrite what you are typing; only the text field carries name, so the form receives one value rather than two.
Tokens 6
Level 2, declared on .color-input itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --color-pair-hex-size | 7.5rem |
| --color-input-padding | var(--space-1) |
| --color-input-size | var(--input-min-size, 2.5rem) |
| --color-input-swatch-border-color | var(--color-border) |
| --color-input-swatch-border-width | var(--border-width) |
| --color-input-swatch-radius | var(--radius-sm) |
Variants and states
Variants
None. It is one shape, and the page does the rest.
Inside it
.color-pair.color-pair-hex
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
:disabled
Before you ship it
This one needs a script. Opt the markup in with data-ui="color-input" and import import 'mostlycss/js/components/color-input' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.
What you have to do 3 requirements
- A colour input has no text alternative. The swatch is the control: its value is announced, if at all, as a hex string, and the currently chosen colour is conveyed by nothing but the colour itself. Two consequences, and both are the consumer's to handle because no CSS can:
- Give it a real
<label>naming what the colour is for ("Brand colour"), not what it is. Nothing else in the control carries meaning. - Pair it with a text input holding the hex value, labelled and bound to the same state. Choosing by eye alone fails anyone who cannot distinguish the colour — and it also fails anyone who has a hex code and wants to type it, which the native picker allows and this field does not. The pair is the usual answer and it is the accessible one. Measured 2026-08-08, there is a blunter reason too: WebKit leaves a colour input out of the Tab order entirely. A bare
<input type="color">with no styling at all is skipped, while a<button>and a checkbox beside it are not, so this is the engine's policy and no CSS or markup here changes it. In that engine the control is reachable only by pointer, and the paired text field is the only keyboard route to the value. That turns the pairing from a recommendation into the thing that keeps the control operable — which is why it is written here and not left to taste. Contrast is the other half of this: a colour someone picks here is one the library cannot audit. If the chosen colour will be painted behind text, show the resulting contrast ratio rather than trusting the eye that picked it.
src/css/components/color-input.css · npx mostlycss add color-input