Skip to main content
Copy for LLM

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 by hand.

Before & After

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

<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:

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

Basic Usage

Field with input

We will never share your email.

Give the hint to the control, not to the wrapper

The hint is on the input above, not on the field. A control renders its own hint and points its own aria-describedby at it, so a screen reader reads the two together. The wrapper cannot do that for a control it did not render — Blade slots are opaque, so field has no way to add an attribute to whatever you put inside it.

field's own hint prop still exists, and it is the right one when the slot holds markup you wrote yourself: it renders the text with the stable id {name}-hint for you to reference. Reference it — a hint that no aria-describedby points at is never announced with the field, which is WCAG 1.3.1 and 3.3.2.

<x-wirekit::field label="Email address" name="email" hint="We will never share your email.">
    <input id="email" name="email" type="email" aria-describedby="email-hint" />
</x-wirekit::field>

Do not put the same hint on both — that emits the id twice, and a duplicate id makes the reference ambiguous.

Required Fields

Required indicator

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. An explicit error prop covers one-off messages (useful in demos, previews, or programmatic validation). The preview below puts both on the control, which is the shape that also gets the red ring, aria-invalid and the aria-describedby link; the field keeps for so its label still points at the input:

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, its aria-invalid and its aria-describedby link (the field then carries the label and for="{name}" instead of name, so it still points at the control without doing a second bag lookup), or give it to the field when a plain message below the control is enough (leave the control's name/error off) — in that second shape the message is text next to the control rather than text attached to it, so reach for it when the control is not a WireKit one and you will wire aria-describedby="{name}-error" yourself.

Field with an explicit error message

That email is already taken.

Works With Any Input

Select inside field
Textarea inside field

500 characters max.

Some WireKit controls give their input an id of their own, among them the combobox, the date picker and the multi-select. Inside a field they take the field's label as their name: the combobox below is announced as "Country of residence", and a click on the label focuses its search field.

Combobox inside 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.

Horizontal field

Shown on your public profile.

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.

Radio group inside a fieldset
Notification frequency

Choose how often we email you.

A Rich Legend

When the caption needs more than a string — a badge, an icon, emphasis — pass it as the named legend slot instead of the prop. It renders into the same <legend> element, in the same place.

Fieldset with a rich legend
Permissions Beta

Applies to every project in the workspace.

Put the legend in the slot, not in the default slot

A <legend> must be the <fieldset>'s first child to be the group's caption. <x-wirekit::field.set> puts its default slot inside a spacing wrapper, so a <x-wirekit::field.legend> written there is a grandchild — an ordinary inline box. It looks right and is wrong: the text still appears where you put it, but the <fieldset> has no accessible name and a screen reader announces nothing before the controls.

Use the legend prop, the legend slot, or <x-wirekit::field.legend> inside a <fieldset> you wrote yourself — one of the three. In debug mode, a <legend> found in the default slot is written to the log. A field.set nested in the slot does not count: its legend is the caption of its own group.

A Group Error

Some errors belong to the whole group: "choose at least one role", or a rejected entry in an array field. Pass the message as error, or give the set the name your validation uses and it reads the error bag itself. name also answers for every entry below it, so a checkbox group bound to roles shows the error Laravel files under roles.1.

The set renders the message once, in the hint's place under the legend. Every checkbox, radio and toggle inside is marked aria-invalid and points at the message through aria-describedby, next to its own hint or error. While the group is valid, they point at the set's hint the same way.

The <fieldset> points at the message too. That is what a group of anything else has: a row of buttons choosing a template takes no error of its own and cannot be marked invalid, so without a description on the container nothing in the accessibility tree reaches the sentence the reader can see. An aria-describedby you write on the set yourself is kept and appended, never replaced.

Checkbox group with a group error
Roles

Choose at least one role.

In a Livewire component, validate the array and each of its entries:

<?php

namespace App\Livewire;

use Livewire\Component;

class EditMember extends Component
{
    public array $roles = [];

    public function save(): void
    {
        $this->validate([
            'roles' => ['required', 'array', 'min:1'],
            'roles.*' => ['in:admin,editor,viewer'],
        ]);
    }
}

Then give the set the same name:

<x-wirekit::field.set legend="Roles" name="roles">
    <x-wirekit::checkbox wire:model="roles" value="admin" label="Admin" />
    <x-wirekit::checkbox wire:model="roles" value="editor" label="Editor" />
    <x-wirekit::checkbox wire:model="roles" value="viewer" label="Viewer" />
</x-wirekit::field.set>

An empty selection fails as roles and a rejected entry as roles.1, and the set shows either. Radios that share the set's name leave the message to the set, so it appears once instead of under every option.

The message is a polite live region, like a field's error. Turn that off for one set with :announce-error="false", or for a whole form with <x-wirekit::form :announce-errors="false">.

When the Error Is Not True of Every Control

A group error can belong to some of its controls and not the rest. Three events under one caption, and a rejection that only two of them can resolve: announced on the third as well, a reader hears invalid and a sentence that ticking that box will not satisfy.

:group-error="false" on a checkbox, radio or toggle declines the group's message. The control is then neither marked aria-invalid by the group nor described by it, and everything else stays: its own error, its own hint, and the set's hint while the group is valid. The set still owns its error-bag entry, so a declining control does not start showing the group's message underneath itself.

<x-wirekit::field.set legend="What should it watch?" name="watch_release">
    <x-wirekit::checkbox wire:model="watchRelease" label="Releases" />
    <x-wirekit::checkbox wire:model="watchTag" label="Tags" />
    <x-wirekit::checkbox wire:model="watchPush" label="Pushes" :group-error="false" />
</x-wirekit::field.set>

A Disabled Group

disabled on the set renders a <fieldset disabled>, and the browser disables every native control inside it: inputs, selects, textareas and buttons leave the tab order, take no input and are left out of a submitted form. The WireKit controls built partly from other elements follow the fieldset as well. The handles of a range slider leave the tab order and stop moving, the editor surface stops taking text, a click on the value of an inline edit opens nothing, a row of an open multi-select list picks nothing, and a file upload drop zone takes no file.

They follow the attribute while the page runs, so a set that Livewire disables for the length of a request is usable again once the request ends:

<x-wirekit::field.set legend="Budget" wire:loading.attr="disabled" wire:target="save">
    <x-wirekit::range-slider
        label="Budget"
        name="budget"
        :min="0"
        :max="10000"
        :min-value="$budget['min']"
        :max-value="$budget['max']"
        wire:model="budget"
    />
</x-wirekit::field.set>

As in HTML, a control inside the set's legend stays enabled, so a switch that unlocks the group can sit there.

How It Works

The wrapper coordinates three pieces:

  1. Label — Rendered via <x-wirekit::label :required="$required">. Its for points at name, the id a plain input carries, or at the element a WireKit control in the slot records when it takes the label.
  2. Slot — Your input (any WireKit form component or plain HTML).
  3. Hint / Error — Rendered with stable IDs ({name}-hint, {name}-error) for you to reference with aria-describedby. The wrapper does not attach them to the slot: a WireKit form control links its OWN hint and its OWN error, and never sees the wrapper's. Which one renders the message is therefore the same choice as which one owns the name — see the note under Error State.

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.

There are two answers, and which one you want depends on how many columns there are.

For a row of several fields, reach for align-fields on the row itself. It turns the row into three shared tracks — labels, controls, messages — and every field hands its parts to them, so a control sits on the control track whatever its neighbors carry. It is the answer built for this, and it scales past two columns without any per-column markup.

<x-wirekit::field.spacer> is the other one, and it is not a lesser version of the same thing. It 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. Two things make it the right choice sometimes: align-fields switches the row from a flex line to a grid, so align and justify stop applying — and it rides on CSS subgrid, which is above the support baseline and falls back to stacked fields where it is missing. The spacer keeps the row a flex row and works everywhere.

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.

A button that starts where the input starts

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

Two actions beside a field

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 reserveMessage — the same anchor select.md and textarea.md point at. 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, with the id {name}-hint. Not attached to the slot — reference it with aria-describedby, or give the hint to the control instead
help string|null null An explanation shown in a tooltip from a question mark beside the label. Its hidden copy has the id {name}-help, offered for the control in the slot as the hint's is: reference it in that control's aria-describedby, or give the control help itself. A click on the question mark dispatches wirekit:field-help with the field's name. See Help beside the label
error string|null null Explicit error message (overrides $errors bag)
announceError bool|null null Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). 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 caption, rendered as the <fieldset>'s first-child <legend>. Also accepts the named <x-slot:legend> for rich content
hint string|null null Optional helper text below the legend. The checkboxes, radios and toggles inside point at it while the group is valid
name string|null null The error-bag key the group answers for, including every entry below it (roles also reads roles.1). Rendered as the <fieldset>'s name attribute
error string|null null A message for the whole group. Takes precedence over the error bag and renders in the hint's place
announceError bool|null null Render the group error in an aria-live="polite" region. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error')
scope string|null null Scoped personalization name

The base, legend, hint and error blocks all resolve through the personalization chain, so a project whose group captions are one size smaller sets that once instead of per call site — see Customization for the priority chain and the Tailwind source-scanning caveat.

// config/wirekit.php
'components' => [
    'field.set' => [
        'classes' => [
            'legend' => 'mb-1 text-[length:var(--text-wk-sm)] font-[number:var(--font-wk-heading-weight)] text-[color:var(--color-wk-text)]',
        ],
    ],
],

<x-wirekit::field.spacer> props

Prop Type Default Description
scope string|null null Scoped personalization name

Slots

Slot Description
Default The control the label names: a WireKit control or one of your own.
labelAfter Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label.

Accessibility

  • The label names the control in the slot. A plain input carries the field's name as its id, and the label's for points there.
  • A WireKit control whose input carries an id of its own takes the label instead: the combobox, the multi-select, the tags input, the date picker, the file upload, the color picker, the slider, the range slider, the editor and the inline edit. The label's for points at the element the control types into. Where there is no such element (the thumbs of a range slider, the surface an editor builds in the browser), the label names the control by reference and a click on it focuses the control. Clicking the label does what clicking the label of a native field does in that browser, so it opens the file picker or the color panel.
  • The control's own name gives way to the label: the hidden name built from name, a placeholder sentence, or "Minimum" and "Maximum" on the thumbs of a range slider, which read "Price minimum" and "Price maximum" under a label reading "Price". A file upload keeps its drop zone sentence after the label.
  • A control with a label or an aria-label of its own keeps that name, and a for you pass to the field keeps your wiring. A field labels one control: the first WireKit control in the slot takes the label. For a group of controls, such as a minimum and a maximum, use <x-wirekit::field.set>, whose legend names the group.
  • Hint and error text rendered by a WireKit form control is linked via aria-describedby on that control — each of them does this independently for its own hint / error.
  • An aria-describedby you pass to a WireKit form control lands on the control itself, after the control's own hint or error id, so a notice elsewhere on the page can describe the field as well.
  • Hint and error text rendered by <x-wirekit::field> gets the stable id {name}-hint / {name}-error and nothing more: a slot is opaque, so the wrapper cannot put aria-describedby on whatever is inside it. Put the message on the control, or point at the id yourself.
  • 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.
  • <x-wirekit::field.set> emits its <legend> as the <fieldset>'s first child, which is what gives the group its accessible name. That holds for the legend prop and for the <x-slot:legend> form alike; a <legend> placed in the default slot instead is a grandchild and names nothing.
  • <x-wirekit::field.set> is the one wrapper whose controls link its messages themselves. The checkboxes, radios and toggles inside read the set through the component tree rather than through the slot: they point at its hint, or at its error while they carry aria-invalid. The description sits on each control and on the <fieldset>: a control's own description is what a screen reader reads when that control takes focus, and the group's is what it reads on entering the group — which is the only description a group of buttons or links can have at all.

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 above the slot and renders hint / error text using the same tokens as Input. 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-sm Hint + error font size
--color-wk-text-muted Hint text color
--color-wk-danger-text Error text color
--color-wk-text Body text

See Also

  • Inline Edit — edit this value in place, with an explicit confirm step

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store