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.
Text inputs: .sb-input
Section titled “Text inputs: .sb-input”<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>Textarea: .sb-textarea
Section titled “Textarea: .sb-textarea”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>Select: .sb-select
Section titled “Select: .sb-select”<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. |
Input group: .sb-input-group
Section titled “Input group: .sb-input-group”Joins an input with addons or buttons into one bordered control.
<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. |
Checkbox: .sb-checkbox
Section titled “Checkbox: .sb-checkbox”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.
Radio: .sb-radio
Section titled “Radio: .sb-radio”Group radios by name, and wrap related ones in <fieldset class="sb-field-group"> with a <legend>.
<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>Switch: .sb-switch
Section titled “Switch: .sb-switch”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>Range: .sb-range
Section titled “Range: .sb-range”<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>Choice controls reference
Section titled “Choice controls reference”| 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). |
Label and field
Section titled “Label and field”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 hassb-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-fieldis always visible.- A field containing a
:disabledcontrol dims its label and hint. - Tie the error and hint to the control with
aria-describedby.
Native behaviors
Section titled “Native behaviors”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