Skip to main content
Copy for LLM

Input

A text input with label, error handling, hints, and prefix/suffix support.

Basic Usage

A labeled text input

With Error

Error state

This field is required.

Errors from Laravel's validation bag are shown automatically when name matches a field, also for a nested name such as items[0][qty], whose error Laravel stores as items.0.qty, and for a list such as tags[].

A field emptied after a success

A required field the reader has interacted with turns red while it is invalid. Setting its property back to '' after a successful action, the usual row that adds one entry and waits for the next, is not the reader leaving it empty, so the field keeps its resting color. The rest of the form is not reset:

public function subscribe(): void
{
    // 1. Store what the reader entered
    $this->project->subscriptions()->create(['reference' => $this->reference]);

    // 2. Empty the field for the next entry; it does not turn red
    $this->reference = '';
}

The field turns red again as soon as the reader empties it themselves, or tries to submit it empty.

With Success

Confirm a valid field with a green border and an optional confirmation message — the mirror of the error state. The field stays valid, so it never sets aria-invalid. Pass a string to show a message, or :success="true" for just the green border. error always wins when both are present.

Success / valid state

Username is available

With Hint

With a hint below the field

We'll never share your email.

Help Beside the Label

help sets a small question mark beside the label. Pointing at it or reaching it with the keyboard shows the text in a tooltip, and Escape hides it again. A screen reader hears the same text as the field's description when it reaches the field. Every field with a label takes it, from a select to a checkbox, and a field without it shows no question mark.

A question mark beside the label

The help needs a label to stand beside. With hide-label the button is left out, since nobody could see it, and the text still describes the field. Keep it to a sentence or two: what the field is for and how to fill it in. What the reader has to see without pointing at anything belongs in hint, below the field.

Explaining a Field at Length

A click on the question mark, and Enter or Space on it, dispatches wirekit:field-help from the button. The event bubbles, so a listener on window or on any element around the field hears it, and its detail carries the field's name and its label. A field without a name reports its id. Nothing listens by default, and the tooltip shows on hover and focus either way.

A page that explains a field in full, such as a setting with its current value and where that value comes from, listens for it and opens its own modal. In a Livewire component #[On] hears the event, and the keys of its detail arrive as arguments:

use Livewire\Attributes\On;

public string $explaining = '';

#[On('wirekit:field-help')]
public function explain(string $name, string $label): void
{
    $this->explaining = $name;
    $this->dispatch('wirekit-modal-show', name: 'field-help');
}
<x-wirekit::modal name="field-help">
    <x-wirekit::modal.header>About this setting</x-wirekit::modal.header>
    <x-wirekit::modal.body>
        {{-- What your page knows about the field named in $explaining --}}
    </x-wirekit::modal.body>
</x-wirekit::modal>

Without Livewire, an Alpine listener on window does the same: x-on:wirekit:field-help.window="explain($event.detail.name)".

A Mark After the Label

The labelAfter slot sets markup after the label and its question mark, in the same row: a badge that says a setting is not decided yet, say. It stands outside the <label> and outside the question mark's button, so it joins neither the field's name nor the button's. On a narrow screen the mark moves to its own line, and the question mark stays beside its label. Every field with a label takes the slot, with help or without it.

A mark after the label
Not decided yet
New

In a Livewire view, keep the slot in place and put the condition inside it, with the @if around the badge rather than around <x-slot:labelAfter>. A toggle, a checkbox and a radio hold their control inside their label, and while the slot is passed, the markup around the control is the same with a mark and without one.

A mark that a screen reader should hear with the field belongs in hint or help as well, since the slot describes nothing. A file upload has no row above its drop zone, which is its label; put it in a field with a label, and give the field the slot.

With Button

Combine an input with a button for inline actions like newsletter sign-ups or search forms. The wrapper uses align-items: flex-start, so the button starts at the container's top while the input's control starts one label-height lower — and the button needs that height back.

Use <x-wirekit::field.spacer /> for it, not a margin. The spacer is a label-height box because it IS one: whatever a label is made of — font size, line-height, the gap under it, and every token behind those — the spacer matches it without anyone restating the composition.

A hardcoded spacer height goes wrong even when it starts out right: --text-wk-md is calc(0.875rem * var(--font-scale-wk)), so raising the font scale for accessibility moves the input row and leaves a fixed spacer behind.

Email subscribe with button

We'll send you a confirmation email.

Search with icon button

Prefix & Suffix

Prefix and suffix
€ .00
https://

Clearable & Copyable

Opt into trailing affordance buttons with the clearable and copyable flags. clearable shows an X button — visible only while the field has content — that empties the field, refocuses it, and fires input/change so wire:model stays in sync. copyable shows a copy-to-clipboard button with a brief "Copied" state. Both are off by default, so existing inputs render unchanged.

Each button is named after the field it acts on, so a form with several of them does not offer a row of buttons with one name. On the fields below they read "Clear Search", "Copy API token", "Clear Coupon code" and "Copy Coupon code". The field's name is its label, its aria-label, or the label of the field around it, and a field with none of them keeps "Clear input" and "Copy to clipboard".

The X follows the value whoever writes it. When your component empties or fills the property the field's wire:model binds, as a scan field does after each scan, the X hides or appears with it, without a keystroke.

The two buttons are 24px under a mouse. On a touch screen they take the 44px touch target at every size, and the field around them grows to match. At size="lg" they take it on every pointer, which is the size to choose for a screen worked by finger that reports a mouse, such as a till or a kiosk with one plugged in.

On a field of type="search" the browser draws a clear button of its own once the field holds text. A clearable field hides it, so the field shows one X, the kit's. Without clearable the browser's button stays, as the field's only one.

Clearable, copyable, and both

What the X clears, and what else goes with it

The X is an icon-only button, so its name is also its tooltip: "Clear" and the field's name by default. clear-label says what this field's X clears, and it becomes both.

Clearing fires input and change for the binding, and then wirekit:input-cleared on the field, once, with the field's name in detail.name. It bubbles, so a listener on the component hears it. Use it when the X means more than the text, such as a search that also drops the record it found. A deferred wire:model sends nothing until the next request, and this event is the moment to act instead:

{{-- 1. The X names what it clears, as its accessible name and its tooltip. --}}
{{-- 2. Clearing the search also drops the receipt it found. --}}
<x-wirekit::input label="Receipt" name="receipt" wire:model="receiptQuery" clearable
    clear-label="Clear the receipt search"
    x-on:wirekit:input-cleared="$wire.clearReceipt()" />

Your own x-ref on the field

Attributes you put on the component land on the <input>, and an x-ref among them stays yours with the buttons on. The buttons run in a small component of their own beside the field, not around it, so $refs in your component reaches the field, for instance to put the caret back after an action:

{{-- 1. The ref registers on your component, clearable or not. --}}
{{-- 2. The button puts the caret back into the field through it. --}}
<div x-data>
    <x-wirekit::input label="Scan" name="scan" wire:model.live="scan" clearable x-ref="scan" />
    <x-wirekit::button x-on:click="$refs.scan.focus()">Scan again</x-wirekit::button>
</div>

Input Types

Use the type prop or native HTML attributes to restrict input. WireKit styles the :user-invalid pseudo-class automatically — the moment the user types something that violates min, max, pattern, type="email", required, minlength or maxlength and then leaves the field, the border and focus ring turn red without any JavaScript or server round-trip. Try it: type a letter into the phone field below, then press Tab.

Number-only input

Type a value outside 1–99 and tab out — the field turns red.

Phone number with pattern

Type a letter and tab out — the field turns red. Allowed: digits, spaces, + and -.

Why doesn't the browser prevent you from typing invalid characters? That's intentional — filtering keystrokes breaks paste, autofill, IME input, and in-place corrections (you couldn't type 100 and then backspace to 99). WireKit follows the pattern the platform itself uses: accept any input, validate after blur or on submit, and show a clear visual error state.

With type="number" or type="range", a wire:model bound to a property declared int or float reads the field as a number: wire:model.live goes out as wire:model.number.live. The field gives its value as text, and Livewire would otherwise take the typed number the server sends back for a change made there and write it over what the reader typed while the request was out. A property without a type, or a string one, gets the text. A range field always holds a number: bound to a property that holds null, it stands in the middle of its scale.

Date and time fields

type="date", type="time" and type="datetime-local" render the browser's own field. The order of the parts, the calendar button and the picker come from the browser and follow the reader's language. WireKit evens out what the browser leaves to the page:

  • The text sits in the middle of the field in every engine and at every size.
  • The separators between the parts take the placeholder color. Safari fills an empty date field with today's date, in a gray the page cannot restyle. With the separators muted too, an empty field reads as a hint rather than as a date somebody set.
  • The calendar or clock button keeps the end of the field. A field wider than its value, in a grid column or a full-width form, shows the value at the start and the button at the far edge in Chrome and Edge, as it does at its own width.

Bind a date or time field to a string property in the field's own form: Y-m-d for date, H:i for time and Y-m-d\TH:i for datetime-local. Livewire sends a date object, such as a Carbon property or a model attribute with a date cast, to the browser as a full timestamp, which the field drops, so it would stay empty. In debug mode the component logs a warning for a binding to a date object.

In a row that sizes its fields to their content, a date or time field takes the width its browser measures, and browsers measure differently: a calendar button takes room where the browser draws one, and the width of a date changes with the reader's language and, in Chrome, with the pixel density of the screen. WireKit leaves that width to the browser rather than declaring one, because any single width would cut some languages short or pad every other one. Where a row has to line up the same in every browser, give the field a width of its own.

Date and time fields in a row

When the browser writes dates in another order

A date field takes its order from the browser's language, not from the page, so 03.04.2026 and 04/03/2026 are both accepted and one of them is the wrong day. format-hint on a date or datetime-local field says so below the field, in your application's language and with a worked example. It renders nothing while the browser and the page write dates the same way. Why the field cannot simply follow the page is explained on the date picker page: the order is the browser's.

A date field that names the browser's order

To turn it on for every date field in your application, set the defaults in config/wirekit.php. A field that passes format-hint itself keeps its own value.

'components' => [
    'input' => ['size' => 'md', 'format-hint' => true],
    'date-picker' => ['size' => 'md', 'format-hint' => true],
],

Character Counter

counter counts the characters in the field as the reader types, in the browser, with no request to the server. With a maxlength the count reads "12 / 60" and turns to the danger color past the limit; without one it is the number on its own.

Character counter with a limit

A string instead of true is the sentence the counter shows. Its placeholders are :count, :max, :remaining, and it is also what a screen reader hears, so write it as a sentence:

Character counter with your own sentence

The visible count is hidden from screen readers, which would otherwise read it out on every key. Once typing pauses, a polite live region says it in words: what is left of the limit, how far past it the text is, or the count itself.

It counts what maxlength counts in Chromium and Firefox, UTF-16 code units, so an emoji can count as two. With wire:model the field still sends nothing while the reader types, and a value the server sets shows its count once the server has answered. The counter runs under Alpine's CSP build. Textarea takes the same prop.

Tokens the Reader Can Insert

tokens puts a row of buttons under the field, one for each token, such as the variables of a template. A button puts its token into the field in the browser, with no request to the server: at the caret the reader left in the field, over the text they selected there, or at the end before they placed a caret. The focus stays on the button, and the caret moves past what was inserted, so a second token follows the first.

Tokens for a code prefix

Pass a list of tokens, or a map of each token to what it stands for: the button shows the token, and a screen reader hears what it stands for after it. The field hears an input and a change event for each token, so wire:model, wire:model.live, wire:model.change and x-model take the new value at once. Bound with .blur, the field takes it when its form is submitted or when the reader next leaves the field. A token that would take the value past maxlength is not inserted, and a disabled or read-only field takes none. The buttons run under Alpine's CSP build.

Width

All form components default to full width (w-full) — they fill whatever container they're in. Control the width by constraining the parent element:

Fixed max width
Two columns in a grid
Mixed column widths (2/3 + 1/3)

This applies to all WireKit form components: Input, Textarea, Select, Combobox, Date Picker, Time Picker, Number Input, Password Input, and File Upload.

Compact Size

size="md-compact" is a middle tier (2.25rem) between sm and md — sized for dense list/filter toolbars where sm reads cramped and md runs slightly too tall. Available on input, select, and button.

md-compact in a dense toolbar

Icons in the frame and the mono variant

The leading / trailing slots put an icon or addon inside the field frame — a search glyph, a unit symbol — with the input padding adjusting to make room. They are distinct from the text-only prefix / suffix props. Set mono to render the value in the monospace font, for SKUs, measurements, and codes:

Leading icon and a monospace SKU field

Optimistic UI

Pass the name of the Livewire method the field should call and the value is sent when you leave the field, shown as saving while it goes:

<x-wirekit::input
    name="nickname"
    label="Nickname"
    :value="$nickname"
    optimistic="saveNickname"
/>

Load wirekit-optimistic.js alongside whichever bundle you already use — below it, in your layout:

@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></script>

Try it

The demo below runs the real path: the change shows immediately, the outline says it is provisional, and the server's answer either confirms it silently or takes it back.

Optimistic field — accepted, refused, and a slow answer

The <livewire:demos.…> wrapper above exists only on this site — it supplies the demo methods so the page can show a real round trip. The block under it is what you write.

A refusal does not take your value back. For a toggle or a select, putting the old value back costs you nothing — it is simply the other choice. Here the old value belongs to the server and the new one is what you just typed, so restoring it would delete your work because a save failed. The value stays, and you are told two things: that it did not save, and that it is still there.

The field is not marked invalid. aria-invalid means this value is wrong, and a save that failed on the network says nothing about the value.

What a screen reader hears

Leaving the field announces once, hedged — "Saving". Confirmation is silent: what was announced is what happened. A refusal announces once more, and says the entry is still there.

Where the field already shows a validation message, the layer stays silent and leaves that message to speak: it tells you what to fix, "could not save" does not.

An aborted request announces nothing at all. Focus stays exactly where you put it.

Fields in a Row

A field is a vertical stack: label, control, message. When validation fires, the stack grows by the height of the message — and in a horizontal row every sibling re-anchors to the new bottom edge, so the toolbar jumps and the field the reader was reaching for is somewhere else.

Row alignment cannot fix this. items-end follows the growth by definition, and items-start lines the row up with the labels rather than the controls, so the controls sit at different heights the moment one label wraps. There is no value of align-items that anchors siblings to an element two levels down.

The direct answer is a row that lines its fields up. In row align-fields an input, a select and a textarea hand their label, control and message to the row's three tracks, so every control and a button beside them stand on one line, whatever is above or below them:

A row that lines its fields up

Unknown reference

Where the row cannot be one, such as a toolbar laid out by its own flex rules, reserve-message keeps the line's height whether or not there is anything to say:

A toolbar that holds still

Unknown reference

The same row without the reservation — the first two fields lift as soon as the third has something to say.

Unknown reference

The top edge has the same problem

reserve-message holds the bottom. The top is the mirror image, and the same align-items argument applies to it: a button or a plain block beside a labeled field starts at the container's top while the field's control starts one label-height lower, so two things meant for one line are not on one line.

<x-wirekit::field.spacer /> gives the non-field column the same starting box. Wrap that column in a <x-wirekit::field> of its own so it also keeps the distance a field puts between a label and its control — the top edges line up on the spacer, and everything below them lines up on that:

A button that lines up with the field beside it

Without the spacer the button sits a label-height above the field it belongs to.

It renders a real <x-wirekit::label> holding a no-break space, not a <span> copying the label's classes. That distinction is the whole point: whatever a label is — font, line-height, margin, and any token behind them — the spacer is exactly as tall, because it is one. A copy drifts the first time a token changes, and nothing renders wrong when it does: the two elements simply stop being the same height.

It is aria-hidden, so a screen reader is not told about a layout decision as if it were content.

It is off by default because in a stacked form the reserved line is wasted space — an empty row under every field. Turn it on where fields sit side by side.

Available on input, select and textarea.

Props

Prop Type Default Description
label string|null null Label text above the input
hideLabel bool false Render the label visually hidden (sr-only) but keep it for assistive tech — for a compact field in a toolbar / header
hint string|null null Help text below the input
formatHint bool false For type="date" and type="datetime-local": say so below the field when the browser writes dates in a different order than the page, with a worked example. Renders nothing while they agree, and other types ignore it. The default comes from components.input.format-hint. See When the browser writes dates in another order
help string|null null An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label
reserveMessage bool false Keep the message line's height even when there is no message, so a field in a horizontal row does not move its neighbors when validation fires. See Fields in a row
error string|null null 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.
success string|bool|null null Valid-state confirmation — string shows a green message, true shows just the green border. error wins when both are set.
size string 'md' sm, md-compact, md, lg. md-compact is the 2.25rem tier between sm and md, for dense toolbars — the worked example is above
type string 'text' HTML input type
optimistic string|null null Livewire method to call when you leave the field, showing the new value as saving. A refusal keeps your value. See Optimistic UI.
optimisticArgs array [] Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to.
mono bool false Render the field value in the monospace font (--font-wk-mono) — for SKUs, measurements, codes, hashes
prefix string|null null Fixed text before the value (a $, a unit). For an icon inside the frame use the leading slot
suffix string|null null Fixed text after the value (.00, a unit). For an icon inside the frame use the trailing slot
clearable bool false Shows a trailing X button (visible only while the field has content) that clears the value, refocuses, and fires input/change for wire:model sync, then wirekit:input-cleared.
clearLabel string|null null The X's accessible name and tooltip. Defaults to "Clear" and the field's name, such as "Clear Email", or to the translated "Clear input" on a field without a name.
copyable bool false Shows a trailing copy-to-clipboard button with a brief "Copied" confirmation. It is named "Copy" and the field's name, such as "Copy Email", or "Copy to clipboard" on a field without a name.
counter bool|string false Counts the characters under the field as they are typed, in the browser with no request: true shows the number ("12 / 60" with a maxlength), a string is the sentence that shows it, with the placeholders :count, :max, :remaining. See Character Counter.
tokens array|null null Tokens a button under the field puts into it, in the browser with no request: at the caret the reader left, over their selection, or at the end. A list, or a map of each token to what it stands for. See Tokens the Reader Can Insert.
required bool false Marks the input as required. Emits the required HTML attribute AND propagates to the associated label so its required indicator (*) renders.
disabled bool false Disables the input. Emits the disabled HTML attribute; styling already accounts for :disabled.
readonly bool false Makes the input read-only. Emits the readonly HTML attribute and paints the muted surface, so the field does not look like one that takes typing. Unlike disabled, the value stays legible, selectable and focusable, and it is submitted with the form. With clearable, the clear button is out of use as well; copyable keeps its button, since copying changes nothing.
autocomplete string|null null Sets the autocomplete hint (e.g. 'email', 'name', 'new-password', 'one-time-code', 'off').
placeholder string|null null Placeholder text inside the empty input.
scope string|null null Scoped personalization name

The HTML5 form-state props also flow through the attribute bag — passing them as plain HTML attributes (<x-wirekit::input required>) is equivalent to passing them as bound props (<x-wirekit::input :required="true">). Mixing both forms is safe and produces a single attribute on the underlying <input>.

Slots

Slot Description
leading An icon or addon inside the field frame, before the value (a search glyph, a unit). Distinct from the text-only prefix prop. The slot owns its own accessibility — a decorative <x-wirekit::icon> is already aria-hidden, and the field's label is its accessible name.
trailing The same, after the value — before any clearable / copyable buttons.
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 input component automatically handles label pairing, error announcement, and validation feedback — no manual ARIA wiring needed.

  • Label pairing — When the label prop is set, a <x-wirekit::label> is rendered with for pointing to the input's id. The id is auto-generated from the name attribute if not provided explicitly.

  • Unique ids across duplicate names — When no id is given, the input derives one from name. If two controls share a name on the same page (a create + an edit form, a filter bar + a modal, a repeater row), the first keeps the clean derived id and each later one gets a -2 / -3 suffix, so label[for] and aria-describedby always resolve to the right control while the form key name stays repeated as intended. This is on by default; set wirekit.a11y.dedupe_ids to false (env WIREKIT_DEDUPE_IDS=false) to restore the verbatim behavior. Livewire caveat: the registry resets per request, so if you repeat the same name across independently updating islands, pass an explicit id — a partial island re-render can otherwise recompute a suffixed id back to the base.

  • Error states — When an error is present (via prop or Laravel's $errors bag), the input sets aria-invalid="true" and aria-describedby="{id}-error", linking the error message to the input for screen readers. The error message is also an ARIA live region (aria-live="polite") by default, so an error that appears dynamically (e.g. after a Livewire round-trip) is announced without the focus returning to the field — set :announce-error="false" to opt out when the page runs its own live region.

  • Hint text — When hint is set and neither an error nor a success message is active, the hint paragraph is linked via aria-describedby="{id}-hint" so assistive technology announces it when the input receives focus. A success message takes the link instead ({id}-success), and an error takes it over both — only one description is linked at a time, because a second aria-describedby on the same element replaces the first rather than adding to it.

  • Help — help renders a button beside the label, outside the <label> so it does not join the field's name. It is named "Help for" and the label, shows the text in a tooltip on hover and on focus, and Escape closes the tooltip. A hidden copy of the text is linked through aria-describedby, after the field's own message, so the field is described by it too.

  • :user-invalid styling — Native HTML5 constraint violations (required, pattern, min, max, minlength, maxlength, type) trigger a red border and focus ring automatically after the user interacts with the field. This uses the CSS :user-invalid pseudo-class (not :invalid), so empty required fields don't show errors on page load.

  • Disabled state — Uses the native disabled attribute. Visually muted with opacity-[var(--opacity-wk-disabled)] and cursor-not-allowed.

  • Focus ring — Uses focus-visible so keyboard users see the ring but mouse users don't.

  • Tokens — With tokens the buttons sit in a group named "Insert into" and the label. Each button is named by its token, which it shows, and by what the token stands for, which only a screen reader hears. The focus stays on the button after a token goes in, so a keyboard reader can add the next one.

  • autocomplete — Pass the native autocomplete attribute for login forms, addresses, and payment fields to support browser autofill:

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

Keyboard Interaction

Key Action
Tab Move focus to the input
Any character Insert the character at caret position
Backspace / Delete Remove characters around caret
Home / End Move caret to start / end of value
Shift+ArrowLeft / Shift+ArrowRight Extend text selection

Pitfalls

  • Don't forget name="..." for non-Livewire usage. Without wire:model AND without name, the value never reaches the server on form submit.
  • Don't use class="bg-...". The component reads its background from --color-wk-bg-input. Override the token in CSS, not in the class string.
  • Don't apply dark: prefix. The token system auto-switches via the parent .dark class — dark: here is redundant and bypasses the design system.

Design Tokens

Token Used for
--font-wk-sans Body font family
--font-wk-letter-spacing Letter spacing
--text-wk-sm / --text-wk-md / --text-wk-lg Font size per size prop
--color-wk-text Input text
--color-wk-text-muted / --color-wk-text-subtle Prefix / suffix accent text
--color-wk-text-placeholder Placeholder text
--color-wk-bg-input Input background
--color-wk-border-strong Default border
--color-wk-border-strong-hover Hover border
--color-wk-border-error Error-state border
--color-wk-danger / --color-wk-danger-text Error message
--color-wk-ring / --color-wk-ring-offset Focus ring color + offset
--ring-wk-width / --ring-wk-offset Focus ring geometry
--border-wk-width Border width
--radius-wk-sm / --radius-wk-md Border radius
--shadow-wk-sm Subtle shadow
--size-wk-sm / --size-wk-md-compact / --size-wk-md / --size-wk-lg Control height per size prop
--padding-wk-x-sm / --padding-wk-x-md / --padding-wk-x-lg Horizontal padding
--opacity-wk-disabled Disabled visual weight
--transition-wk-duration / --transition-wk-easing Hover / focus transition

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