Skip to content

Native forms and accessibility

A notification settings app needs a checkbox list, one radio group, a switch, and a select. An agent can build each control from styled div elements, then add click handlers, keyboard support, focus state, disabled behavior, and ARIA. Or it can use the elements browsers already implement.

For generated static apps, native form controls are the right default. They reduce source lines, work with assistive technology, survive partial JavaScript failure, and give browser tests stable accessible names.

This chapter continues from Mobile navigation and motion. A sound screen stack still fails users if its controls have no labels or keyboard behavior.

Concept map

text
semantic element
  -> accessible name
  -> native state
  -> design-system style
  -> browser-test selector

One correct label helps users, tests, and generated code at the same time.

Terms

Accessible name is the text assistive technology uses to identify a control.

Native state is browser-owned state such as checked, selected, open, disabled, invalid, or pressed.

Field group is a fieldset with a legend describing related choices.

Touch target is the full clickable area, which may include a wrapping label.

Focus-visible state is the visual indicator shown for keyboard or equivalent focus navigation.

Progressive enhancement means useful content and controls exist before optional JavaScript runs.

ARIA state is an accessibility attribute used when no native element expresses the required interaction.

Start with labelled controls

Use a visible label whenever the interface permits:

html
<label>
  Display name
  <input name="displayName" autocomplete="name" required>
</label>

<fieldset>
  <legend>Digest frequency</legend>
  <label><input type="radio" name="digest" value="daily"> Daily</label>
  <label><input type="radio" name="digest" value="weekly"> Weekly</label>
</fieldset>

Wrapping the input is concise and makes the whole row clickable. An explicit for and id pair is equally valid when layout requires separate elements.

Placeholder text is not a label. It disappears during entry, often has weak contrast, and does not reliably describe errors or format. Use helper text for examples. Connect it with aria-describedby when it adds information.

Buttons need text or an accessible label. An icon-only button should still have aria-label="Delete item" or visually hidden text. Its target should remain at least 44 pixels even if the icon is 16 pixels.

Let native state drive style

Checkboxes and radios already expose checked and disabled state. A native input with role="switch" can represent a binary setting while retaining input behavior. details owns expanded state. dialog owns modality, Escape handling, initial focus rules, and focus return. progress exposes completion to assistive technology.

Style these states with selectors such as :checked, :disabled, :invalid, [aria-pressed="true"], and [open]. Do not duplicate them into custom classes unless the class represents a temporary animation phase.

Custom controls become justified when the native element cannot represent the interaction. Tabs are one example. They need a tablist, tab roles, tabpanel links, roving focus, arrow keys, Home and End, and atomic selection updates. This is exactly the kind of behavior a tested design-system helper should own.

Generated app code should not rewrite that behavior per app.

Forms should work with touch keyboards

Mobile input behavior is part of accessibility. Set appropriate type, inputmode, autocomplete, autocapitalize, and enterkeyhint. Use a 16-pixel input font on mobile to avoid unwanted iOS zoom.

Do not assume Enter should always submit. In multiline textareas it must add a line break. Chat-style composers may send on Enter for fine pointers while coarse-pointer keyboards keep Enter as a newline. Respect IME composition before interpreting key events.

Textarea resizing should stay vertical. Give it a useful minimum of roughly three lines, then cap growth only when the layout truly requires it.

Validation messages should name the problem and connect to the field. "Invalid" is weak. "Enter an amount above zero" tells the user what to do. Keep submitted data in the controls after an error.

Use native constraint validation carefully. It provides useful semantics, but browser messages vary and may not fit custom workflows. If JavaScript handles validation, set aria-invalid, attach the message with aria-describedby, and move focus only when that helps the user find the first error.

Design systems should document elements, not only classes

An agent may query checkbox, select, range, progress, or dropdown. The answer should include valid markup, state rules, and target-size expectations. Some entries need no custom class at all.

This matters because class indexes alone suggest that only classed patterns are supported. Native controls are still components. Put them in the agent contract and examples.

Browser fixtures should measure controls in light and dark schemes at phone and desktop widths. For a checkbox, measure the labelled row as the touch target, not the square input alone. Otherwise the audit will flag the design system's own correct markup.

The theme should map every native control colour to public tokens. Unstyled browser or base-framework colours can leak into range tracks, progress bars, switches, marks, and validity borders.

Accessibility checks that belong in the pipeline

Static checks can catch:

  • Missing lang on the document.
  • Missing page title or viewport metadata.
  • No h1.
  • Inputs, selects, and textareas without labels.
  • Buttons without accessible text.
  • Invalid ARIA relationships.
  • Zoom-locking viewport settings.
  • Click handlers on non-interactive elements.
  • outline: none without a focus-visible replacement.

Browser checks add evidence:

  • Keyboard traversal reaches every interactive control.
  • Focus indicators are visible.
  • Targets meet their intended size.
  • Contrast meets the release contract.
  • Dialog focus enters and returns correctly.
  • Tabs respond to arrow keys and disabled states.
  • Reduced motion removes nonessential movement.

No automated list proves accessibility. It catches repeatable errors and preserves known component behavior.

Decisions and rejected approaches

Prefer semantic HTML. Rebuilding controls with div elements adds code and misses edge cases.

Use ARIA to fill semantic gaps. ARIA should not replace a native element that already has the behavior.

Treat labelled rows as touch targets. Enlarging the checkbox graphic itself makes controls visually clumsy.

Keep focus outlines. Removing them for appearance breaks keyboard use. Style :focus-visible instead.

Use platform dialog behavior. Custom focus traps and scroll locks are hard to get right.

Document native controls in the agent API. Otherwise agents assume unsupported or invent custom markup.

Avoid a full component framework. Small static apps gain more from native controls and a tested theme than from a runtime abstraction.

Frequent failures

A label is visually near an input but not associated. Use wrapping markup or for and id.

A switch is a clickable div. Use a checkbox input with switch semantics.

Icon buttons have tooltips but no accessible name. Tooltips are not dependable names.

The audit measures only the 22-pixel checkbox. Measure the wrapping label row.

Disabled styling removes contrast so far that text becomes unreadable. Disabled does not mean invisible.

A form blocks rendering while data loads. Render useful local state first, then synchronize.

A native dialog is nested or replaced with a portal without need. Keep one top-layer interaction at a time.

Validation clears the user's input. Preserve values and report the specific correction.

Field checklist

  • Does every form control have an accessible name?
  • Are related choices grouped by fieldset and legend?
  • Are native checked, selected, open, disabled, and invalid states used?
  • Do icon-only controls have names and 44-pixel targets?
  • Is keyboard focus visible?
  • Do mobile fields use suitable input hints and a readable font size?
  • Does Enter behavior respect multiline fields and IME composition?
  • Are validation messages specific and connected to fields?
  • Do dialogs, tabs, and sheets use tested behavior helpers?
  • Does the design-system query include native element components?
  • Do static checks catch unlabeled controls and non-interactive click targets?
  • Do browser tests cover keyboard and reduced-motion behavior?

Accessible controls make an app usable. They do not decide whether it should become a public product. Launch is a different mode separates casual publication from the identity, privacy, indexing, jobs, notifications, domains, and promises of a launch.

Built from field notes on durable software systems.