Skip to content
Brok UI

Loading…

No results

Input

Open source Core

Text input with sm/md/lg sizes, invalid, disabled, readonly, file, dark, RTL, and responsive states; composes with label/field/button for forms. 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.4.7
Stability
stable
License
MIT
Related
Textarea
Native Select
Masked Input

Preview

Size
Invalid
Disabled
previews.components.input.default.blade.php Blade
<x-ui.input type="email" value="jane@example.com" placeholder="you@example.com" aria-label="{{ __('Email') }}" class="w-full max-w-sm" />

Size options

Sm Current
Md Current
Lg Current
Auto Current

Invalid options

False Current
True Current

Installation

terminal
php artisan ui:add input

Registry contract

php artisan ui:add input 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/input.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.

input.md
# Brok UI: Input (`input`)

Text input with sm/md/lg sizes, invalid, disabled, readonly, file, dark, RTL, and responsive states; composes with label/field/button for forms. 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 input
```

## Usage

```blade
<x-ui.input type="email" value="[email protected]" placeholder="[email protected]" aria-label="{{ __('Email') }}" class="w-full max-w-sm" />
```

## Props

- `type` (string, default `text`) — Native input type (text, email, number, date, and so on).
- `size` (md|sm|lg, default `md`) — Control height and horizontal padding; default is a backward-compatible alias for md.
- `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.
- `value` (mixed|null, default `null`) — Initial value; overridden by a flashed old() value when the field has a resolvable name.

## Use when

- Use when users must enter freeform information that cannot be reliably selected from a list.
- Collecting a single line of free text, a number, an email, a date, or any other native input type value in a form.
- 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.
- Multi-line text; use textarea instead.
- A fixed set of options to choose from; use select, select-advanced or combobox instead.

## Anti-patterns

- Using freeform entry for a bounded answer set

## Rules

- Use the `<brok:input>` tag (or `<x-ui.input>`) 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/input
- Registry JSON (files, props, contract): https://brokui.dev/r/open/input.json

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

Examples

file.blade.php Blade
<x-ui.field class="w-full max-w-sm text-start">
    <x-ui.label for="preview-input-file">{{ __('Resume') }}</x-ui.label>
    <x-ui.input id="preview-input-file" type="file" class="pt-2" />
    <x-ui.field.description>{{ __('PDF or DOCX, up to 5 MB.') }}</x-ui.field.description>
</x-ui.field>
sizes.blade.php Blade
<div class="flex w-full max-w-sm flex-col gap-3">
    <x-ui.input size="sm" placeholder="{{ __('Small') }}" aria-label="{{ __('Small input') }}" />
    <x-ui.input size="md" placeholder="{{ __('Medium') }}" aria-label="{{ __('Medium input') }}" />
    <x-ui.input size="lg" placeholder="{{ __('Large') }}" aria-label="{{ __('Large input') }}" />
</div>
states.blade.php Blade
<div class="flex w-full max-w-sm flex-col gap-3">
    <x-ui.input :invalid="true" type="email" value="not-an-email" aria-label="{{ __('Invalid email') }}" />
    <x-ui.input :disabled="true" value="{{ __('Disabled') }}" aria-label="{{ __('Disabled input') }}" />
    <x-ui.input readonly value="{{ __('Read only') }}" aria-label="{{ __('Read only input') }}" />
</div>
with-button.blade.php Blade
<form class="flex w-full max-w-sm items-center gap-2" role="search" onsubmit="return false">
    <x-ui.input type="search" name="q" placeholder="{{ __('Search...') }}" aria-label="{{ __('Search') }}" />
    <x-ui.button type="submit">{{ __('Search') }}</x-ui.button>
</form>
with-label.blade.php Blade
<x-ui.field class="w-full max-w-sm text-start">
    <x-ui.label for="preview-input-email" :required="true">{{ __('Email') }}</x-ui.label>
    <x-ui.input id="preview-input-email" type="email" autocomplete="email" placeholder="jane@example.com" />
    <x-ui.field.description>{{ __('We will only use this for account updates.') }}</x-ui.field.description>
</x-ui.field>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
type string text Native input type (text, email, number, date, and so on).
size md | sm | lg md Control height and horizontal padding; default is a backward-compatible alias for md.
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.
value mixed | null null Initial value; overridden by a flashed old() value when the field has a resolvable name.

Slots

  • default — Unused; the input renders from props only.

Data slots

Stable hooks for CSS overrides and browser tests.

input

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.
  • With no field ancestor and no name attribute, every inherited behaviour resolves to null and the input behaves as a plain native input.
  • Forwards normal input attributes such as name, type, value, placeholder, disabled, and aria-* to the native input.
  • 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 a single line of free text, a number, an email, a date, or any other native input type value in a form.
  • 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.
  • Multi-line text; use textarea instead.
  • A fixed set of options to choose from; use select, select-advanced or combobox 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
input

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, so consumers rarely need to set them by hand.
  • 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

Safe

Livewire can update this component through forwarded wire:* attributes.

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:input
        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/input.blade.php Blade
{{-- A wrapping field with a name shares it down via @aware, so the input can
     self-wire old()/aria-invalid/aria-describedby without the consumer repeating
     it. With no field ancestor (or its own name attribute) this resolves to null
     and nothing below changes — fully backward-compatible. --}}
@aware([
    'name' => null,
    'htmlName' => null,
    'hint' => null,
    'required' => false,
])

@props([
    'type' => 'text',
    'size' => 'md',
    'invalid' => false,
    'disabled' => false,
    'value' => null,
])

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

    // @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 the consumer set on the input 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');

    // The shared error bag drives invalid state + the error message id, so a field
    // wrapper and a standalone named input behave identically after validation.
    $bag = ($errors ?? null) instanceof \Illuminate\Support\ViewErrorBag ? $errors : null;
    $hasBagError = $bag && filled($fieldName) && $bag->has($fieldName);
    $isInvalid = $invalid || $hasBagError;

    $state = $isInvalid ? $input['states']['invalid'] : $input['states']['default'];

    // `default` is a backward-compatible alias for `md` (matches checkbox/switch);
    // any unknown size also falls back to `md`. Resolved at the Blade level so no
    // `default` key leaks into the `_styles.php` size map (and PreviewControls).
    $size = $size === 'default' ? 'md' : (isset($input['sizes'][$size]) ? $size : 'md');
    $classes = trim($input['base'].' '.$state.' '.$input['sizes'][$size]);

    // old($name, $value) repopulates after a failed redirect-back; falls back to the
    // passed value (or null) when there is no flashed input.
    $resolvedValue = filled($fieldName) ? old($fieldName, $value) : $value;

    // Preserve consumer-provided descriptions and add every node owned by the
    // wrapping field. The hint remains associated when an error is also present.
    $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');

    // A named control needs a stable id so the field's <label for> targets it.
    // Respect a consumer-supplied id; default it to the field name otherwise.
    $resolvedId = $attributes->get('id', filled($fieldName) ? $fieldName : null);
@endphp

<input
    type="{{ $type }}"
    @if (filled($resolvedHtmlName)) name="{{ $resolvedHtmlName }}" @endif
    @if (filled($resolvedId)) id="{{ $resolvedId }}" @endif
    @if (! is_null($resolvedValue)) value="{{ $resolvedValue }}" @endif
    data-slot="input"
    @disabled($disabled)
    @required($isRequired)
    @if ($isRequired) aria-required="true" @endif
    @if ($isInvalid) aria-invalid="true" @endif
    @if (filled($describedBy)) aria-describedby="{{ $describedBy }}" @endif
    {{ $attributes->except(['name', 'value', 'id', 'required', 'aria-required', 'aria-describedby'])->merge(['class' => $classes]) }}
/>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.4.7
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