Skip to content
milan.swiss Halbton v0.1.0
Concept project

Components

Text field

The most-used component in any back-office tool, and the one where getting the error state wrong is most expensive.

When to use it

For short, free-form input. If the set of valid answers is known and small, use a Select; if it is known and very small, use radios, which show all options without a click.

When not to

  • Never without a visible label. A placeholder is not a label: it disappears the moment someone types, which is exactly when they need it, and it fails at low contrast by design.
  • Not for dates or numbers with structure. A text field asking for a date gets every format a person can invent.
  • Not sized to hint at content length. A short input for a postcode looks tidy and breaks the moment someone has a longer one.

States

We use this for the receipt only.

Enter an address in the form [email protected]

Set when the account was created.

Write the error, not the rule

"Invalid input" tells someone they are wrong and nothing else. "Enter an address in the form [email protected]" tells them what to type next. The second is barely longer and removes the guess.

Markup

default, with a hint
<div class="ht-field">
  <label class="ht-field__label" for="email">Work email</label>
  <input class="ht-field__input" id="email" type="email"
         aria-describedby="email-hint" />
  <p class="ht-field__hint" id="email-hint">
    We use this for the receipt only.
  </p>
</div>
invalid
<div class="ht-field">
  <label class="ht-field__label" for="email">Work email</label>
  <input class="ht-field__input" id="email" type="email"
         aria-invalid="true" aria-describedby="email-error" />
  <p class="ht-field__error" id="email-error">
    <span aria-hidden="true">!</span>
    Enter an address in the form [email protected]
  </p>
</div>

Validation timing

Validate on blur, not on every keystroke — flagging an email as invalid while someone is still typing the domain is noise. Re-validate on input only after a field has already failed once, so the error clears as soon as it is fixed rather than lingering until they leave the field again.

Accessibility

Each of these is required, not recommended.
Requirement Why
<label for> Names the field, and makes the label a click target that focuses the input — which matters most on touch.
aria-describedby Links hint and error text so both are announced with the field rather than orphaned after it.
aria-invalid The programmatic error state. Without it the red border means nothing to anyone not looking at it.
type Sets the mobile keyboard. type="email" saves a person three taps every time.

The border uses --ht-border-interactive, which is held to 3:1 for WCAG 1.4.11 non-text contrast — an input a person cannot find is not an input.