Textarea
A textarea component with label, error handling, hint support, and configurable rows.
Basic Usage
Custom Rows
Disable Resize
Auto-size
Set rows="auto" to grow the textarea with its content as the user types — no JavaScript needed (it uses the CSS field-sizing: content feature, supported across the WireKit browser baseline). The numeric rows still acts as the minimum height. Auto-size grows the height only — the width follows the container, as in any real layout (here a fixed-width form column).
With Error
Message 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 message — the mirror of the error state, but valid (no aria-invalid). error wins when both are set.
Saved
With Hint
Maximum 500 characters.
Character Counter
Combine the textarea with Alpine.js to show a live character count with min/max validation:
The counter is not part of the textarea component itself — it's a lightweight Alpine.js wrapper you add in your template. This gives you full control over the counter position, validation messages, and threshold logic.
Width
Like all WireKit form components, the textarea fills its container (w-full). Control the width via the parent element:
See Input — Width for more layout examples (grid columns, mixed widths).
Optimistic UI
Pass the name of the Livewire method the field should call and the text is sent when you leave the field, shown as saving while it goes:
<x-wirekit::textarea
name="bio"
label="Bio"
optimistic="saveBio"
>{{ $bio }}</x-wirekit::textarea>
Load wirekit-optimistic.js alongside whichever bundle you already use — below it, in your layout:
@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.
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 text back, and that is the important part. Everywhere else in WireKit an optimistic update that fails puts the old value back — for a toggle or a select that costs you nothing, because the old value is simply the other choice. Here the old value belongs to the server and the new one is what you just wrote, so putting it back would delete your work because a save failed. No editor behaves that way, and neither does this.
Instead the text stays exactly where it is, and you are told two things: that it did not save, and that your text is still there. The second one is the question you actually have.
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 — it may be perfectly good and simply unsaved.
What a screen reader hears Leaving the field announces once, hedged — "Saving" — so the state is audible as provisional. Confirmation is silent: what was announced is what happened.
A refusal announces once more, and the message carries the reassurance: could not save, your text is still here.
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 — nothing was refused.
Focus stays exactly where you put it. An undo arrives on the server's schedule, and moving focus then would take you out of your place for a reason you could not predict.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string|null | null |
Label text above the textarea |
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 textarea |
reserveMessage |
bool | false |
Keep the message line's height even when there is no message — see Fields in a row |
error |
string|null | null |
Error message (overrides $errors bag) |
success |
string|bool|null | null |
Valid-state confirmation — string shows a green message, true shows just the green border. error wins. |
size |
string | 'md' |
sm, md, lg |
rows |
int|string | 3 |
Number of visible rows, or 'auto' to grow with content (min height stays at the numeric fallback) |
optimistic |
string|null |
null |
Livewire method to call when you leave the field, showing the new text as saving. A refusal keeps your text. See Optimistic UI. |
optimisticArgs |
array |
[] |
Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
resize |
bool | true |
Allow vertical resizing |
scope |
string|null | null |
Scoped personalization name |
Accessibility
The textarea component follows the same accessibility patterns as Input.
- Label pairing — When the
labelprop is set, a<x-wirekit::label>is rendered withforpointing to the textarea'sid. Theidis auto-generated from thenameattribute. - Error states — When an error is present, the textarea sets
aria-invalid="true"andaria-describedby="{id}-error"to link the error message for screen readers. - Hint text — When
hintis set and no error is active, the hint is linked viaaria-describedby="{id}-hint". :user-invalidstyling — Native HTML5 constraints (required,minlength,maxlength) trigger a red border after user interaction, using:user-invalid(not:invalid).- Character counter — When adding a character counter (see example above), ensure the counter text is perceivable. The Alpine.js pattern uses
x-textwhich updates the DOM live — screen readers pick up changes naturally. For more explicit announcements, wrap the counter in anaria-live="polite"region. - Disabled state — Uses the native
disabledattribute with visual muting. - Focus ring — Uses
focus-visiblefor keyboard-only visibility.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus into the textarea (within the textarea, Tab is treated as focus-out, not as an indent character) |
Enter |
Insert a line break |
Home / End |
Move caret to start / end of the current line |
Ctrl+Home / Ctrl+End |
Move caret to top / bottom of the textarea |
Shift+Arrow |
Extend selection |
Pitfalls
- Don't use textarea for code input. Reach for
<x-wirekit::code-block>(read-only) or a real editor (Monaco / CodeMirror) — textarea has no syntax highlighting, no tab-indent, and no line-numbering. - Don't bind
wire:model.livefor prose. Each keystroke round-trips. Usewire:model.blur.
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 |
Textarea text |
--color-wk-text-muted |
Counter / hint text |
--color-wk-text-placeholder |
Placeholder text |
--color-wk-bg-input |
Textarea 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 |
--padding-wk-x-sm / --padding-wk-x-md / --padding-wk-x-lg |
Horizontal padding |
--padding-wk-y-sm / --padding-wk-y-md / --padding-wk-y-lg |
Vertical 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