Field
Labels, inputs and hints, plus checkbox, radio, switch and slider rows.
Shipping address form
Long forms use .field-group (a borderless fieldset)
to stack fields. Wrap fields in a <div> inside
the group to split them into equal columns. The columns fold into
one on narrow screens.
<form class="card" method="dialog">
<header><h2 id="ad-title">Edit shipping address</h2></header>
<fieldset class="field-group" aria-labelledby="ad-title">
<div class="field">
<label for="ad-country">Country *</label>
<select id="ad-country" aria-describedby="ad-country-hint" autocomplete="country" required>
<option value="US" selected>United States</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
</select>
<small id="ad-country-hint">Destination country</small>
</div>
<div>
<div class="field">
<label for="ad-first">First name *</label>
<input id="ad-first" type="text" value="Emma" autocomplete="given-name" required />
</div>
<div class="field">
<label for="ad-last">Last name *</label>
<input id="ad-last" type="text" value="Wilson" autocomplete="family-name" required />
</div>
</div>
<div class="field">
<label for="ad-line1">Address line 1 *</label>
<div class="input-group">
<span><i class="icon icon-map-pin" aria-hidden="true"></i></span>
<input id="ad-line1" type="text" value="742 Congress Ave" autocomplete="address-line1" aria-describedby="ad-line1-hint" required />
</div>
<small id="ad-line1-hint">House number and street</small>
</div>
<div class="field">
<label for="ad-line2">Address line 2</label>
<input id="ad-line2" type="text" placeholder="Optional" autocomplete="address-line2" />
</div>
<div>
<div class="field">
<label for="ad-city">City *</label>
<input id="ad-city" type="text" value="Austin" autocomplete="address-level2" required />
</div>
<div class="field">
<label for="ad-province">State *</label>
<select id="ad-province" autocomplete="address-level1" required>
<option selected>Texas</option>
<option>California</option>
<option>New York</option>
</select>
</div>
<div class="field">
<label for="ad-zip">ZIP *</label>
<input id="ad-zip" type="text" inputmode="numeric" value="78701" autocomplete="postal-code" required />
</div>
</div>
<div class="field">
<label for="ad-phone">Phone</label>
<input id="ad-phone" type="tel" value="+1 512 555 0142" autocomplete="tel" />
</div>
<div class="field">
<label for="ad-notes">Delivery instructions</label>
<textarea id="ad-notes" rows="3" aria-describedby="ad-notes-hint"></textarea>
<small id="ad-notes-hint">e.g. leave at the front desk</small>
</div>
</fieldset>
<footer>
<button type="button" class="btn">Close</button>
<button type="submit" class="btn" data-variant="primary">Save</button>
</footer>
</form>
Invalid field and a checkbox beside its label
Set aria-invalid="true" on the input to turn its
border and hint danger. Link the error with
aria-describedby. A checkbox placed before its label
in a .field lays out as a row.
<fieldset class="field-group docs-narrow">
<legend>Billing address</legend>
<div class="field">
<label for="bl-zip">ZIP *</label>
<input id="bl-zip" type="text" inputmode="numeric" value="1011" aria-invalid="true" aria-describedby="bl-zip-error" required />
<small id="bl-zip-error">Enter a 5 digit ZIP code</small>
</div>
<div class="field">
<input id="bl-same" type="checkbox" />
<label for="bl-same">Same as shipping address</label>
</div>
</fieldset>
Checkbox: plain, with help text, disabled, whole row as label
For longer text, put the label and a <small> in
a <div> after the checkbox. To make the whole
row clickable, wrap it all in
<label class="field">. Add
data-variant="card" for a border.
<div class="field-group docs-narrow" role="group" aria-label="Options">
<div class="field">
<input id="cb-terms" type="checkbox" />
<label for="cb-terms">Accept terms and conditions</label>
</div>
<div class="field">
<input id="cb-terms-2" type="checkbox" aria-describedby="cb-terms-2-hint" checked />
<div>
<label for="cb-terms-2">Accept terms and conditions</label>
<small id="cb-terms-2-hint">By ticking this you agree to the terms</small>
</div>
</div>
<div class="field">
<input id="cb-notify" type="checkbox" disabled />
<label for="cb-notify">Enable notifications</label>
</div>
<label class="field" data-variant="card">
<input type="checkbox" aria-describedby="cb-notify-2-hint" />
<div>
<h3>Enable order notifications</h3>
<small id="cb-notify-2-hint">You can turn this on or off at any time</small>
</div>
</label>
</div>
Partly ticked (select all)
HTML cannot set the indeterminate state. A script has to set
checkbox.indeterminate = true. This example is a
select-all checkbox that controls a product's variants. The
browser draws the dash itself.
<div class="field-group docs-narrow" data-gap="sm" id="pt-group" role="group" aria-label="Variants">
<div class="field">
<input
id="pt-all"
type="checkbox"
aria-controls="pt-1 pt-2 pt-3"
aria-describedby="pt-all-hint"
/>
<div>
<label for="pt-all">Timeless Watch</label>
<small id="pt-all-hint">1 of 3 variants selected</small>
</div>
</div>
<div class="field">
<input id="pt-1" type="checkbox" checked />
<label for="pt-1">Silver · $149.00</label>
</div>
<div class="field">
<input id="pt-2" type="checkbox" />
<label for="pt-2">Gold · $169.00</label>
</div>
<div class="field">
<input id="pt-3" type="checkbox" />
<label for="pt-3">Black · $149.00</label>
</div>
</div>
<script>
{
const all = document.getElementById("pt-all");
const rows = ["pt-1", "pt-2", "pt-3"].map((id) => document.getElementById(id));
const hint = document.getElementById("pt-all-hint");
const sync = () => {
const n = rows.filter((r) => r.checked).length;
all.checked = n === rows.length;
all.indeterminate = n > 0 && n < rows.length;
hint.textContent = n + " of " + rows.length + " variants selected";
};
all.addEventListener("change", () => {
for (const r of rows) r.checked = all.checked;
sync();
});
for (const r of rows) r.addEventListener("change", sync);
sync();
}
</script>
Radio
Wrap radios in
<fieldset class="field-group" data-gap="sm">
with a <legend> so screen readers announce the
group name. Arrow keys move between options natively.
<fieldset class="field-group" data-gap="sm">
<legend>Table density</legend>
<div class="field">
<input id="rd-default" type="radio" name="rd-density" value="default" />
<label for="rd-default">Default</label>
</div>
<div class="field">
<input id="rd-comfortable" type="radio" name="rd-density" value="comfortable" checked />
<label for="rd-comfortable">Comfortable</label>
</div>
<div class="field">
<input id="rd-compact" type="radio" name="rd-density" value="compact" />
<label for="rd-compact">Compact</label>
</div>
</fieldset>
Switch
A checkbox with role="switch" draws as a switch. Use
it for settings that apply at once. It works inside a
.field or on its own.
<div class="field-group docs-narrow" data-gap="sm" role="group" aria-label="Store settings">
<div class="field">
<input id="sw-open" type="checkbox" role="switch" checked />
<label for="sw-open">Store open</label>
</div>
<div class="field">
<input id="sw-cod" type="checkbox" role="switch" aria-describedby="sw-cod-hint" />
<div>
<label for="sw-cod">Cash on delivery</label>
<small id="sw-cod-hint">$2.00 fee per order</small>
</div>
</div>
<div class="field">
<input id="sw-intl" type="checkbox" role="switch" disabled />
<label for="sw-intl">International shipping</label>
</div>
</div>
Slider
<input type="range" class="slider"> uses its
own min, max, step and value as usual. The script fills the track
up to the thumb and writes the value into any
<output for="id"> that points to it.
<div class="stack docs-narrow">
<div class="field">
<label for="sl-discount">Discount: <output for="sl-discount">15</output>%</label>
<input id="sl-discount" class="slider" type="range" min="0" max="50" step="5" value="15" />
</div>
<div class="field">
<label for="sl-stock">Low stock alert: <output for="sl-stock">20</output> units</label>
<input id="sl-stock" class="slider" type="range" min="0" max="100" value="20" />
</div>
<input class="slider" type="range" min="0" max="100" value="60" aria-label="Alert volume" disabled />
</div>
API
The slider uses js/slider.js (other fields need no
JS). It also works on .slider elements added later.
| Member | Args | Description |
|---|---|---|
--value |
Custom property on the input: the current value as a
percentage (e.g. 30%). The accent fill is
this wide. Set it in style yourself if you
don't load the JS.
|
|
output[for] |
The script writes value into every
<output> whose for
contains the slider's id.
|
|
window.
|
el |
el is an input.slider. Redraws the fill and
outputs. Call it after setting el.value from
script (script changes don't fire an input event).
|
input |
The input's own event. The script redraws on every move and after a form reset. |
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.
|
Wraps a label, input and hint in a vertical stack. With a checkbox or radio as the first child, it lays out as a row. | |
.
|
Small muted label (normal colour in a checkbox row) | |
.
|
Hint or error text. Link it to the input with
aria-describedby.
|
|
.
|
Styled like .input without the class (see the
Input page).
|
|
.
|
In a checkbox row: stacks the label and small (or h3). | |
aria-invalid |
true |
On the input: border and small turn danger |
disabled |
On the input: row label turns muted, cursor not-allowed | |
data-variant |
card (none = default) |
On label.field: a border around the whole
row. The background changes when ticked.
|
role |
switch |
On input type=checkbox: draws a switch. Works
anywhere.
|
.
|
On input[type=range]: track and thumb use the
colour variables. See the API above.
|
|
.
|
Borderless fieldset or div[role=group]. Stacks fields space-4 apart. | |
.
|
Group heading (bold). With
data-gap="sm" it is a small muted label.
|
|
.
|
A div with no class splits its fields into equal columns, each at least 9rem wide. | |
data-gap |
sm (none = space-4) |
On .field-group: tight spacing for checkbox, radio and switch lists. |