---
title: Input
description: Text input with prefix/suffix support
visibility: guest
draft: false
---

# Input

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

## Basic Usage

:::preview{title="A labeled text input"}
<x-wirekit::input label="Name" name="name" wire:model="name" />
<x-wirekit::input label="Email" type="email" name="email" wire:model="email" />
:::

## With Error

:::preview{title="Error state"}
<x-wirekit::input label="Name" name="name" error="This field is required." />
:::

Errors from Laravel's validation bag are shown automatically when `name` matches a field.

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

:::preview{title="Success / valid state"}
<x-wirekit::input label="Username" name="username" value="ada-lovelace" success="Username is available" />
:::

## With Hint

:::preview{title="With a hint below the field"}
<x-wirekit::input label="Email" name="email" hint="We'll never share your email." />
:::

## With Button

Combine an input with a button for inline actions like newsletter sign-ups or search forms. The wrapper uses `display: flex; align-items: flex-start;` so the button sits flush with the top edge of the input control (not the label), and a top spacer on the button nudges it down to align with the input row when the input has a label above it. The label is `1rem` text on a `1.5` line-height (~24px) followed by a `space-y-1.5` (6px) gap, so the input control's top edge sits 30px below the wrapper top — that's the offset to match.

:::preview{title="Email subscribe with button"}
<x-wirekit::row align="start" gap="sm">
    <div style="flex: 1 1 0%; min-width: 0;">
        <x-wirekit::input label="Email" name="email" type="email"
            hint="We'll send you a confirmation email."
            placeholder="you@example.com" />
    </div>
    <x-wirekit::button style="margin-top: 1.875rem;">Subscribe</x-wirekit::button>
</x-wirekit::row>
:::

:::preview{title="Search with icon button"}
<x-wirekit::row align="start" gap="sm">
    <div style="flex: 1 1 0%; min-width: 0;">
        <x-wirekit::input aria-label="Search documentation" name="search" type="search"
            placeholder="Search documentation..." />
    </div>
    <x-wirekit::button intent="neutral" surface="outline">
        <x-slot:iconLeft>
            <x-wirekit::icon name="search" class="w-4 h-4" />
        </x-slot:iconLeft>
        Search
    </x-wirekit::button>
</x-wirekit::row>
:::

## Prefix & Suffix

:::preview{title="Prefix and suffix"}
<x-wirekit::input label="Price" name="price" type="number" prefix="€" suffix=".00" placeholder="0" />
<x-wirekit::input label="Website" name="url" prefix="https://" placeholder="example.com" />
:::

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

:::preview{title="Clearable, copyable, and both"}
<x-wirekit::input label="Search" name="q-demo" clearable value="WireKit components" placeholder="Search…" />
<x-wirekit::input label="API token" name="token-demo" copyable readonly value="wk_live_8a2f3c9d1e" />
<x-wirekit::input label="Coupon code" name="coupon-demo" clearable copyable value="SAVE20" />
:::

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

:::preview{title="Number-only input"}
<x-wirekit::input label="Quantity" name="quantity" type="number" min="1" max="99" placeholder="1" hint="Type a value outside 1–99 and tab out — the field turns red." />
:::

:::preview{title="Phone number with pattern"}
<x-wirekit::input label="Phone" name="phone" type="tel" inputmode="tel" pattern="[0-9\+\-\s]+" placeholder="+49 123 456 789" hint="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 standard web pattern used by GitHub, Stripe, and Linear:
accept any input, validate after blur or on submit, and show a clear visual
error state.

## Character Counter

Use [Alpine.js](https://alpinejs.dev/) to add a live character counter with min/max validation to any input:

:::preview{title="Live character counter with min/max"}
<div x-data="{ text: '', max: 30, min: 3 }">
    <x-wirekit::input
        label="Username"
        name="username"
        x-model="text"
        maxlength="30"
        placeholder="Choose a username..."
    />
    <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.375rem; font-size: var(--text-wk-sm); gap: 0.5rem;">
        <div style="min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
            <span x-cloak x-show="text.length > 0 && text.length < min" style="color: var(--color-wk-danger-text);">
                Minimum 3 characters required.
            </span>
            <span x-cloak x-show="text.length >= max" style="color: var(--color-wk-danger-text);">
                Character limit reached.
            </span>
        </div>
        <span style="color: var(--color-wk-text-muted); flex-shrink: 0; white-space: nowrap;" x-text="`${text.length}/${max}`">0/30</span>
    </div>
</div>
:::

The counter is not built into the input component — it's a lightweight Alpine.js wrapper you add in your template. See the [Textarea](/components/textarea#character-counter) docs for a multi-line version of the same pattern.

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

:::preview{title="Fixed max width"}
<div style="max-width: 24rem;">
    <x-wirekit::input label="Username" name="username-width-demo" placeholder="Parent is max-w-sm (24rem)" />
</div>
:::

:::preview{title="Two columns in a grid"}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
    <x-wirekit::input label="First Name" name="first_name" />
    <x-wirekit::input label="Last Name" name="last_name" />
</div>
:::

:::source{language="blade"}
<x-wirekit::grid cols="1 sm:2" gap="md">
    <x-wirekit::input label="First Name" name="first_name" />
    <x-wirekit::input label="Last Name" name="last_name" />
</x-wirekit::grid>
:::

:::preview{title="Mixed column widths (2/3 + 1/3)"}
<x-wirekit::row wrap gap="md">
    <div style="flex: 2 1 12rem; min-width: 0;"><x-wirekit::input label="Street" name="street" /></div>
    <div style="flex: 1 1 8rem; min-width: 0;"><x-wirekit::input label="Zip" name="zip" /></div>
</x-wirekit::row>
:::

This applies to all WireKit form components: [Input](/components/input), [Textarea](/components/textarea), [Select](/components/select), [Combobox](/components/combobox), [Date Picker](/components/date-picker), [Time Picker](/components/time-picker), [Number Input](/components/number-input), [Password Input](/components/password-input), and [File Upload](/components/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`.

:::preview{title="md-compact in a dense toolbar"}
{{-- align="end", not the default "center": one of the three carries a visible
     label and the other two do not, so centering puts the labeled field a label's
     height lower than its neighbors. Aligning the ends lines up what a reader
     actually looks at — the controls themselves — whichever of them has a label
     above it. --}}
<x-wirekit::row gap="sm" align="end" wrap>
    <x-wirekit::input aria-label="Search" size="md-compact" placeholder="Search…" />
    <x-wirekit::select size="md-compact" label="Status" :options="['All', 'Open', 'Closed']" />
    <x-wirekit::button size="md-compact">Filter</x-wirekit::button>
</x-wirekit::row>
:::

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

:::preview{title="Leading icon and a monospace SKU field"}
<x-wirekit::stack style="max-width: 24rem;">
    <x-wirekit::input label="Search" name="q" placeholder="Search products…">
        <x-slot:leading><x-wirekit::icon name="search" class="w-5 h-5" /></x-slot:leading>
    </x-wirekit::input>
    <x-wirekit::input label="SKU" name="sku" mono placeholder="WK-000-000">
        <x-slot:trailing><x-wirekit::icon name="tag" class="w-5 h-5" /></x-slot:trailing>
    </x-wirekit::input>
</x-wirekit::stack>
:::

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

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

```blade
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></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.

:::preview{title="Optimistic field — accepted, refused, and a slow answer"}
<livewire:demos.optimistic-host>
<x-wirekit::stack gap="lg" style="max-width: 26rem;">
    <x-wirekit::input name="opt-accept" label="Accepted — the confirmation is silent" value="Ada" optimistic="demoAccept" />
    <x-wirekit::input name="opt-reject" label="Refused — your text stays, and you are told it did not save" value="Ada" optimistic="demoReject" />
    <x-wirekit::input name="opt-slow" label="Slow answer — the dashed outline is the provisional state" value="Ada" optimistic="demoSlow" />
</x-wirekit::stack>
</livewire:demos.optimistic-host>
:::

:::source{language="blade"}
{{-- In your app there is no host: your own Livewire component owns the method. --}}
<x-wirekit::input
    name="nickname"
    label="Nickname"
    :value="$nickname"
    optimistic="saveNickname"
/>
:::

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.

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

`reserve-message` keeps the line's height whether or not there is anything to say:

:::preview{title="A toolbar that holds still"}
<x-wirekit::stack gap="md">
    <x-wirekit::row gap="md" align="end">
        <x-wirekit::input name="from-a" label="From" reserve-message />
        <x-wirekit::input name="to-a" label="To" reserve-message />
        <x-wirekit::input name="ref-a" label="Reference" reserve-message error="Unknown reference" />
    </x-wirekit::row>
    <x-wirekit::text size="sm" variant="muted">The same row without the reservation — the first two fields lift as soon as the third has something to say.</x-wirekit::text>
    <x-wirekit::row gap="md" align="end">
        <x-wirekit::input name="from-b" label="From" />
        <x-wirekit::input name="to-b" label="To" />
        <x-wirekit::input name="ref-b" label="Reference" error="Unknown reference" />
    </x-wirekit::row>
</x-wirekit::stack>
:::

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

:::preview{title="A button that lines up with the field beside it"}
<x-wirekit::stack gap="md">
    <x-wirekit::row gap="md" align="start">
        <x-wirekit::input name="search-a" label="Search" reserve-message />
        <x-wirekit::field>
            <x-wirekit::field.spacer />
            <x-wirekit::button intent="primary">Go</x-wirekit::button>
        </x-wirekit::field>
    </x-wirekit::row>
    <x-wirekit::text size="sm" variant="muted">Without the spacer the button sits a label-height above the field it belongs to.</x-wirekit::text>
    <x-wirekit::row gap="md" align="start">
        <x-wirekit::input name="search-b" label="Search" reserve-message />
        <x-wirekit::button intent="primary">Go</x-wirekit::button>
    </x-wirekit::row>
</x-wirekit::stack>
:::

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`](/components/input), [`select`](/components/select) and [`textarea`](/components/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 |
| `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](/components/input#fields-in-a-row) |
| `error` | string\|null | `null` | 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 |
| `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`, `lg` |
| `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](#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. |
| `copyable` | bool | `false` | Shows a trailing copy-to-clipboard button with a brief "Copied" confirmation. |
| `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. |
| `autocomplete` | string\|null | `null` | Sets the [`autocomplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/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. |

## 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 no error is active, the hint paragraph is linked via `aria-describedby="{id}-hint"` so assistive technology announces it when the input receives focus.
- **`: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.
- **`autocomplete`** — Pass the native `autocomplete` attribute for login forms, addresses, and payment fields to support browser autofill:

  ```blade
  <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` / `--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](/components/inline-edit) — edit this value in place, with an explicit confirm step

## Further Reading

- [MDN: `<input>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
- [MDN: Input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types)
- [MDN: `aria-invalid`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid)
- [WebAIM: Creating Accessible Forms](https://webaim.org/techniques/forms/)
