Form guidelines

Native form controls in the light DOM with duo-field — markup order, helpers, errors, and accessibility.

Form guidelines

Forms should use predictable structure, native controls in the light DOM, and accessibility preserved end-to-end.

See also the Field component reference.

Primary field primitive: <duo-field>

<duo-field> wraps native input, textarea, select, checkbox, and radio elements. It does not replace those controls or expose a parallel input API.

  • Do not add Duo field CSS classes in product markup — duo-field applies internal duo-field__* classes to direct children only at runtime.
  • Slotted custom controls (duo-file-input, duo-otp-input, …) receive the host’s invalid / disabled from duo-field automatically.

Supported markup (direct children only)

Children must be direct descendants of <duo-field>. Order is part of the contract.

Text-like field (input, textarea, select)

Order: label → control → optional p / small (helper or error).

<duo-field>
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email" />
<p>We'll send a code to your email.</p>
</duo-field>

Invalid state:

<duo-field invalid>
<label for="email">Email</label>
<input id="email" type="email" aria-describedby="email-error" aria-invalid="true" />
<p id="email-error" role="alert">Enter a valid email address.</p>
</duo-field>

Use a plain <p> or <small> for helper text — not custom CSS classes on hints.

Checkbox

Order: input[type=checkbox] → label → optional helper/error.

<duo-field>
<input id="remember" type="checkbox" name="remember" />
<label for="remember">Remember me on this device</label>
</duo-field>

Radio

One radio + label per row when using separate fields:

<duo-field>
<input id="contact-email" type="radio" name="contact-method" value="email" />
<label for="contact-email">Email</label>
</duo-field>

Unsupported

Checkbox or radio nested inside label is not supported.

<!-- Not supported -->
<duo-field>
<label>
<input type="checkbox" />
<span>Remember me</span>
</label>
</duo-field>

Use the sibling pattern above instead.

Helper and error text

Optional p / small nodes may be styled using:

  • role="alert", or id containing error → error styling
  • id containing hint → hint styling
  • id containing description / desc → description styling
  • Otherwise: before the first control → description; after → hint

Set aria-describedby / aria-invalid on the native control when needed.

Host attributes (invalid, disabled, required)

  • invalid — Invalid styling on the host and control chrome. Does not replace aria-invalid logic.
  • disabled — Disables direct-child controls; clearing the host restores prior disabled state.
  • required — Marks the primary text-like control required; clearing restores prior required state.

CSS import order

Load tokens → @duo/styles → @duo/ui so variables and field chrome match.

import "@duo/tokens/css";
import "@duo/styles";
import "@duo/ui";

When to add another component

Keep duo-field for simple fields. Add dedicated components for complex widgets (combobox, date picker, file upload, …) only when justified.