Form fields

ui.css · Cross-system: Cross-system reference § Forms

Overview

Native <input>, <textarea>, <select> with shared styling from /ui/ui.css. Two structural classes: .form-fields wraps a form, .form-group wraps each field + its label.

For customizable selects with option groups, use <x-select>.

Anatomy

.form-fields is a vertical flex container. Each child is a .form-group — a <p> or <fieldset> with <label> (or <legend>) followed by the input.

Indent the label and .field-help or .field-error text to align with the field's text: one border width plus --size-2, also used for .prefix icons. The border extends beyond that alignment. Apply this only beside framed fields. Radio and checkbox groups and legends over buttons keep their own alignment.

Show a value the caller may not change as a field as well, so the form keeps its shape whether or not the value can be edited. Put it in an .input-group with readonly on the input, give it a .prefix if it needs a leading icon, and keep the label and companion line where they sit for every other field. Do not replace the control with a .badge: a pill stretched across the field's width reads as a disabled control — something that could be switched on later — rather than a value that is simply stated. The states and their dimming are on Form field states, which also says what such a field does under the pointer and whether it stays a Tab stop.

Fields use --size-2 padding on both axes unless a specific design requires otherwise. Text line height adds space above and below the glyphs, so equal padding can still look taller than it is wide. Buttons retain --size-3 horizontal padding to give centered labels more room.

Usage

Textarea
Textarea with .font-reading
Show code

Input groups

Wrap an input together with a prefix and/or suffix decoration (icons, units, clear buttons, status indicators) using .input-group. It's a modern-CSS pattern — flex layout plus :has(:focus-visible) plus Open Props tokens — not a custom element. The group owns the border, background and focus ring; native inputs inside go transparent. <ui-combobox> or other custom fields that ship their own chrome need the raw attribute so the group owns the framing.

Any of <label>, <div role="group">, or <fieldset> can carry the class; <label> is simplest for single-field groups (implicit labelling), <fieldset> with <legend> is right when you have multiple controls sharing a group label.

$

Show code

Raw custom fields (ui-combobox, x-select)

Custom form controls like <ui-combobox> and <x-select> ship their own border, background, and internal padding. Adding raw strips that chrome so the outer .input-group owns the border and focus ring — and the control's inner horizontal padding adapts to its siblings:

No consumer CSS required — the rules live on .input-group > ui-combobox[raw] and .input-group > x-select[raw] in /ui/ui.css. Each side keys off its own sibling test: :first-child restores the left pad, :last-child the right, so a control only loses the pad on the side where a prefix or suffix already insets it. See the <ui-combobox> rich-combobox demo (prefix + combobox) and the x-select raw wrapping demo (prefix + x-select) for concrete renders.

Chips modes (<ui-combobox raw chips> or multiple) follow the same three cases. Apply padding-inline-start and -end to the host because the chip row wraps; leave the inner input unpadded. Check the prefix and suffix sides independently. A single :first-child check for both sides could remove the far-side padding from a prefixed chip row.

Field height

Every undecorated framed field draws at the same height, so a form column reads as one column whichever control a row happens to use. The height is the browser's normal line box for the field's font, plus --size-2 above and below, plus the border — 38 px for the 16 px reading font this site uses. An <input>, an <x-select>, a <ui-combobox>, a native <select> and a plain .input-group all land there. A <textarea> is the exception: it carries min-height: 12rem so a multi-line field opens at a usable size.

The native <select> is the one control the stylesheet writes that height out for. Left alone it draws 40 px in Chromium against everyone else's 38, because Chromium wraps the chosen text in an internal button box two pixels taller than the line box; Firefox draws 38 either way. Measured 2026-09-17 in Chrome 151, nothing about appearance reaches those two pixels: appearance: none still draws 40 and appearance: base-select draws 42, as do line-height: 20px, line-height: 1, field-sizing: content and align-items: center. So the select keeps appearance: auto, each browser goes on drawing its own dropdown arrow, and ui.css declares block-size: calc(var(--font-lineheight-1) * 1em + 2 * var(--size-2) + 2 * var(--border-size-1)) instead. A <select multiple> or one carrying size is a list box that sizes itself from its rows, and is left alone.

That calc is the paragraph above with one substitution: the browser's normal line box is written as --font-lineheight-1, because CSS has no portable way to read normal. The lh unit looks like the way to read it and is not: Firefox resolves lh from the fallback font's metrics and does not recompute it once the web font arrives, which put the select at 41 px on a cold load while Chromium had it at 38. 1.25 is what normal measures for this library's font at --font-size-1, so changing the font family would move the other fields and leave the select behind. That is what the browser test in public/ui/test/form/dimensions-wpt.spec.mjs is for: it compares each field against the <input> beside it rather than against a figure.

A control that builds its field from a <button> or an inner <input> has to declare line-height: normal for that to hold. font: inherit otherwise hands it the page's body line height, which is taller than the line box a native field keeps, and the row grows by the difference.

A decoration can make its group taller, and that is allowed: .prefix and .suffix are flex siblings, so a group is as tall as its tallest child. Text and icon decorations at the reading size fit inside the field height and change nothing; a graphic larger than that raises the whole row. Read the height of a decorated group from its content rather than expecting the figure above.

Show code

HTML

JavaScript

States

Focus ring is a shared rule on input, textarea, select, .btn, .btn-icon, x-select > button — see /ui/ui.css. Hover darkens the border of a field the caller can edit; readonly, disabled and locked fields keep their resting border and their focus ring (Form field states).

Validation coloring on .input-group is driven by native :user-valid / :user-invalid (Baseline 2023); a <span class="suffix state"> picks up the group's state color automatically. The full vocabulary — disabled, readonly, user-valid / user-invalid, error, flux, updating, locked — plus the ranking that settles which of them shows when several hold at once, live in Form field states.

The companion line: .field-help and .field-error

Use one <small> below the field for its hint, error or success message. Change its text and class between .field-help, .field-error and .field-success, keeping aria-describedby pointed at it. Add .info to .field-help for a line worked out from the value, or .warning for a value that is allowed but unusual; see Form field states § Info and warning lines. Set aria-invalid="true" on the input for an error; this also gives error styling the highest priority. aria-live="polite" announces updated text.

Shown to everyone on the team.

A short name may not contain spaces.

Show code

The second field's red border and icon come from aria-invalid="true". No group class or prior interaction is needed, so the same pattern can show server errors. Form field states § Asking the server demonstrates the initial hint, loading feedback and server response.

Report a failure affecting the whole submission through toast(msg, {type: 'error'}). If it must remain visible, retain an active notify({id, …, is_current}) entry for <ui-statusbar>. See Status messages § Form errors for choosing between field feedback and submission feedback.

Reading font

Textareas that hold long-form reading content use the .font-reading class (Literata serif).

Accessibility

Cross-system

See Cross-system reference § Forms for mappings to Material md-text-field (incl. prefix/suffix), Bootstrap input-group, and APG patterns.

See also