Skip to content
Brok UI

Loading…

No results

Textarea

Open source Core

Multiline text input with invalid, disabled, dark, RTL, and responsive states. Inside a <x-ui.field name> (or with its own name) it self-wires old() repopulation and aria-invalid/aria-describedby from the shared $errors bag.

Version
v1.5.2
Stability
stable
License
MIT
Related
Input
Mention
Field

Preview

Invalid
Disabled
previews.components.textarea.default.blade.php Blade
<x-ui.textarea placeholder="{{ __('Type your message here.') }}" aria-label="{{ __('Message') }}" class="max-w-sm" />
False Current
True Current

Installation

terminal
php artisan ui:add textarea

Registry contract

php artisan ui:add textarea writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/textarea.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

textarea.md
# Brok UI: Textarea (`textarea`)

Multiline text input with invalid, disabled, dark, RTL, and responsive states. Inside a <x-ui.field name> (or with its own name) it self-wires old() repopulation and aria-invalid/aria-describedby from the shared $errors bag.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add textarea
```

## Usage

```blade
<x-ui.textarea placeholder="{{ __('Type your message here.') }}" aria-label="{{ __('Message') }}" class="max-w-sm" />
```

## Props

- `rows` (int, default `4`) — Visible row count before scrolling.
- `invalid` (bool, default `false`) — Forces the invalid visual state independent of the shared error bag.
- `disabled` (bool, default `false`) — Disables the field and excludes it from submission.
- `autogrow` (bool, default `false`) — Grow with the content instead of scrolling; rows is the starting height. Uses CSS field-sizing: content, with an Alpine fallback that sets the height on input in browsers without it.
- `maxRows` (int|null, default `null`) — With autogrow: the row count at which the field stops growing and scrolls. Null grows without a limit.

## Use when

- Use when users must enter freeform information that cannot be reliably selected from a list.
- Collecting multiple lines of free text, such as a comment, message or description.
- Needing automatic old() repopulation and aria-invalid/aria-describedby wiring off Laravel's shared $errors bag.

## Avoid when

- Do not choose a freeform field when a constrained choice would reduce errors or cognitive load.
- A single line of text; use input instead.
- The text should highlight @-mentions as the user types; use mention instead.

## Anti-patterns

- Using freeform entry for a bounded answer set

## Rules

- Use the `<brok:textarea>` tag (or `<x-ui.textarea>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/textarea
- Registry JSON (files, props, contract): https://brokui.dev/r/open/textarea.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

autogrow.blade.php Blade
{{-- Autogrow: a one-row task title that wraps and grows with its text, up to
     four rows, then scrolls. --}}
<x-ui.textarea
    autogrow
    rows="1"
    max-rows="4"
    aria-label="{{ __('Task title') }}"
    placeholder="{{ __('Task title') }}"
    class="max-w-sm"
>{{ __('Prepare the quarterly budget review with finance and send the draft to the board before Friday') }}</x-ui.textarea>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
rows int 4 Visible row count before scrolling.
invalid bool false Forces the invalid visual state independent of the shared error bag.
disabled bool false Disables the field and excludes it from submission.
autogrow bool false Grow with the content instead of scrolling; rows is the starting height. Uses CSS field-sizing: content, with an Alpine fallback that sets the height on input in browsers without it.
maxRows int | null null With autogrow: the row count at which the field stops growing and scrolls. Null grows without a limit.

Slots

  • default — Initial content, used when there is no flashed old() value.

Data slots

Stable hooks for CSS overrides and browser tests.

textarea

Behavior

  • Inside a wrapping field (or with its own name attribute) self-wires old() repopulation, aria-invalid and aria-describedby off the shared $errors bag.
  • A flashed old() value wins over the slotted content after a failed redirect-back.
  • With autogrow the height follows the content from rows up to max-rows lines (data-autogrow, field-sizing: content, no resize handle); a long one-row title wraps instead of being cut.
  • Forwards native textarea attributes and preserves placeholder, disabled, required, rows, and aria-* semantics.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Freeform text input

Collect unpredictable freeform information.

Use when

  • Use when users must enter freeform information that cannot be reliably selected from a list.
  • Collecting multiple lines of free text, such as a comment, message or description.
  • Needing automatic old() repopulation and aria-invalid/aria-describedby wiring off Laravel's shared $errors bag.

Avoid when

  • Do not choose a freeform field when a constrained choice would reduce errors or cognitive load.
  • A single line of text; use input instead.
  • The text should highlight @-mentions as the user types; use mention instead.

Use instead

  • Radio or checkbox for bounded choices
  • Select or combobox for known options

Anti-patterns

  • Using freeform entry for a bounded answer set
Anatomy
root
Theming hooks
textarea

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • aria-required, aria-invalid and aria-describedby are set automatically from validation state and any inherited hint or error.
  • Pair with label (or a wrapping field) so the control has an accessible name.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="textarea-{{ $record->id }}">
    <x-ui.textarea placeholder="{{ __('Type your message here.') }}" aria-label="{{ __('Message') }}" class="max-w-sm" />
</div>

Validation

Validation support: laravel-error-bag. Keep the error message connected with aria-describedby.

livewire-form.blade.php Blade
<form wire:submit="save" class="space-y-2">
    <brok:textarea
        wire:model="value"
        :aria-invalid="$errors->has('value') ? 'true' : 'false'"
        aria-describedby="value-error"
    />

    @error('value')
        <p id="value-error" role="alert">{{ $message }}</p>
    @enderror

    <brok:button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove>Save</span>
        <span wire:loading>Saving…</span>
    </brok:button>
</form>

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/textarea.blade.php Blade
{{-- Inherited from a wrapping field with a name (see input for the rationale). --}}
@aware([
    'name' => null,
    'htmlName' => null,
    'hint' => null,
    'required' => false,
])

@props([
    'rows' => 4,
    'invalid' => false,
    'disabled' => false,
    // Grow with the content instead of scrolling: `rows` becomes the
    // starting height. Uses CSS field-sizing: content, with a small script
    // fallback for browsers without it.
    'autogrow' => false,
    // With autogrow: the row count at which it stops growing and scrolls.
    'maxRows' => null,
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $textarea = $styles['textarea'];

    // @aware leaves $name undefined when there is no wrapping field, so normalise it.
    $awareName = $name ?? null;
    $awareHtmlName = $htmlName ?? null;

    // Effective field name: the @aware value from a wrapping field, else an explicit
    // `name` attribute on the textarea itself.
    $fieldName = $awareName ?? $attributes->get('name');
    $resolvedHtmlName = $awareName
        ? ($awareHtmlName ?? collect(explode('.', $awareName))->reduce(
            fn ($carry, $segment, $index) => $index === 0 ? $segment : $carry.'['.$segment.']',
            ''
        ))
        : $attributes->get('name');

    $bag = ($errors ?? null) instanceof \Illuminate\Support\ViewErrorBag ? $errors : null;
    $hasBagError = $bag && filled($fieldName) && $bag->has($fieldName);
    $isInvalid = $invalid || $hasBagError;

    $state = $isInvalid ? $textarea['states']['invalid'] : $textarea['states']['default'];
    $classes = trim($textarea['base'].' '.$state);

    // Repopulate after redirect-back: a flashed old($name) wins; otherwise the
    // slotted content is used. The slot is already escaped Blade output, so it is
    // printed raw; a flashed value is raw user input and is escaped here.
    $flashedValue = filled($fieldName) && old($fieldName) !== null ? e((string) old($fieldName)) : null;

    $describedBy = collect(preg_split('/\s+/', trim((string) $attributes->get('aria-describedby', ''))) ?: [])
        ->when(filled($awareName) && filled($hint ?? null), fn ($ids) => $ids->push($fieldName.'-description'))
        ->when(filled($awareName) && $hasBagError, fn ($ids) => $ids->push($fieldName.'-error'))
        ->filter()
        ->unique()
        ->implode(' ');

    $isRequired = (bool) ($required ?? false) || $attributes->has('required');

    $resolvedId = $attributes->get('id', filled($fieldName) ? $fieldName : null);

    // Autogrow: the height follows the content between `rows` and `maxRows`
    // lines (1lh each, plus the py-2 padding and the 1px borders).
    $rows = max(1, (int) $rows);
    $maxRows = filled($maxRows) ? max($rows, (int) $maxRows) : null;
    $autogrowStyle = $autogrow
        ? 'min-height: max(var(--control-height-md), calc('.$rows.' * 1lh + 1rem + 2px));'
            .($maxRows !== null ? ' max-height: calc('.$maxRows.' * 1lh + 1rem + 2px);' : '')
        : null;
    if ($autogrow) {
        $classes .= ' field-sizing-content resize-none overflow-y-auto';
    }
@endphp

<textarea
    rows="{{ $rows }}"
    @if (filled($resolvedHtmlName)) name="{{ $resolvedHtmlName }}" @endif
    @if (filled($resolvedId)) id="{{ $resolvedId }}" @endif
    data-slot="textarea"
    @disabled($disabled)
    @required($isRequired)
    @if ($isRequired) aria-required="true" @endif
    @if ($isInvalid) aria-invalid="true" @endif
    @if (filled($describedBy)) aria-describedby="{{ $describedBy }}" @endif
    @if ($autogrow)
        data-autogrow
        style="{{ $autogrowStyle }}"
        x-data
        x-init="if (!CSS.supports('field-sizing', 'content')) { const fit = () => { $el.style.height = 'auto'; $el.style.height = ($el.scrollHeight + $el.offsetHeight - $el.clientHeight) + 'px' }; fit(); $el.addEventListener('input', fit) }"
    @endif
    {{ $attributes->except(['name', 'id', 'required', 'aria-required', 'aria-describedby'])->merge(['class' => $classes]) }}
>{!! $flashedValue ?? trim((string) $slot) !!}</textarea>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.5.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0