Field
Field ties a form control together with its label, description, and error message — and wires up the accessibility between them. It's the wrapper you reach for whenever a control needs a label or help text.
The controls in this library are deliberately label-less and standalone-capable. Field is what coordinates them: it generates a shared id, points the label's for at the control, links the control's aria-describedby to the description and error, and cascades disabled/required/invalid down to the control — all automatically, so you never manage ids or ARIA attributes by hand.
Anatomy
Field ← provides context; sets orientation and state
├── FieldLabel ← label; auto-targets the control (shows * when required)
└── FieldContent ← groups the control with its messages
├── (your control) ← Input, Checkbox, Switch… inherits id/state from Field
├── FieldDescription ← help text; linked via aria-describedby
└── FieldError ← error message; role="alert", linked when invalidPut the control inside FieldContent alongside its FieldDescription and FieldError. The control picks up everything from the surrounding Field.
Usage
A label, a control, and a description. Notice you set no id anywhere — Field generates it and wires the label and description to the input automatically.
<script setup lang="ts">
import { ref } from "vue";
import {
Field,
FieldLabel,
FieldContent,
FieldDescription,
Input,
} from "@dlbcodes/ui";
const email = ref("");
</script>
<template>
<Field class="w-full max-w-sm">
<FieldLabel>Email</FieldLabel>
<FieldContent>
<Input v-model="email" type="email" placeholder="you@example.com" />
<FieldDescription>We'll never share your email.</FieldDescription>
</FieldContent>
</Field>
</template>
Required
Set required on the Field and the label gets a * indicator, in the theme's --destructive color. The state cascades to the control too (setting its aria-required).
<script setup lang="ts">
import { ref } from "vue";
import { Field, FieldLabel, FieldContent, Input } from "@dlbcodes/ui";
const name = ref("");
</script>
<template>
<Field required class="w-full max-w-sm">
<FieldLabel>Full name</FieldLabel>
<FieldContent>
<Input v-model="name" placeholder="Ada Lovelace" />
</FieldContent>
</Field>
</template>
Error state
Set invalid and add a FieldError. The control gets error styling and aria-invalid, and its aria-describedby now points at both the description and the error — so screen readers announce the problem. The message is shown in the --destructive color.
<script setup lang="ts">
import { ref } from "vue";
import {
Field,
FieldLabel,
FieldContent,
FieldDescription,
FieldError,
Input,
} from "@dlbcodes/ui";
const value = ref("not-an-email");
</script>
<template>
<Field invalid class="w-full max-w-sm">
<FieldLabel>Email</FieldLabel>
<FieldContent>
<Input v-model="value" type="email" />
<FieldDescription>Your work email address.</FieldDescription>
<FieldError>Please enter a valid email address.</FieldError>
</FieldContent>
</Field>
</template>
Horizontal orientation
For toggles and checkboxes, orientation="horizontal" lays the control and label out in a row instead of stacked.
<script setup lang="ts">
import { ref } from "vue";
import { Field, FieldLabel, Switch } from "@dlbcodes/ui";
const on = ref(false);
</script>
<template>
<Field orientation="horizontal">
<Switch v-model="on" />
<FieldLabel>Enable analytics</FieldLabel>
</Field>
</template>
How the wiring works
When you wrap a control in a Field:
id—Fieldgenerates one (or uses theidyou pass it) and shares it, soFieldLabel'sforand the control'sidmatch. Clicking the label focuses the control.aria-describedby— the control is linked to theFieldDescription's id, and also to theFieldError's id wheninvalid.disabled/required/invalid— set them once on theFieldand they cascade to the control and the label.
A control still works perfectly without a Field — it just falls back to its own props, and generates its own id if you don't pass one. Field is the convenience layer for the common labelled-control case.
Props
Field
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | auto | Base id for the control. Generated automatically if omitted. |
orientation | "vertical" | "horizontal" | "vertical" | Stacks the parts (vertical) or rows them (horizontal). |
invalid | boolean | false | Marks the field invalid — cascades error styling and aria-invalid. |
disabled | boolean | false | Disables the field — cascades to the control. |
required | boolean | false | Marks required — shows the label * and sets aria-required. |
class | string | — | Classes merged onto the root. |
FieldLabel
| Prop | Type | Default | Description |
|---|---|---|---|
for | string | auto | Target control id. Defaults to the Field's generated id. |
class | string | — | Classes merged onto the label. |
FieldContent, FieldDescription, FieldError
Each takes only class. FieldDescription and FieldError automatically render with the ids the control's aria-describedby expects; FieldError uses role="alert" so it's announced when it appears.
Accessibility
- The label/control association,
aria-describedby, andaria-invalidare all wired automatically — the main reason to useFieldover loose elements. FieldErrorhasrole="alert", so an error is announced when it appears.- State set on the
Field(disabled/required/invalid) cascades to the control, keeping the visual and accessible states in sync. - The error and the required marker are red, but they don't rely on color alone: the error is text, and the label carries a
*andaria-requiredon the control.