A field is a label, a control, and — when needed — a hint or an error.
Form Controls v1 is Irozon's third canonical component family —
packages/design-system/src/components/{field,input,select}.css. Native
HTML elements throughout: an <input> stays an
<input>, a <select> stays a
<select>. State comes from native/ARIA attributes, never a
visual state class.
.iz-field — label, control, hint/error, stacked..iz-field__label — top label, associated via for/id. Canonical default; no floating labels in v1..iz-input / .iz-textarea / .iz-select — the native element itself carries the class..iz-field__hint and .iz-field__error — connected to the control via aria-describedby.<div class="iz-field">
<label class="iz-field__label" for="name">Full name</label>
<input class="iz-input" id="name" name="name" type="text" autocomplete="name" />
<p class="iz-field__hint" id="name-hint">As it appears on your ID.</p>
</div>
One quiet surface, every native type.
Height matches Button's default (44px) so a form and its submit action read as one system. Green appears only at focus — not as permanent decoration.
Focus is the moment Green shows up.
Hover is a quiet border shift — no lift, no scale, nothing a passive control should borrow from Button's interaction language. Focus uses a box-shadow ring, so nothing reflows when it appears.
disabled — muted, not-allowed cursor
readonly — full contrast, still selectable
Disabled vs. read-only
- Disabled — the control is genuinely unavailable right now. Native
disabled, muted text, reduced opacity,not-allowedcursor. - Read-only — a real value exists and stays legible; it just can't be edited here (e.g. a generated ID). Full-contrast text, no opacity cut, default cursor — deliberately not styled like disabled, so the two aren't confused.
- Prefer plain read-only text over a disabled field when the value only needs to be seen, not eventually edited by this same control.
What's wrong, and how to fix it.
aria-invalid="true" is the source of truth, not a class. Pair it with
aria-describedby pointing at the error text, and don't rely on the red
border alone — say the problem in words.
Used on your invoice.
Enter a valid email address, like name@company.com.
Timing
Validate on submit or on blur, not on every keystroke — a field turning red while someone is still mid-way through typing their email reads as the system arguing with them. Live feedback is fine when it's genuinely useful in the moment (a password-strength meter, a live character count) — it should never be the default for ordinary required-field validation.
Same surface, naturally variable height.
.iz-textarea shares typography, border, radius, focus and invalid
treatment with .iz-input. Vertical resizing stays on.
resize: vertical
Native, styled at the edges only.
A real <select> — full keyboard behavior, screen-reader support
and the mobile picker stay exactly as the browser provides them. Only the closed
field and a decorative chevron are styled; the chevron is
pointer-events: none and never intercepts a click.
Field + Input + an icon Button, composed.
The show/hide toggle isn't a new component — it's .iz-field,
.iz-input and an .iz-button[data-icon-only] positioned via
.iz-field__control-wrap. The behavior below is a small page-only
controller, kept separate from the canonical CSS.
type + aria-pressed
Semantic tokens only — no separate dark palette.
Enter a valid email address.
Do / Don't.
type and
autocomplete · explain errors in plain words · validate on submit/blur
· use a hint for genuinely useful context.
No JavaScript required for an ordinary field.
<label for="id"> — never a placeholder standing in for one.type (email, password, search) drives the right mobile keyboard and browser validation for free.aria-invalid="true" plus aria-describedby pointing at the .iz-field__error id — screen readers announce both the state and the reason.aria-describedby, even when there's no error — so the hint is read alongside the label, not just visually nearby.disabled/readonly — no ARIA substitute needed; both are already correctly exposed by every browser and screen reader.:focus-visible, never removed — meets contrast requirements in both themes and never shifts layout.rem-based tokens throughout, so text-size/zoom scaling doesn't break the layout.autocomplete value (name, email, current-password, country, …) — it's what makes browser autofill and password managers work correctly, and CSS never fights it.Canonical form components from @irozon/design-system-react.
Input, Textarea, Select, and
FormField render exactly the markup documented above —
FormField generates and wires the label/control id/
for relationship for you, and switches to the error style the moment
an error prop is passed.
import { FormField, Input, Select } from "@irozon/design-system-react";
<FormField label="Email" hint="We'll never share it">
<Input type="email" name="email" />
</FormField>
<FormField label="Currency" error={errors.currency}>
<Select name="currency">
<option value="SEK">SEK</option>
</Select>
</FormField>