Irozon Design System — design foundations, tokens and components by Irozon AB.
Irozon Design System · v1.0
Components / Forms

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.

Wrapper
.iz-field — label, control, hint/error, stacked.
Label
.iz-field__label — top label, associated via for/id. Canonical default; no floating labels in v1.
Control
.iz-input / .iz-textarea / .iz-select — the native element itself carries the class.
Hint / Error
.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>
Inputs

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.

Text · placeholder shown
Email · value entered
Search · native decoration normalized
States

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.

No interaction yet
Try Tab-ing to a real field above
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-allowed cursor.
  • 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.
Hints & errors

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.

Hint — muted, concise

Enter a valid email address, like name@company.com.

Error — icon + text, not color alone

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.

Textarea

Same surface, naturally variable height.

.iz-textarea shares typography, border, radius, focus and invalid treatment with .iz-input. Vertical resizing stays on.

96px starting height · resize: vertical
Select

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.

Default
Disabled
Password

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.

Click the eye — toggles type + aria-pressed
Dark theme

Semantic tokens only — no separate dark palette.

Rest

Enter a valid email address.

Invalid
Select
Usage

Do / Don't.

Do
Always show a visible label · use the correct native type and autocomplete · explain errors in plain words · validate on submit/blur · use a hint for genuinely useful context.
Don't
Use a placeholder as the label · disable a field without explaining why · show an error before the user has had a chance to finish the field · build a custom dropdown when native Select is sufficient · put a Green border around every field at rest.
Required / optional
No automatic asterisk. If most fields on a form are required, mark the few optional ones instead — "Phone (optional)". If only a minority are required, marking those with a required indicator is fine. Pick one convention per form and stay consistent.
Accessibility

No JavaScript required for an ordinary field.

Label association
Every control has a real <label for="id"> — never a placeholder standing in for one.
Native semantics
Correct type (email, password, search) drives the right mobile keyboard and browser validation for free.
Invalid state
aria-invalid="true" plus aria-describedby pointing at the .iz-field__error id — screen readers announce both the state and the reason.
Hints
Connected the same way via aria-describedby, even when there's no error — so the hint is read alongside the label, not just visually nearby.
Disabled / read-only
Native disabled/readonly — no ARIA substitute needed; both are already correctly exposed by every browser and screen reader.
Focus visibility
A 3px Green box-shadow ring on :focus-visible, never removed — meets contrast requirements in both themes and never shifts layout.
Target size & zoom
44px control height clears the AAA touch target; sizing uses rem-based tokens throughout, so text-size/zoom scaling doesn't break the layout.
Autocomplete
Set the correct autocomplete value (name, email, current-password, country, …) — it's what makes browser autofill and password managers work correctly, and CSS never fights it.
React

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>