---
title: Field
description: Form field wrapper with label, hint, and error
visibility: guest
draft: false
---

# Field

A wrapper that composes label, hint text, error message, and required indicator around any form input. Eliminates the boilerplate of wiring up `for`/`id` matching and `aria-describedby` by hand.

## Before & After

Without the wrapper, every form field needs 5+ lines of manual composition:

```blade
<div class="space-y-1.5">
    <x-wirekit::label for="email" required>Email</x-wirekit::label>
    <x-wirekit::input name="email" type="email" id="email" />
    @error('email')<p class="...">{{ $message }}</p>@enderror
</div>
```

With the wrapper, it collapses to:

```blade
<x-wirekit::field label="Email" name="email" required>
    <x-wirekit::input name="email" type="email" />
</x-wirekit::field>
```

## Basic Usage

:::preview{title="Field with input"}
<x-wirekit::field label="Email address" name="email" hint="We will never share your email.">
    <x-wirekit::input name="email" type="email" placeholder="Your email address" />
</x-wirekit::field>
:::

## Required Fields

:::preview{title="Required indicator"}
<x-wirekit::field label="Full name" name="name" required>
    <x-wirekit::input name="name" />
</x-wirekit::field>
:::

The red asterisk is marked `aria-hidden="true"` — screen readers rely on the `required` attribute on the input itself (pass it through as usual: `<x-wirekit::input ... required />`).

## Error State

The wrapper automatically reads from Laravel's `$errors` bag when you pass a `name`, so after a failed submission the matching message renders below the input — no manual `@error` directive needed. You can also pass an explicit `error` prop for one-off messages (useful in demos, previews, or programmatic validation):

> **Own the error on ONE level.** The form controls (`input`, `select`, `textarea`, `checkbox`, `toggle`) ALSO read the `$errors` bag by their own `name` and render their own message + red ring + `aria-invalid`. If you give the same `name` (or an explicit `error`) to BOTH the `field` and the control it wraps, the message renders twice. Pick one: give `name`/`error` to the **control** when you want its red ring and `aria-invalid` treatment (use the `field` for the label + hint only, without a `name`), or give it to the **field** when a plain message below the control is enough (leave the control's `name`/`error` off).

:::preview{title="Field with an explicit error message"}
<x-wirekit::field label="Email" name="field-error-demo" error="That email is already taken.">
    <x-wirekit::input name="field-error-demo" type="email" placeholder="Your email address" />
</x-wirekit::field>
:::

## Works With Any Input

:::preview{title="Select inside field"}
<x-wirekit::field label="Country" name="country" required>
    <x-wirekit::select name="country">
        <option value="">— Select a country —</option>
        <option value="DE">Germany</option>
        <option value="CH">Switzerland</option>
        <option value="AT">Austria</option>
    </x-wirekit::select>
</x-wirekit::field>
:::

:::preview{title="Textarea inside field"}
<x-wirekit::field label="Feedback" name="feedback" hint="500 characters max.">
    <x-wirekit::textarea name="feedback" rows="4" />
</x-wirekit::field>
:::

## Horizontal Orientation

Set `orientation="horizontal"` to place the label in a column beside the control instead of above it — the classic settings-form layout. The error/hint messages stay aligned under the control.

:::preview{title="Horizontal field"}
<x-wirekit::field label="Display name" name="display" orientation="horizontal" hint="Shown on your public profile.">
    <x-wirekit::input name="display" />
</x-wirekit::field>
:::

## Grouped Fields (Fieldset + Legend)

Wrap a set of related controls — most often a radio or checkbox group — in `<x-wirekit::field.set>`. It renders a native `<fieldset>` with a `<legend>`, the WCAG-recommended grouping pattern: the legend is announced before each control in the set. Pass `legend` (and optional `hint`) as props, or use `<x-wirekit::field.legend>` inside the slot for rich legend content.

:::preview{title="Radio group inside a fieldset"}
<x-wirekit::field.set legend="Notification frequency" hint="Choose how often we email you.">
    <x-wirekit::radio name="freq" value="realtime" label="Real-time" checked />
    <x-wirekit::radio name="freq" value="daily" label="Daily digest" />
    <x-wirekit::radio name="freq" value="weekly" label="Weekly summary" />
</x-wirekit::field.set>
:::

## How It Works

The wrapper coordinates three pieces:

1. **Label** — Rendered via `<x-wirekit::label :for="$name" :required="$required">`.
2. **Slot** — Your input (any WireKit form component or plain HTML).
3. **Hint / Error** — Rendered with stable IDs (`{name}-hint`, `{name}-error`) that individual inputs already reference via `aria-describedby`.

## Design Decisions

- **Why does the child input still need its own `name`?** The wrapper doesn't inject attributes into its slot — Blade slots are opaque. You pass `name` to both the field and the input. The wrapper uses it for the label's `for` attribute and for the `$errors` lookup; the input uses it for submission.
- **Why isn't there a `size` prop?** Size is visual and belongs on the actual input (`<x-wirekit::input size="lg">`), not the wrapper.

## Lining a Control Up With a Field

A toolbar is usually a field **plus** something beside it — a button, a second control. Put
them in a row and the neighbor sits a label-height too high: the field's control starts below
its label, and the neighbor starts at the top of the row.

`items-end` does not fix it, because it follows the field as the field grows. `items-start`
does not either, because the first thing in the field's stack is the **label**, not the
control.

`<x-wirekit::field.spacer>` puts a label-shaped blank where the label would go, so both columns
begin with the same box and `items-start` lines the controls up.

Put it in a `<x-wirekit::field>` of its own. A field is what sets the distance between a label
and the control under it, so a column that borrows the label's height has to borrow that
distance too — otherwise the top edges match and everything below them is off by that gap. Use
the field itself rather than a stack with a chosen gap: it is the same spacing, from the
component that defines it, and it stays right when that spacing changes.

:::preview{title="A button that starts where the input starts"}
<x-wirekit::row class="items-start" gap="sm">
    <x-wirekit::field label="Project name" name="project">
        <x-wirekit::input name="project" placeholder="Atlas" />
    </x-wirekit::field>

    <x-wirekit::field>
        <x-wirekit::field.spacer />
        <x-wirekit::button>Save</x-wirekit::button>
    </x-wirekit::field>
</x-wirekit::row>
:::

More than one control goes in a row inside that same field, so the group still starts on the
input's line:

:::preview{title="Two actions beside a field"}
<x-wirekit::row class="items-start" gap="sm">
    <x-wirekit::field label="Project name" name="project-2">
        <x-wirekit::input name="project-2" placeholder="Atlas" />
    </x-wirekit::field>

    <x-wirekit::field>
        <x-wirekit::field.spacer />
        <x-wirekit::row gap="sm">
            <x-wirekit::button>Save</x-wirekit::button>
            <x-wirekit::button intent="neutral" surface="outline">Cancel</x-wirekit::button>
        </x-wirekit::row>
    </x-wirekit::field>
</x-wirekit::row>
:::

It renders the real `<x-wirekit::label>`, not a hand-written stand-in with copied classes —
so it stays exactly as tall as a label when the typography changes. A copy would drift, and
the drift would look like a small misalignment rather than like a stale duplicate.

It is `aria-hidden`: a screen reader announcing an empty label would be describing a layout
decision as if it were content.

This is the other half of `reserve-message`. That one holds the space **below** a control so a
field does not shove its neighbors down when validation fires; this one holds the space
**above** so two columns start on the same line.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | string\|null | `null` | Label text (omit to skip label rendering) |
| `name` | string\|null | `null` | Used for `for="{name}"` AND `$errors->has(name)` lookup |
| `hint` | string\|null | `null` | Helper text below the input |
| `error` | string\|null | `null` | Explicit error message (overrides `$errors` bag) |
| `announceError` | bool | `true` | Render the error as an ARIA live region (`aria-live="polite"`) so a dynamically appearing validation error is announced to screen readers. Set `false` when the page runs its own live region |
| `required` | bool | `false` | Renders red asterisk next to label |
| `for` | string\|null | `null` | Custom `for` target (defaults to `name`) |
| `orientation` | string | `'vertical'` | `vertical` (label above) or `horizontal` (label beside the control) |
| `scope` | string\|null | `null` | Scoped personalization name |

### `<x-wirekit::field.set>` props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `legend` | string\|null | `null` | Group label rendered as `<legend>` (or use `<x-wirekit::field.legend>` in the slot) |
| `hint` | string\|null | `null` | Optional helper text below the legend |
| `scope` | string\|null | `null` | Scoped personalization name |

### `<x-wirekit::field.spacer>` props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `scope` | string\|null | `null` | Scoped personalization name |

## Accessibility

- `for` / `id` matching is automatic — clicking the label always focuses the input.
- Error messages are linked via `aria-describedby` on the inner input (each WireKit form component does this independently).
- The error message is an ARIA live region (`aria-live="polite"`) by default, so a validation error that appears after a Livewire round-trip is announced without the focus returning to the field. Disable it with `:announce-error="false"` when the page runs its own live region.
- Required indicator is `aria-hidden` — the semantic `required` attribute on the input itself is what screen readers announce.

## Keyboard Interaction

This component is a layout wrapper. Keyboard interaction is delegated to its children.

## Design Tokens

The Field wrapper is layout-only — it composes [Label](/components/label#design-tokens) above the slot and renders hint / error text using the same tokens as [Input](/components/input#design-tokens). Visual styling on the input itself comes from whichever WireKit form component you slot in.

| Token | Used for |
| --- | --- |
| `--font-wk-sans` | Hint + error font family |
| `--text-wk-md` | Hint + error font size |
| `--color-wk-text` | Body text |
| `--color-wk-danger-text` | Error message |

## See Also

- [Inline Edit](/components/inline-edit) — edit this value in place, with an explicit confirm step

## Further Reading

- [MDN: `<label>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label)
- [MDN: `aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby)
- [MDN: `aria-required`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-required)
- [WebAIM: Creating Accessible Forms](https://webaim.org/techniques/forms/)
