SolutionsPackagesComponentsCLI — Coming soonResourcesDocumentationStorybookChangelogBlogAboutSign up freeGet early access

Documentation

Field

Pairs a form control with its label, hint, and error message, accessibly.

Open this component in Storybook

The <dsgn-field> component pairs one form control with a label, a hint, and an error message. It works with <dsgn-input> today, and other form controls adopt the same labelling contract.

Field is not a form control. It is not form-associated, has no value, and does not validate: it shows whatever error you give it.

Usage

<dsgn-field
  label="Alert email"
  hint="We warn this address 7 days before expiry."
  required
>
  <dsgn-input type="email" name="email"></dsgn-input>
</dsgn-field>

Import the component modules in your application entrypoint:

import '@ds-gn/ui/field';
import '@ds-gn/ui/input';

Playground

Props

PropTypeDefaultDescription
labelstring''Label text for the control.
hintstring''Supporting text below the control. Stays visible alongside an error.
errorstring''Error message. A non-empty value marks the control invalid.
requiredbooleanfalseMarks the control required and adds a * to the label. Reflected.
size'sm' | 'md' | 'lg''md'Size of the label, control, hint, and error: 32, 40, and 48px controls. Reflected.

Slots

SlotDescription
(default)One form control that implements the labelling contract, such as <dsgn-input>.

Events

Field fires no events. Listen on the control (for example dsgn-input).

Labelling contract

aria-labelledby and aria-describedby cannot reference IDs across shadow roots, so Field cannot point a slotted control at a label it renders itself. Instead:

  1. Field writes label, hint, error, required, invalid, and size onto the slotted control as properties. invalid is true whenever error is non-empty.
  2. The control renders the label, hint, and error inside its own shadow root, next to the native element they describe, and wires the ARIA there:
    • the <label> is associated with the native element through for;
    • aria-describedby lists the error ID first, then the hint ID, and only IDs of rendered elements;
    • aria-invalid="true" is set while invalid;
    • required sets the native required state and a * marked aria-hidden="true".
  3. Field is the source of truth for those six properties. Values set on the control directly are overwritten while it is inside a Field. Everything else (disabled, readonly, value, name, surface) stays on the control.

A screen reader then announces, in order: the label, “required”, “invalid”, the value, the error, and the hint.

Adopting the contract in a new control

A control implements the FieldControl interface from common/helpers/field-control.ts and brands its class with markFieldControl:

import {
  describedBy,
  markFieldControl,
} from '../../../common/helpers/field-control.ts';

export class Select extends Base {
  @property({ type: String }) label = '';
  @property({ type: String }) hint = '';
  @property({ type: String }) error = '';
  @property({ type: Boolean, reflect: true }) required = false;
  @property({ type: Boolean, reflect: true }) invalid = false;
  @property({ ...sizeProps }) size: SizeType = sizeValues.MD;
  // Render label/hint/error next to the native element, and set
  // aria-describedby="${describedBy(!!this.error && 'error', !!this.hint && 'hint')}"
}

markFieldControl(Select);

Field recognises the control by that brand, including a control whose module loads after Field. Each control keeps the label, hint, and error class lists in its own .styles.ts, because Tailwind compiles each component from its own src/ only. Match Input.styles.ts for the type scale.

Examples

Error and hint

<dsgn-field
  label="Alert email"
  hint="We warn this address 7 days before expiry."
  error="Enter an email like name@company.com."
  required
>
  <dsgn-input type="email" name="email" value="alex@"></dsgn-input>
</dsgn-field>

Optional field

Field does not mark optional fields. Include the marker in the label:

<dsgn-field label="Notes (optional)" hint="Visible to everyone in the workspace.">
  <dsgn-input name="notes"></dsgn-input>
</dsgn-field>

Sizes

<dsgn-field label="Adapter" size="sm"><dsgn-input></dsgn-input></dsgn-field>
<dsgn-field label="Adapter" size="md"><dsgn-input></dsgn-input></dsgn-field>
<dsgn-field label="Adapter" size="lg"><dsgn-input></dsgn-input></dsgn-field>

Validation timing

Showing errors is the consumer’s job. A common pattern is to show an error once a field has lost focus, or on submit for every field, and to move focus to the first invalid field on submit so its error is announced:

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const field = form.querySelector('dsgn-field[data-name="email"]');
  const input = field.querySelector('dsgn-input');
  if (!input.value.includes('@')) {
    field.error = 'Enter an email like name@company.com.';
    input.focus();
  }
});

Accessibility

  • Every field needs a visible label. A placeholder is not a label: it disappears on typing.
  • The hint stays visible when an error is shown, and the error is read before the hint.
  • The error pairs an icon with text, so it is never conveyed by color alone.
  • The error is not a live region, so it is not announced the moment it appears. Errors shown on blur are read when the field is next focused; on submit, move focus to the field.
  • Disabled fields dim the label along with the control; the hint keeps its color so it stays readable. Prefer readonly for values people need to read or copy.
© 2026 LightLeap Labs LLC. All rights reserved. · Terms of Service · Privacy Policy · Cookies · Contact · Support