Form elements are styled globally using semantic tokens. All text inputs, textareas, selects, checkboxes, and radios share consistent sizing, focus states, and disabled styling.
Form Tokens
All form styling is controlled by semantic tokens in :root:
| Token | Default | Purpose |
|---|---|---|
| Default border color | ||
| Input background | ||
| Input text color | ||
| Placeholder text color | ||
| Focus border and ring color | ||
| Disabled background | ||
| Disabled text color | ||
| Checkbox unchecked background | ||
| Checkbox/radio checked fill | ||
| Checkbox/radio border color | ||
| Checkmark/dot color | ||
| Toggle track background | ||
| Toggle knob (unchecked) | ||
| Toggle track (checked) | ||
| Toggle knob (checked) |
Labels
Labels are styled as block elements with medium weight:
<label for="name">Full name</label>
<input type="text" id="name" placeholder="Enter your name">
Properties: display: block, font-size: var(--font-s), font-weight: var(--font-weight-medium), bottom margin for spacing from the input.
Text Inputs
All standard text input types are styled globally:
<input type="text" placeholder="Text">
<input type="email" placeholder="Email">
<input type="password" placeholder="Password">
<input type="number" placeholder="Number">
<input type="search" placeholder="Search">
<input type="url" placeholder="URL">
<input type="tel" placeholder="Phone">
Properties: full width, font-size: var(--font-m) (matches body text), padding: var(--space-m) var(--space-l), border from , smooth focus transition.
Focus State
All inputs share a consistent focus style:
- Border color changes to
- A subtle box-shadow ring appears (2px, 75% transparent)
- No outline (replaced by box-shadow for consistency)
This is accessibility-safe and keyboard-visible.
Textarea
Textareas have a minimum height and allow vertical resizing:
<label for="message">Message</label>
<textarea id="message" placeholder="Enter your message..."></textarea>
Properties: min-height: 120px, resize: vertical.
Select
Selects use a custom dropdown arrow via an inline SVG background:
<label for="country">Country</label>
<select id="country">
<option value="" disabled selected>Choose a country</option>
<option value="nz">New Zealand</option>
<option value="au">Australia</option>
</select>
Properties: appearance: none, custom caret, right padding for arrow space.
Colour Input
The native colour picker is styled to match other form inputs. Browser chrome is removed so the colour swatch fills the entire element. Use alongside a text input for hex/named colour entry.
<div class="block row gap-s align-center">
<input type="color" id="color-picker" value="#ffa500">
<input type="text" value="ffa500" placeholder="hex or name">
</div>
Properties: appearance: none, aspect-ratio: 1 / 1, align-self: stretch (matches sibling height), padding: var(--space-xs), swatch wrapper padding removed. Same border and focus styles as text inputs.
Disabled State
Add the disabled attribute to any input, textarea, or select:
<input type="text" value="Cannot edit" disabled>
Properties: background, color, cursor: not-allowed.
Checkbox & Radio
Checkboxes and radios use appearance: none with custom styling. Wrap each in for inline label alignment.
Checkbox
<div class="form-check">
<input type="checkbox" id="terms">
<label for="terms">I agree to the terms</label>
</div>
Properties: 24px size (), fill, border with corners. Checked state uses fill with a white checkmark SVG.
Radio
<div class="form-check">
<input type="radio" name="group" id="option-a">
<label for="option-a">Option A</label>
</div>
Properties: same as checkbox but with border-radius: 50% and a centered dot on checked.
Toggle / Switch
A toggle is a checkbox styled as a sliding switch. Use instead of . Always include role="switch" for accessibility.
Default (label left)
<div class="form-toggle">
<input type="checkbox" id="notifications" role="switch">
<label for="notifications">Enable notifications</label>
</div>
Label right
Add to place the label after the toggle.
<div class="form-toggle is-label-right">
<input type="checkbox" id="darkmode" role="switch">
<label for="darkmode">Dark mode</label>
</div>
Disabled
<div class="form-toggle">
<input type="checkbox" id="feature" role="switch" disabled>
<label for="feature">Coming soon</label>
</div>
Properties: 44px × 24px pill-shaped track, 18px circular knob. Unchecked: background with knob. Checked: background, knob slides right and becomes .
Layout Patterns
Form Group
Use to wrap a label + input pair with consistent bottom spacing:
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email">
</div>
Form Check
Use for inline checkbox/radio + label pairs:
<div class="form-check">
<input type="checkbox" id="opt-in">
<label for="opt-in">Subscribe to newsletter</label>
</div>
Properties: display: flex, align-items: center, gap for spacing. The label inside is inline with regular weight.
Fieldset & Legend
Use <fieldset> and <legend> to group related form controls:
<fieldset>
<legend>Contact details</legend>
<div class="form-group">
<label for="phone">Phone</label>
<input type="tel" id="phone">
</div>
</fieldset>
Segmented Control
is a button group that acts like a single-select input — similar to a group of radio buttons but with a tab-like appearance.
<div class="segmented-control">
<button class="segmented-control-btn is-active">Option A</button>
<button class="segmented-control-btn">Option B</button>
<button class="segmented-control-btn">Option C</button>
</div>
Icon variant
Use on a segment button for icon-only options:
<button class="segmented-control-btn is-icon" aria-label="Grid view">
<div class="svg-icn" data-icon="grid"><!-- SVG --></div>
</button>
Styling
- Track: with corners
- Active segment: background with text
- Focus: 2px outline with offset
Accessibility
- Add
role="group"andaria-labelto the container - Use
aria-pressed="true"on the active segment button - Toggle
is-activeandaria-pressedvia JavaScript on click
Slider
input[type="range"] is styled with design system tokens. Use for a labelled slider with live value display. See the Slider docs for full details.
<div class="slider-wrapper">
<div class="slider-header">
<label for="opacity">Opacity</label>
<span class="slider-value" id="opacity-val">75%</span>
</div>
<input type="range" id="opacity" min="0" max="100" value="75"
oninput="document.getElementById('opacity-val').textContent = this.value + '%'">
</div>
Number Input
wraps a native number input with decrement/increment buttons. See the Number Input docs for full details.
<div class="number-input" role="group" aria-label="Quantity">
<button class="number-input-btn" data-number-decrement type="button" aria-label="Decrease">−</button>
<input type="number" value="1" min="0" max="99" aria-label="Quantity">
<button class="number-input-btn" data-number-increment type="button" aria-label="Increase">+</button>
</div>
Requires assets/js/number-input.js.
Radio Group
wraps multiple radio inputs with consistent spacing. Supports vertical (default) and horizontal () layouts. See the Radio Group docs for full details.
<fieldset>
<legend class="radio-group-label">Shipping method</legend>
<div class="radio-group">
<div class="form-check">
<input type="radio" name="shipping" id="std" checked>
<label for="std">Standard (5-7 days)</label>
</div>
...
</div>
</fieldset>
Rules
| Do | Don't |
|---|---|
Use <label> with for attribute |
Use placeholder as a label replacement |
| Use for spacing | Add margins directly to inputs |
| Use for checkbox/radio pairs | Float labels next to checkboxes manually |
| Use for toggle switches | Style a checkbox as a toggle without the wrapper |
Add role="switch" on toggle inputs |
Use a toggle without the switch role |
| Use semantic tokens for customization | Hardcode colors on individual inputs |
Use <fieldset> for logical grouping |
Use <div> with borders to fake fieldsets |
| Keep inputs full-width by default | Set fixed widths unless layout requires it |