Skip to content

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 invalid

Put 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.

BasicLabel, input, and description — auto-wired.
<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).

RequiredThe required indicator, driven by the field.
<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.

ErrorInvalid state with an error message.
<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.

HorizontalA switch with an inline label.
<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 — Field generates one (or uses the id you pass it) and shares it, so FieldLabel's for and the control's id match. Clicking the label focuses the control.
  • aria-describedby — the control is linked to the FieldDescription's id, and also to the FieldError's id when invalid.
  • disabled / required / invalid — set them once on the Field and 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 ​

PropTypeDefaultDescription
idstringautoBase id for the control. Generated automatically if omitted.
orientation"vertical" | "horizontal""vertical"Stacks the parts (vertical) or rows them (horizontal).
invalidbooleanfalseMarks the field invalid — cascades error styling and aria-invalid.
disabledbooleanfalseDisables the field — cascades to the control.
requiredbooleanfalseMarks required — shows the label * and sets aria-required.
classstring—Classes merged onto the root.

FieldLabel ​

PropTypeDefaultDescription
forstringautoTarget control id. Defaults to the Field's generated id.
classstring—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, and aria-invalid are all wired automatically — the main reason to use Field over loose elements.
  • FieldError has role="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 * and aria-required on the control.