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
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | '' | Label text for the control. |
| hint | string | '' | Supporting text below the control. Stays visible alongside an error. |
| error | string | '' | Error message. A non-empty value marks the control invalid. |
| required | boolean | false | Marks 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
| Slot | Description |
|---|---|
| (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:
- Field writes
label,hint,error,required,invalid, andsizeonto the slotted control as properties.invalidistruewhenevererroris non-empty. - 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 throughfor; aria-describedbylists the error ID first, then the hint ID, and only IDs of rendered elements;aria-invalid="true"is set while invalid;requiredsets the nativerequiredstate and a*markedaria-hidden="true".
- the
- 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
readonlyfor values people need to read or copy.