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

Every native form control, styled with no classes
TypeExample
checkbox
radio
color
date
datetime-local
time
email
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.

Sign in
Contact preference

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:

Type something that isn't an email, then blur the field.

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

Form variables — Controls & typography
NameDefaultNotes
$form-itemsthe text-ish input types + select + textareaThe 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-colorget-color(primary)accent-color for checkboxes, radios, range and progress — the browser draws the control, this tints it.
$form-fonttoken(font-primary)font-family on every control in the stack.
$input-backgroundtoken(color-bg)--jf-form-input-background.
$placeholder-colortoken(color-text-subtle)--jf-form-input-placeholder-color.
$form-label-sizetoken(font-size-base)<label> text size.
$form-label-weightget-weight(semibold)<label> text weight.

Borders & focus

Form variables — Borders & focus
NameDefaultNotes
$border-width1pxShared border primitive — used well beyond forms. See Variables.
$border-stylesolidShared border primitive.
$border-colorget-color-role(neutral, muted)Shared border primitive.
$border-radiustoken(radius-sm)Shared border primitive — the control corner radius.
$form-border-hoverget-color(neutral)Border colour on hover.
$form-focus-border-colorget-color(neutral)Border colour while focused.
$form-focus-shadow-inneran inset bevelInner box-shadow on focus.
$form-focus-shadow-outeran inset bevelOuter box-shadow on focus. The two compose the pressed-in focus look; the keyboard focus ring is separate (%accessible-focus).

Select chevron

Form variables — Select chevron
NameDefaultNotes
$select-chevron-colorget-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-svga chevronThe chevron SVG itself. Keep the {{color}} placeholder in any replacement so select-bg() can still substitute a colour.

Range slider

Form variables — Range slider
NameDefaultNotes
$range-thumb-colorget-color-role(neutral, muted)Thumb fill.
$range-thumb-heightget-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-heightThumb width — square by default.
$range-thumb-radiuscalc($range-thumb-height * 0.5)Thumb corner radius — half its height, so a circle.
$range-thumb-border-width2pxThumb border width.
$range-thumb-border-colortoken(color-white)Thumb border colour — the ring that lifts it off the track.
$range-thumb-shadow-size0px 1pxThumb shadow offset.
$range-thumb-shadow-blur6pxThumb shadow blur.
$range-thumb-shadow-colorrgba(200, 200, 200, 0.8)Thumb shadow colour.
$range-track-colorget-color-role(neutral, subtle)Track fill.
$range-track-color-focusget-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-heightcalc($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-colorTrack border colour.
$range-track-shadow-size0px 0pxTrack shadow offset.
$range-track-shadow-blur0pxTrack shadow blur.
$range-track-shadow-colortransparentTrack shadow colour — off by default.

Custom properties

Form variables — Custom properties
NameDefaultNotes
--jf-form-input-background$input-backgroundControl fill.
--jf-form-input-border-width$border-widthControl border width.
--jf-form-input-border-style$border-styleControl border style.
--jf-form-input-border-color$border-colorControl border colour.
--jf-form-input-border-radius$border-radiusControl corner radius.
--jf-form-input-placeholder-color$placeholder-color::placeholder colour.
--jf-form-select-bg-imgthe chevron data-URISet by select-bg(). Assign it directly only if you already have an encoded url().