Inputs and labels

label, legend and every text-like input, the inset box-shadow outline, and the per-type handling for search, number, date and file.

base/form.css is the largest file in the layer. It styles label, legend and every input that is not a button, a checkbox, a radio or a range. Largely inspired by KNACSS.

There is no .input or .label class. The selectors are the native elements.

Labels and legends

label, legend {
  color: var(--label-color-text);
  display: block;
  font-size: var(--label-font-size);
  font-weight: var(--label-font-weight);
  line-height: var(--label-line-height);
  padding-block: var(--label-padding-block);
  padding-inline: var(--label-padding-inline);
  text-align: var(--label-text-align);
  text-transform: var(--label-text-transform);
}

display: block puts the label on its own line, which is the layout a stacked form wants. The checkbox and radio files override the padding through --label-padding-block and --label-padding-inline for their inline rows.

Which inputs this file catches

input:not(
  [type='button'],
  [type='reset'],
  [type='submit'],
  [type='checkbox'],
  [type='radio'],
  [type='range']
) { … }

Buttons are excluded because they are a component’s job, not a base style, and the three excluded control types have their own files. Everything else, text, email, password, search, number, date, time, file and the rest, gets the same box.

TokenRole
--input-color-backgroundBackground
--input-color-borderBorder colour, drawn as an inset shadow
--input-color-border-hoverHover border colour
--input-border-widthBorder thickness
--input-border-radiusCorner radius
--input-color-textText colour
--input-color-placeholderPlaceholder colour
--input-paddingPadding
--form-transitionTransition, under prefers-reduced-motion: no-preference

The border is an inset shadow

border: 0;
box-shadow: inset 0 0 0 var(--input-border-width) var(--input-color-border);

This is the convention across every text-like control in the layer: input, textarea, select, checkbox and radio.

A real border occupies space, so thickening it on hover or focus moves the content by a pixel and nudges everything after it. An inset shadow is painted inside the box and takes no space at all, so a state change is purely a colour change and the layout never moves.

The switch and the range thumb are the two exceptions. Both need a real border, because their outline is part of a shape that moves.

::placeholder sets opacity: 100% explicitly, because Firefox applies its own opacity to placeholder text and the token colour would otherwise be lightened twice.

Per-type handling

TypeAdjustment
searchappearance: textfield, and the four WebKit decoration and cancel pseudo-elements neutralised
numberappearance: textfield, and the spin buttons removed
date, time, datetime-localcursor: pointer, and a calendar picker indicator that goes from 60% to full opacity on hover and focus
filecursor: pointer, a tighter padding, and a styled ::file-selector-button

The file selector button reads --form-color-accent and --form-color-on-accent, with --form-color-accent-hover on hover, so it matches the accent used by the checkbox tick without being a button component.

Most of these are wrapped in :where(), which contributes no specificity, so a component overrides them with a single class.

States

&:disabled       { opacity: var(--opacity-disabled); }
&:focus-visible  { outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-color-outline); }

Hover sits inside @media (hover: hover) and (pointer: fine) and excludes :disabled, so a disabled field never lights up and a touch device never leaves a field looking hovered.

What is not here

No layout. This file styles controls, never their arrangement. The two-column grid, the help text and the check row belong to .form in @uncinq/css-components.

No validation styling. :invalid matches an empty required field before the user has typed anything, so styling it in a base layer marks a pristine form as wrong. Style [aria-invalid='true'] in your own layer, set from your own validation code.

No accessible names. Every control needs a <label for> pointing at its id, or a .visually-hidden label when the design has no room for a visible one.