Forms
Every standard control is styled with no classes. Labels are block; controls take accent-color from $form-accent-color. Control font-size is max(16px, 1em) so iOS doesn't zoom on focus.
Form elements
| Type | Example |
|---|---|
| checkbox | |
| radio | |
| color | |
| date | |
| datetime-local | |
| time | |
| number | |
| password | |
| range | |
| search | |
| text | |
| select | |
| textarea |
<textarea> uses field-sizing: content where supported, so it grows to fit what's typed (rows sets the starting height); elsewhere it stays at rows height and scrolls.
Checkboxes, radios & groups
Wrap a checkbox / radio in its <label>. .form-group adds vertical spacing around a control or a checkbox row.
Both are sized in em, so they scale with whatever font-size the label sits in — no separate size classes:
Fieldsets
Group related controls in a <fieldset> with a <legend> (styled across the top border). Add .is-plain to drop the border / padding so it reads as a plain label plus its controls; add .form-group-inline for an inline row of options.
Validation states
- :user-invalid — after the user has interacted, an invalid field's border turns error. No class, no JS; unlike :invalid it doesn't flag empty required fields on load. There's no green :user-valid style.
- aria-invalid="true" — for a server- or script-rendered error; styled identically. Point aria-describedby at the message (this is what a screen reader needs).
- .form-hint — muted helper text. .form-error — the error-coloured message; add role="alert" if inserted after load.
On error, add aria-invalid="true" to the input and swap the hint for the same element restyled:
Search
Wrap a search field and its button in <search> so assistive tech exposes it as a search landmark.
Layout
Lay a form out with the grid classes for full control over sizing per breakpoint:
Or .form-row — a tighter-gutter flex row where bare .col children just share the space equally, for a quick pair of fields with no sizing to think about:
Range & select
- <input type="range"> is styled full-width; the focus ring is on the thumb, not the track.
- <select> is width: fit-content; max-width: 100% with a custom chevron. On a dark surface, recolour it with @include select-bg(#fff) or set --jf-form-select-bg-img.
Recolouring the select chevron
The chevron is drawn from $select-chevron-icon-svg, coloured by $select-chevron-color. Custom properties can't be used inside an embedded SVG data-URI, so to recolour the chevron for a given context (e.g. a dark background), use the select-bg() mixin rather than overriding a CSS variable directly.
.my-dark-section select {
@include select-bg(#fff);
}
To change the chevron's shape entirely, override $select-chevron-icon-svg before Jellyfish is imported — keep the {{color}} token in your replacement SVG so select-bg() can still substitute the colour.
The chevron is a background image, so in forced-colors modes (Windows High Contrast) it keeps rendering but in its fixed colour rather than a palette one. The <select>'s border and text take system colours as normal.
Disabled
:disabled / [disabled] dims the control and shows not-allowed — native, no class.
Overriding
- Runtime — --jf-form-input-background, --jf-form-input-border-color / -border-width / -border-style / -border-radius, --jf-form-input-placeholder-color, --jf-form-select-bg-img. These stay on :root (a bare <input> outside a <form> still needs them).
- Build — the Sass variables below.
Variables
Controls & typography
| Name | Default | Notes |
|---|---|---|
| $form-items | the text-ish input types + select + textarea | The selector stack every form rule targets. Fixed — no !default, so it can't be overridden; write your own rule for anything outside the stack. |
| $form-accent-color | get-color(primary) | accent-color for checkboxes, radios, range and progress — the browser draws the control, this tints it. |
| $form-font | token(font-primary) | font-family on every control in the stack. |
| $input-background | token(color-bg) | → --jf-form-input-background. |
| $placeholder-color | token(color-text-subtle) | → --jf-form-input-placeholder-color. |
| $form-label-size | token(font-size-base) | <label> text size. |
| $form-label-weight | get-weight(semibold) | <label> text weight. |
Borders & focus
| Name | Default | Notes |
|---|---|---|
| $border-width | 1px | Shared border primitive — used well beyond forms. See Variables. |
| $border-style | solid | Shared border primitive. |
| $border-color | get-color-role(neutral, muted) | Shared border primitive. |
| $border-radius | token(radius-sm) | Shared border primitive — the control corner radius. |
| $form-border-hover | get-color(neutral) | Border colour on hover. |
| $form-focus-border-color | get-color(neutral) | Border colour while focused. |
| $form-focus-shadow-inner | an inset bevel | Inner box-shadow on focus. |
| $form-focus-shadow-outer | an inset bevel | Outer box-shadow on focus. The two compose the pressed-in focus look; the keyboard focus ring is separate (%accessible-focus). |
Select chevron
| Name | Default | Notes |
|---|---|---|
| $select-chevron-color | get-color-raw(neutral, 500) | Colour baked into the <select> chevron data-URI. A literal, not a token — custom properties don't resolve inside an embedded SVG. Recolour per scope with select-bg(). |
| $select-chevron-icon-svg | a chevron | The chevron SVG itself. Keep the {{color}} placeholder in any replacement so select-bg() can still substitute a colour. |
Range slider
| Name | Default | Notes |
|---|---|---|
| $range-thumb-color | get-color-role(neutral, muted) | Thumb fill. |
| $range-thumb-height | get-size(1.5) (24px) | Thumb height. $range-thumb-width, -radius and $range-track-height are all derived from it, so this one value resizes the whole control. |
| $range-thumb-width | $range-thumb-height | Thumb width — square by default. |
| $range-thumb-radius | calc($range-thumb-height * 0.5) | Thumb corner radius — half its height, so a circle. |
| $range-thumb-border-width | 2px | Thumb border width. |
| $range-thumb-border-color | token(color-white) | Thumb border colour — the ring that lifts it off the track. |
| $range-thumb-shadow-size | 0px 1px | Thumb shadow offset. |
| $range-thumb-shadow-blur | 6px | Thumb shadow blur. |
| $range-thumb-shadow-color | rgba(200, 200, 200, 0.8) | Thumb shadow colour. |
| $range-track-color | get-color-role(neutral, subtle) | Track fill. |
| $range-track-color-focus | get-color-role(neutral, subtle) | Track fill while the control has focus — same as the resting colour by default (the focus ring is on the thumb). |
| $range-track-height | calc($range-thumb-height * 0.5) (12px) | Track thickness. |
| $range-track-radius | $border-radius (radius-sm) | Track corner radius. |
| $range-track-border-width | $border-width (1px) | Track border width. |
| $range-track-border-color | $border-color | Track border colour. |
| $range-track-shadow-size | 0px 0px | Track shadow offset. |
| $range-track-shadow-blur | 0px | Track shadow blur. |
| $range-track-shadow-color | transparent | Track shadow colour — off by default. |
Custom properties
| Name | Default | Notes |
|---|---|---|
| --jf-form-input-background | $input-background | Control fill. |
| --jf-form-input-border-width | $border-width | Control border width. |
| --jf-form-input-border-style | $border-style | Control border style. |
| --jf-form-input-border-color | $border-color | Control border colour. |
| --jf-form-input-border-radius | $border-radius | Control corner radius. |
| --jf-form-input-placeholder-color | $placeholder-color | ::placeholder colour. |
| --jf-form-select-bg-img | the chevron data-URI | Set by select-bg(). Assign it directly only if you already have an encoded url(). |