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
We will never share your email.
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
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::).
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$errorsbag by their ownnameand render their own message + red ring +aria-invalid. If you give the samename(or an expliciterror) to BOTH thefieldand the control it wraps, the message renders twice. Pick one: givename/errorto the control when you want its red ring, itsaria-invalidand itsaria-describedbylink (thefieldthen carries the label andfor="{name}"instead ofname, 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'sname/erroroff) — 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 wirearia-describedby="{name}-error"yourself.
That email is already taken.
Works With Any Input
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.
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.
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::. 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.
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.
A <legend> must be the <fieldset>'s first child to be the group's caption. <x-wirekit:: puts its default slot inside a spacing wrapper, so a <x-wirekit:: 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:: 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.
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::.
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:
- Label — Rendered via
<x-wirekit::. Itslabel :required="$required"> forpoints atname, the id a plain input carries, or at the element a WireKit control in the slot records when it takes the label. - Slot — Your input (any WireKit form component or plain HTML).
- Hint / Error — Rendered with stable IDs (
{name}-hint,{name}-error) for you to reference witharia-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 thename— 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 passnameto both the field and the input. The wrapper uses it for the label'sforattribute and for the$errorslookup; the input uses it for submission. - Why isn't there a
sizeprop? Size is visual and belongs on the actual input (<x-wirekit::), not the wrapper.input size="lg">
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:: 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:: 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.
More than one control goes in a row inside that same field, so the group still starts on the input's line:
It renders the real <x-wirekit::, 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
nameas its id, and the label'sforpoints 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
forpoints 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
labelor anaria-labelof its own keeps that name, and aforyou 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::, whose legend names the group.field. set> - Hint and error text rendered by a WireKit form control is linked via
aria-describedbyon that control — each of them does this independently for its ownhint/error. - An
aria-describedbyyou 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::gets the stable idfield> {name}-hint/{name}-errorand nothing more: a slot is opaque, so the wrapper cannot putaria-describedbyon 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 semanticrequiredattribute on the input itself is what screen readers announce. <x-wirekit::emits itsfield. set> <legend>as the<fieldset>'s first child, which is what gives the group its accessible name. That holds for thelegendprop and for the<x-slot:legend>form alike; a<legend>placed in the default slot instead is a grandchild and names nothing.<x-wirekit::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 carryfield. set> 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