Skip to content

Forms

@static-bloom/css styles the native form controls rather than replacing them, so labels, validation, name/value submission and password managers all keep working.

Every control needs an accessible name: a <label for> (or a wrapping <label>) or an aria-label. Placeholders are not labels.

<div class="sb-stack">
<div class="sb-field">
<label class="sb-label" for="in-sm">Small</label>
<input class="sb-input sb-input-sm" id="in-sm" placeholder="2rem tall" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-md">Medium (default)</label>
<input class="sb-input" id="in-md" placeholder="2.5rem tall" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-lg">Large</label>
<input class="sb-input sb-input-lg" id="in-lg" placeholder="3rem tall" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-invalid">Invalid</label>
<input class="sb-input" id="in-invalid" aria-invalid="true" value="not-an-email" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-disabled">Disabled</label>
<input class="sb-input" id="in-disabled" disabled value="Read by nobody" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-readonly">Read-only</label>
<input class="sb-input" id="in-readonly" readonly value="NC-77-0042" />
</div>
</div>
Class Effect
sb-input Text-like <input>: sunken fill, hairline border, focus border, ring and bloom. 2.5rem tall.
sb-input-sm 2rem tall. Also sizes sb-select.
sb-input-md 2.5rem tall (the default, listed for symmetry).
sb-input-lg 3rem tall. Also sizes sb-select.
Selector Effect
:hover Brighter border.
:focus-visible Primary border, ring and bloom.
[aria-invalid="true"] Danger border.
:disabled Dimmed, no pointer feedback.
:read-only Read-only inputs (not disabled) get their own muted look.
[type="number"] Monospace, tabular figures.
[type="file"] The file button is themed.
<div class="sb-stack">
<div class="sb-field">
<label class="sb-label" for="in-num">Number (monospace)</label>
<input class="sb-input" id="in-num" type="number" value="1337" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-search">Search</label>
<input class="sb-input" id="in-search" type="search" placeholder="Search jobs" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-pass">Password</label>
<input class="sb-input" id="in-pass" type="password" value="hunter2hunter2" autocomplete="off" />
</div>
<div class="sb-field">
<label class="sb-label" for="in-file">File</label>
<input class="sb-input" id="in-file" type="file" />
</div>
</div>

sb-textarea shares the input look and states ([aria-invalid="true"], :disabled, focus).

<div class="sb-stack">
<div class="sb-field">
<label class="sb-label" for="ta">Message</label>
<textarea class="sb-textarea" id="ta" placeholder="Leave a note for the fixer">Netrunner wanted. Pay in eddies, half up front.</textarea>
</div>
<div class="sb-field">
<label class="sb-label" for="ta-invalid">Invalid</label>
<textarea class="sb-textarea" id="ta-invalid" aria-invalid="true">Too short</textarea>
</div>
<div class="sb-field">
<label class="sb-label" for="ta-disabled">Disabled</label>
<textarea class="sb-textarea" id="ta-disabled" disabled>Locked</textarea>
</div>
</div>
<div class="sb-stack">
<div class="sb-field">
<label class="sb-label" for="sel">Flavor</label>
<select class="sb-select" id="sel">
<option>Dusk</option>
<option selected>Midnight</option>
<option>Blackout</option>
<option>Daybreak</option>
</select>
</div>
<div class="sb-field">
<label class="sb-label" for="sel-sm">Small</label>
<select class="sb-select sb-input-sm" id="sel-sm"><option>Dusk</option><option>Midnight</option></select>
</div>
<div class="sb-field">
<label class="sb-label" for="sel-multi">Multiple</label>
<select class="sb-select" id="sel-multi" multiple size="3">
<option selected>Plasma</option>
<option>Ion</option>
<option selected>Acid</option>
<option>Sodium</option>
</select>
</div>
<div class="sb-field">
<label class="sb-label" for="sel-invalid">Invalid</label>
<select class="sb-select" id="sel-invalid" aria-invalid="true"><option>Pick one</option></select>
</div>
</div>
Class Effect
sb-select Themed native <select> with a drawn chevron. Honors sb-input-sm and sb-input-lg, [multiple], [aria-invalid="true"] and :disabled.

Joins an input with addons or buttons into one bordered control.

https:// .lua
EDS
<div class="sb-stack">
<div class="sb-input-group">
<span class="sb-input-addon">https://</span>
<input class="sb-input" aria-label="Domain" value="open2077.net" />
<span class="sb-input-addon">.lua</span>
</div>
<div class="sb-input-group">
<span class="sb-input-addon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="16" height="16"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg></span>
<input class="sb-input" aria-label="Search" placeholder="Search" />
<button type="button" class="sb-btn">Go</button>
</div>
<div class="sb-input-group sb-input-group-sm">
<input class="sb-input" aria-label="Amount" value="1250" />
<span class="sb-input-addon">EDS</span>
<button type="button" class="sb-btn sb-btn-outline">Pay</button>
</div>
<div class="sb-input-group sb-input-group-lg">
<input class="sb-input" aria-label="Host" placeholder="host:port" />
<button type="button" class="sb-btn sb-btn-soft">Connect</button>
</div>
</div>
Class Effect
sb-input-group Flex row that merges the borders of child inputs, selects and buttons.
sb-input-group-sm Small controls inside the group.
sb-input-group-lg Large controls inside the group.
sb-input-addon Static text or icon cell next to the input.

Put the input inside a label.sb-choice so the label text is part of the hit target.

<div class="sb-stack">
<label class="sb-choice"><input type="checkbox" class="sb-checkbox" /> Unchecked</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox" checked /> Checked</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox" data-indeterminate /> Indeterminate</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox" aria-invalid="true" /> Invalid</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox" disabled /> Disabled</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox" disabled checked /> Disabled checked</label>
</div>
<div class="sb-cluster sb-cluster-4">
<label class="sb-choice"><input type="checkbox" class="sb-checkbox sb-checkbox-sm" checked /> sm</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox" checked /> md</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox sb-checkbox-lg" checked /> lg</label>
</div>
<div class="sb-cluster sb-cluster-4">
<label class="sb-choice"><input type="checkbox" class="sb-checkbox sb-intent-secondary" checked /> secondary</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox sb-intent-success" checked /> success</label>
<label class="sb-choice"><input type="checkbox" class="sb-checkbox sb-intent-danger" checked /> danger</label>
</div>

The indeterminate state is a DOM property with no HTML attribute. The demo’s data-indeterminate attribute is not styled; a small script sets input.indeterminate = true (see Native behaviors). The CSS keys on :indeterminate.

Group radios by name, and wrap related ones in <fieldset class="sb-field-group"> with a <legend>.

Spawn point
<div class="sb-stack">
<fieldset class="sb-field-group">
<legend>Spawn point</legend>
<label class="sb-choice"><input type="radio" class="sb-radio" name="spawn" checked /> Watson</label>
<label class="sb-choice"><input type="radio" class="sb-radio" name="spawn" /> Westbrook</label>
<label class="sb-choice"><input type="radio" class="sb-radio" name="spawn" aria-invalid="true" /> Invalid</label>
<label class="sb-choice"><input type="radio" class="sb-radio" name="spawn" disabled /> Disabled</label>
</fieldset>
<div class="sb-cluster sb-cluster-4">
<label class="sb-choice"><input type="radio" class="sb-radio sb-radio-sm" name="size" checked /> sm</label>
<label class="sb-choice"><input type="radio" class="sb-radio" name="size" /> md</label>
<label class="sb-choice"><input type="radio" class="sb-radio sb-radio-lg" name="size" /> lg</label>
</div>
</div>

A switch is a checkbox with role="switch"; the CSS targets input[type="checkbox"][role="switch"].

<div class="sb-stack">
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch" /> Off</label>
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch" checked /> On</label>
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch" disabled /> Disabled</label>
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch" disabled checked /> Disabled on</label>
</div>
<div class="sb-cluster sb-cluster-4">
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch sb-switch-sm" checked /> sm</label>
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch" checked /> md</label>
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch sb-switch-lg" checked /> lg</label>
</div>
<div class="sb-cluster sb-cluster-4">
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch sb-intent-secondary" checked /> secondary</label>
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch sb-intent-success" checked /> success</label>
<label class="sb-choice"><input type="checkbox" role="switch" class="sb-switch sb-intent-danger" checked /> danger</label>
</div>
<div class="sb-stack">
<div class="sb-field">
<label class="sb-label" for="rg">Volume</label>
<input type="range" class="sb-range" id="rg" value="40" />
</div>
<div class="sb-field">
<label class="sb-label" for="rg-success">Success intent</label>
<input type="range" class="sb-range sb-intent-success" id="rg-success" value="70" />
</div>
<div class="sb-field">
<label class="sb-label" for="rg-disabled">Disabled</label>
<input type="range" class="sb-range" id="rg-disabled" value="25" disabled />
</div>
</div>
Class Effect
sb-choice Inline-flex label that aligns a checkbox, radio or switch with its text.
sb-checkbox, sb-radio, sb-switch, sb-range Themed native control (appearance: none). :checked, :indeterminate (checkbox), :disabled, :focus-visible and [aria-invalid="true"] are styled.
sb-checkbox-sm, sb-checkbox-lg Smaller and larger checkbox.
sb-radio-sm, sb-radio-lg Smaller and larger radio.
sb-switch-sm, sb-switch-lg Smaller and larger switch.
sb-intent-secondary, sb-intent-success, sb-intent-danger Recolors a checkbox, radio, switch or range (default is primary).

sb-label styles a <label> (or any element acting as one). sb-label-required appends a marker. sb-field stacks a label, a control, a hint and an error with consistent spacing.

Shown to other players in the lobby.

Handle is already taken.

We only use it for password resets.

Enter a full address.

Hidden while the field is invalid.

Pick a plan to continue.

The label and hint dim with the control.

<div class="sb-stack">
<div class="sb-field">
<label class="sb-label" for="f-handle">Handle</label>
<input class="sb-input" id="f-handle" aria-describedby="f-handle-hint" placeholder="e.g. v_silverhand" />
<p class="sb-field-hint" id="f-handle-hint">Shown to other players in the lobby.</p>
<p class="sb-field-error">Handle is already taken.</p>
</div>
<div class="sb-field">
<label class="sb-label sb-label-required" for="f-email">Email</label>
<input class="sb-input" id="f-email" type="email" aria-invalid="true" aria-describedby="f-email-err" value="johnny@" />
<p class="sb-field-hint">We only use it for password resets.</p>
<p class="sb-field-error" id="f-email-err">Enter a full address.</p>
</div>
<div class="sb-field sb-field-invalid">
<label class="sb-label" for="f-plan">Plan</label>
<select class="sb-select" id="f-plan"><option>Choose</option></select>
<p class="sb-field-hint">Hidden while the field is invalid.</p>
<p class="sb-field-error">Pick a plan to continue.</p>
</div>
<div class="sb-field">
<label class="sb-label" for="f-locked">Disabled field</label>
<input class="sb-input" id="f-locked" disabled value="Locked by admin" />
<p class="sb-field-hint">The label and hint dim with the control.</p>
</div>
<div class="sb-field sb-field-row">
<input type="checkbox" role="switch" class="sb-switch" id="f-switch" checked />
<label class="sb-label" for="f-switch">Row layout</label>
</div>
</div>
Class Effect
sb-label Form label type.
sb-label-required Adds a required marker after the label text. Add required or aria-required to the control too.
sb-field Vertical stack of label, control, hint and error.
sb-field-row Horizontal field, for a switch or checkbox beside its label.
sb-field-hint Helper text under the control.
sb-field-error Error text, hidden until the field is invalid.
sb-field-invalid Marks the whole field invalid without an aria-invalid on the control.
sb-field-group <fieldset> for related choices; its <legend> is styled as an eyebrow.

Validity is driven by markup:

  • A field is invalid when a child control has aria-invalid="true", or when the field has sb-field-invalid. Then the label turns danger, the hint hides and the error shows.
  • .sb-field-error[data-visible] shows an error regardless of control state, and an error outside any .sb-field is always visible.
  • A field containing a :disabled control dims its label and hint.
  • Tie the error and hint to the control with aria-describedby.

The forms need no script apart from the indeterminate checkbox. Use the platform for everything else: required, pattern, min/max, form submission and :user-invalid work as usual.

for (const box of document.querySelectorAll('[data-indeterminate]')) box.indeterminate = true