Forms
tier 2 — classes.form-field stacks a label, a control and a hint with the right gaps.
.form-input works on input, select and
textarea alike — it is width-100%, so the parent controls the size.
Every control here is a native element with a class on it: no wrappers, no
@bind to work around, and required /
aria-invalid keep doing what they already do.
Fields
Label, control, hint. The hint explains the consequence, not the control.
How often the queue refreshes. Shorter means more database load.
<div style="max-width:420px; display:flex; flex-direction:column; gap:16px">
<div class="form-field">
<label class="form-label" for="f-name">Display name</label>
<input class="form-input" id="f-name" type="text" value="Alex Fischer" />
</div>
<div class="form-field">
<label class="form-label" for="f-poll">Poll interval</label>
<select class="form-input form-select" id="f-poll">
<option>10 seconds</option>
<option selected>30 seconds</option>
<option>2 minutes</option>
</select>
<p class="form-hint">How often the queue refreshes. Shorter means more database load.</p>
</div>
<div class="form-field">
<label class="form-label" for="f-reason">Reason</label>
<textarea class="form-input" id="f-reason" rows="3" placeholder="Why are you overriding the reservation?"></textarea>
</div>
</div>
Selects
Add .form-select beside .form-input to replace the platform
caret with one that follows the theme. The caret is drawn from two gradients — this
stylesheet loads and inlines nothing, and a pseudo-element does not render on a
<select> in any browser. A list box (multiple, or
size above 1) drops the caret on its own, since there is nothing to drop.
<div class="field-grid" style="max-width:620px">
<div class="form-field">
<label class="form-label" for="f-env">Target environment</label>
<select class="form-input form-select" id="f-env">
<option>Production</option>
<option selected>Staging</option>
<option>Development</option>
</select>
</div>
<div class="form-field">
<label class="form-label" for="f-groups">Notify groups</label>
<select class="form-input form-select" id="f-groups" multiple size="3">
<option selected>Fulfilment</option>
<option>Network</option>
<option selected>On call</option>
</select>
</div>
</div>
Two-up rows
.field-grid lays fields out side by side and collapses to one column when
there is not room for two — no breakpoint to pick.
<div class="field-grid" style="max-width:620px">
<div class="form-field">
<label class="form-label" for="f-from">Active from</label>
<input class="form-input" id="f-from" type="time" value="18:00" />
</div>
<div class="form-field">
<label class="form-label" for="f-to">Active to</label>
<input class="form-input" id="f-to" type="time" value="06:00" />
</div>
</div>
Checkboxes
.form-check is the clickable label wrapper — the whole label toggles the
box. The check mark is drawn in CSS, so it picks up the brand token.
<div style="display:flex; flex-direction:column; gap:10px">
<label class="form-check"><input type="checkbox" checked /> Desktop notifications</label>
<label class="form-check"><input type="checkbox" /> Play a sound on new items</label>
<label class="form-check"><input type="checkbox" checked /> Compact table density</label>
<label class="form-check"><input type="checkbox" disabled /> Auto-advance (unavailable in demo)</label>
</div>
Radio buttons
The same .form-check wrapper — the type attribute decides the shape. A radio
is an unfilled circle with a brand dot rather than a filled disc, so shape
and fill differ from a checkbox and which one is exclusive reads without
colour. Give every radio in a set the same name, and wrap the set in a
<fieldset> so its label is announced with each option.
<fieldset class="form-section" style="max-width:420px">
<legend>On failure</legend>
<div class="form-section-body">
<label class="form-check"><input type="radio" name="onfail" checked /> Retry three times, then alert</label>
<label class="form-check"><input type="radio" name="onfail" /> Alert immediately</label>
<label class="form-check"><input type="radio" name="onfail" /> Log only</label>
</div>
</fieldset>
Switches
.switch is for a setting that takes effect the moment it is flipped. A
checkbox is for a value saved with the rest of the form. The difference is when it
applies, not how it looks — picking the wrong one misleads the reader about whether
Save is still needed. It is still an
<input type="checkbox">, so it needs no ARIA.
<div style="display:flex; flex-direction:column; gap:14px; max-width:460px">
<label class="switch">
<input type="checkbox" checked />
<span class="switch-text">
<span>Auto-refresh the queue</span>
<span class="switch-hint">Applies at once. Turn off while comparing two snapshots.</span>
</span>
</label>
<label class="switch"><input type="checkbox" /> Play a sound on new items</label>
<label class="switch"><input type="checkbox" checked disabled /> Managed by policy</label>
</div>
Three sizes
.form-input-sm, the default, and .form-input-lg, matching
.btn-sm, .btn and .btn-lg exactly. An input group takes
the tier on the group with .input-group--sm or --lg, because the
group is what draws the border.
<div class="dr-col dr-gap-2" style="max-width:420px">
<div class="dr-row">
<input class="form-input form-input-sm" type="search" placeholder="Small" aria-label="Small field" />
<button class="btn btn-sm">Small</button>
</div>
<div class="dr-row">
<input class="form-input" type="search" placeholder="Default" aria-label="Default field" />
<button class="btn">Default</button>
</div>
<div class="dr-row">
<input class="form-input form-input-lg" type="search" placeholder="Large" aria-label="Large field" />
<button class="btn btn-lg">Large</button>
</div>
<span class="input-group input-group--sm">
<span class="input-affix"><i class="ri-search-line"></i></span>
<input class="form-input" type="search" placeholder="Small group" aria-label="Small group" />
<button class="btn btn-icon" type="button" aria-label="Clear"><i class="ri-close-line"></i></button>
</span>
<span class="input-group input-group--lg">
<span class="input-affix"><i class="ri-search-line"></i></span>
<input class="form-input" type="search" placeholder="Large group" aria-label="Large group" />
<button class="btn btn-icon" type="button" aria-label="Clear"><i class="ri-close-line"></i></button>
</span>
</div>
Validation
The error border keys off aria-invalid, not a class. The attribute is what a
screen reader announces, so styling a separate class would let the two disagree — and the
reader who most needs the message is the one who would not get it. Point
aria-describedby at the message so it is read as part of the field.
.form-warning is for a value that is accepted but unwise.
Not a complete address — nothing would be delivered.
Beyond the 365-day archive window, so the tail cannot be restored.
<div style="max-width:460px; display:flex; flex-direction:column; gap:16px">
<div class="form-field">
<label class="form-label form-label--required" for="f-mail">Notification address</label>
<input class="form-input" id="f-mail" type="email" required
aria-invalid="true" aria-describedby="f-mail-err" value="ops.example" />
<p class="form-error" id="f-mail-err">
<i class="ri-error-warning-line"></i>
<span>Not a complete address — nothing would be delivered.</span>
</p>
</div>
<div class="form-field">
<label class="form-label" for="f-retain">Retention</label>
<input class="form-input" id="f-retain" type="text" value="400 days"
aria-describedby="f-retain-warn" />
<p class="form-warning" id="f-retain-warn">
<i class="ri-alert-line"></i>
<span>Beyond the 365-day archive window, so the tail cannot be restored.</span>
</p>
</div>
</div>
Input groups
One border around a control and its affixes. The affixes are real siblings rather than padding plus a background image, so a button stays clickable and a unit stays selectable. The focus ring moves out to the group and the inner control gives up its own, or the group draws two rings nested inside each other.
<div style="max-width:460px; display:flex; flex-direction:column; gap:16px">
<div class="form-field">
<label class="form-label" for="f-search">Search</label>
<div class="input-group">
<span class="input-affix"><i class="ri-search-line"></i></span>
<input class="form-input" id="f-search" type="search" placeholder="Host or order number" />
</div>
</div>
<div class="form-field">
<label class="form-label" for="f-timeout">Timeout</label>
<div class="input-group">
<input class="form-input" id="f-timeout" type="number" value="30" />
<span class="input-affix">seconds</span>
</div>
</div>
<div class="form-field">
<label class="form-label" for="f-token">API token</label>
<div class="input-group">
<input class="form-input" id="f-token" type="text" value="np_live_8f2c…" readonly />
<button class="btn btn-icon" type="button" aria-label="Copy the API token"><i class="ri-file-copy-line"></i></button>
</div>
</div>
</div>
Read-only values
.form-value-display is the shape of an input without being one — for
values the user can read but not change. Do not use a disabled input for this; it reads
as broken rather than as informational.
<div class="field-grid" style="max-width:620px">
<div class="form-field">
<label class="form-label">Signed in as</label>
<div class="form-value-display">alex.fischer@example.com</div>
</div>
<div class="form-field">
<label class="form-label">Role</label>
<div class="form-value-display">admin</div>
</div>
</div>
Sections and actions
.form-section is a <fieldset> with the browser's border
and inset legend removed. Use the real element: the legend is then announced with every
control in the group, which a heading above a <div> is not. The fields
go in .form-section-body rather than in the fieldset directly, because a
<legend> is not a flex item and lands somewhere different in every
engine if you make the fieldset the flex container.
.form-actions closes the form. It wraps instead of overflowing, so three
buttons on a phone stack rather than pushing the commit button off-screen.
.form-actions--split puts a destructive action on the far side, where it
cannot be hit by aiming for Save, and
.form-actions--start aligns the row to the leading edge — for a form embedded
in a page rather than closing a dialog, where the far corner is nowhere in particular.
Where the group genuinely is not a set of related controls — a heading over a region rather
than over fields — use .form-section-title on a plain element instead of a
<legend>. It is the same rule, without claiming a grouping that is not
there.
<form style="max-width:520px">
<fieldset class="form-section">
<legend>Escalation</legend>
<p class="form-section-hint">Who is paged when nobody has acknowledged in time.</p>
<div class="form-section-body">
<div class="form-field">
<label class="form-label form-label--required" for="f-team">Team</label>
<select class="form-input form-select" id="f-team" required>
<option selected>Fulfilment</option>
<option>Network</option>
</select>
</div>
<div class="form-field">
<label class="form-label" for="f-after">Escalate after</label>
<div class="input-group">
<input class="form-input" id="f-after" type="number" value="15" />
<span class="input-affix">minutes</span>
</div>
</div>
</div>
</fieldset>
<div class="form-actions form-actions--split">
<button class="btn btn-danger" type="button"><i class="ri-delete-bin-line"></i> Delete rule</button>
<span style="display:flex; gap:8px">
<button class="btn" type="button">Cancel</button>
<button class="btn btn-primary" type="submit">Save</button>
</span>
</div>
</form>
A settings card
Fields in a card, actions in a footer row.
Notifications are suppressed during this window. The queue still updates.
<div class="card" style="max-width:520px">
<div class="card-head"><strong>Notifications</strong></div>
<div class="card-body" style="display:flex; flex-direction:column; gap:14px">
<label class="form-check"><input type="checkbox" checked /> Notify me about new pending items</label>
<div class="form-field">
<label class="form-label" for="f-quiet">Quiet hours</label>
<input class="form-input" id="f-quiet" type="text" value="22:00 – 07:00" />
<p class="form-hint">Notifications are suppressed during this window. The queue still updates.</p>
</div>
</div>
<div class="modal-footer">
<button class="btn">Reset</button>
<button class="btn btn-primary">Save</button>
</div>
</div>