Skip to content

Text Input

Basic Usage

The TextInput field renders a standard text input. Pass the field name to make() and it will auto-generate a label.

Example
php
TextInput::make('name');

To customize the label:

php
TextInput::make('name')->label('Full Name');

Input Types

You may change the HTML input type using shortcut methods. The preview shows common email, password, and date inputs; each shortcut method automatically adds the appropriate validation rule.

Example
php
TextInput::make('email')->email();
TextInput::make('password')->password();
TextInput::make('birthday')->date();

Password fields do not read their initial value from a bound model. This keeps stored password hashes out of serialized form data on edit screens while the field still renders, submits, and validates normally.

php
TextInput::make('password')->password();

If a rare workflow intentionally stores a plain temporary password and must show it again, opt in deliberately:

php
TextInput::make('temporary_password')
    ->password()
    ->withModelBinding();

Available shortcut methods:

php
TextInput::make('name')->string();          // type="text", validates string
TextInput::make('email')->email();          // type="email", validates string|email
TextInput::make('password')->password();    // type="password", validates string
TextInput::make('age')->number();           // type="number", validates numeric
TextInput::make('age')->integer();          // type="number", validates integer
TextInput::make('phone')->tel();            // type="tel", validates string
TextInput::make('website')->url();          // type="url", validates string|url
TextInput::make('color')->color();          // type="color", validates string|hex_color
TextInput::make('query')->search();         // type="search", validates string
TextInput::make('birthday')->date();        // type="date", validates date
TextInput::make('starts_at')->datetime();   // type="datetime-local", validates date
TextInput::make('alarm')->time();           // type="time", validates date_format:H:i

Or set the type manually:

php
TextInput::make('code')->type('text');

type() sets the native input type only and does not add validation. Use it for browser-supported types without a shortcut, then add the matching Laravel rule yourself. The frontend type surface recognizes text, email, password, number, tel, url, search, date, datetime-local, time, month, week, color, and range.

Min, Max, and Step

For numeric inputs, you may set min, max, and step attributes. The min and max methods also add the corresponding validation rules.

Example
php
TextInput::make('quantity')
    ->number()
    ->min(1)
    ->max(100)
    ->step(1);

TextInput::make('price')
    ->number()
    ->min(0.01)
    ->max(9999.99)
    ->step(0.01);

Use floats for decimal steps when the value should accept cents, percentages, weights, or other fractional values. step() is a native browser constraint only; it does not add a Laravel rule. Passing null to min(), max(), or step() clears that current option. Generated min and max validation rules follow the latest non-null values.

Length Limits

Use minLength and maxLength to constrain the input length. Both methods add validation rules automatically.

Example
php
TextInput::make('username')
    ->minLength(3)
    ->maxLength(20);

Input Mask

Apply an input mask to format the value as the user types. The preview shows phone and card masks; use 9 for digits, a for letters, and * for any character.

Example
php
TextInput::make('phone_number')->mask('(999) 999-9999');
TextInput::make('card')->mask('9999 9999 9999 9999');

Use any mask pattern that fits the value:

php
TextInput::make('zip')->mask('99999');

Masking is frontend formatting, not validation. The unmasked model value is submitted, so add Laravel rules for the required server-side shape. Pass null to mask() to disable it.

There are also shortcut methods for common masks:

php
TextInput::make('phone_number')->phone();       // tel type + (999) 999-9999 mask
TextInput::make('card_number')->creditCard();    // 9999 9999 9999 9999 mask

Clearable

Add a clear button that empties the input value:

Example
php
TextInput::make('search')->clearable();

Copyable

Add a copy-to-clipboard button. This is especially useful for readonly values:

Example
php
TextInput::make('api_key')->readonly()->copyable();

Viewable

For password fields, add a toggle to reveal the password:

Example
php
TextInput::make('password')->password()->viewable();

Keyboard Shortcut

Display a keyboard shortcut hint inside the input and register it to focus and select the control:

Example
Ctrl+K
php
TextInput::make('search')->kbd('Ctrl+K');

Icons

Add a leading or trailing icon to the input by passing an icon name. The frontend resolves that name with your configured icon resolver:

Example
php
TextInput::make('email')
    ->email()
    ->leadingIcon('MailIcon');

TextInput::make('website')
    ->url()
    ->trailingIcon('ExternalLinkIcon');

See Icons for global resolver setup, per-form resolvers, direct component usage, and Lucide examples.

Currency and Number Formatting

Format the input value as currency or a plain number. The control becomes a text input for editing, while the bound value is parsed to a raw number on blur. An empty formatted control writes an empty string. Formatting does not add a Laravel numeric rule.

Example
php
TextInput::make('price')->currency();
TextInput::make('population')->numberFormat();
TextInput::make('euro_price')->currency('de-DE', 'EUR');
TextInput::make('yen_price')->currency('ja-JP', 'JPY', 0);

Customize fixed-currency formatting when the locale, currency, or decimal precision differs.

For number formatting without a currency symbol:

php
TextInput::make('population')->numberFormat();                  // 1,234 (en-US, 0 decimals)
TextInput::make('percentage')->numberFormat('en-US', 2);        // 1,234.56
TextInput::make('weight')->numberFormat('nl-NL', 1);            // 1.234,6

Variable-currency amounts use a native select addon; see Prepend and Append. Fixed-currency amounts should continue to use currency().

ParameterDefaultDescription
locale'en-US'Locale for number formatting
currency'USD' (currency) / null (numberFormat)Currency code or null
decimals2 (currency) / 0 (numberFormat)Number of decimal places

Enter Key Hint

Set the enterkeyhint attribute to customize the Enter key label on mobile keyboards. The preview shows search and send hints; the same pattern works for the other shortcut methods.

Example
php
TextInput::make('search')->enterKeySearch();
TextInput::make('message')->enterKeySend();

Other shortcut methods:

php
TextInput::make('first_name')->enterKeyNext();
TextInput::make('last_name')->enterKeyDone();
TextInput::make('url')->enterKeyGo();

Or set it manually:

php
TextInput::make('field')->enterkeyhint('enter');

Autocomplete

Set the autocomplete attribute to help browsers autofill the input. The preview shows email and new-password hints.

Example
php
TextInput::make('email')->autocomplete('email');
TextInput::make('password')->password()->autocomplete('new-password');

Use any browser-supported autocomplete token:

php
TextInput::make('name')->autocomplete('name');

Pass null to remove the configured token.

Autofocus

Automatically focus the input when the page loads:

php
TextInput::make('name')->autofocus();

Pass false to turn autofocus off.

Prepend and Append

Add text, icons, native selects, or native submit buttons before or after the input:

Example
https://
USD
.00
php
TextInput::make('website')->url()->leadingAddon('https://');
TextInput::make('amount')->leadingAddon('USD')->trailingAddon('.00');

TextInput::make('status')
    ->leadingAddonIcon('ShieldCheckIcon')
    ->trailingAddonIcon('CheckIcon');

TextInput::make('localized_amount')
    ->label('Amount currency')
    ->numberFormat('en-US', 2)
    ->leadingSelectAddon(
        'localized_amount_currency',
        [
            'usd' => 'USD',
            'eur' => 'EUR',
            'gbp' => 'GBP',
        ],
        default: 'usd',
        label: 'Currency',
    );

TextInput::make('coupon_code')
    ->trailingButtonAddon(
        label: 'Apply',
        name: 'text_input_action',
        value: 'apply_coupon',
    );

For addon icons that should render as their own segment outside the input, use the leadingAddonIcon() / trailingAddonIcon() helpers. These use the same icon resolver as inline input icons.

Native select addons submit a sibling form value under their configured name. Use leadingSelectAddon() or trailingSelectAddon() for ordinary select segments:

php
TextInput::make('amount')
    ->leadingSelectAddon(
        'amount_currency',
        [
            'usd' => 'USD',
            'eur' => 'EUR',
            'gbp' => 'GBP',
        ],
        default: 'usd',
        rules: ['required'],
        label: 'Currency',
    );

// Submitted data contains both keys:
// ['amount' => 99, 'amount_currency' => 'usd']

Select addons support options, a default value, and validation rules. Omitting the default uses the first resolved option value; an explicit null remains empty. Resolved options always add an allowed-value rule, while additional rules default to an empty array.

Select addon options may also come from Laravel models or queries:

php
TextInput::make('amount')
    ->leadingSelectAddon(
        'amount_currency_id',
        Currency::query()->where('active', true)->orderBy('code'),
        optionLabel: 'code',
        optionValue: 'id',
    );

Use closures when labels or groups need custom PHP logic:

php
TextInput::make('estimate')
    ->leadingSelectAddon(
        'assignee_id',
        User::query()->with('role')->orderBy('role_id')->orderBy('name'),
        optionLabel: fn (User $user) => "{$user->id} {$user->name}",
        optionValue: 'id',
        grouped: 'role_id',
        groupLabel: fn (User $user) => $user->role->name,
    );

Use array maps when a resolved value needs a simple lookup:

php
TextInput::make('estimate')
    ->leadingSelectAddon(
        'assignee_id',
        User::query()->orderBy('role_id')->orderBy('name'),
        optionLabel: 'name',
        optionValue: 'id',
        grouped: 'role_id',
        groupLabel: [
            1 => 'Administrators',
            2 => 'Editors',
        ],
    );

Select addons are native selects, so grouped options render real <optgroup> elements:

php
TextInput::make('estimate')
    ->leadingSelectAddon(
        'assignee_id',
        User::query()->orderBy('role')->orderBy('name'),
        optionLabel: 'name',
        optionValue: 'id',
        grouped: 'role',
    );

For variable-currency amounts, currencySelectAddon() is a shortcut for the same select addon behavior. It also calls numberFormat() for the amount:

php
TextInput::make('amount')
    ->currencySelectAddon('amount_currency', [
        'usd' => 'USD',
        'eur' => 'EUR',
        'gbp' => 'GBP',
    ], default: 'usd');

// Submitted data contains both keys:
// ['amount' => 99, 'amount_currency' => 'usd']

currencySelectAddon() accepts the same option sources and mapping arguments as native select addons, plus formatting options:

php
TextInput::make('amount')->currencySelectAddon(
    name: 'amount_currency',
    options: Currency::query()->orderBy('code'),
    default: 'usd',
    locale: 'en-US',
    decimals: 2,
    rules: ['required'],
    label: 'Currency',
    position: 'leading',
    optionLabel: 'code',
    optionValue: 'id',
    grouped: 'region',
    groupLabel: 'region_name',
);

Unlike ordinary select addons, currencySelectAddon() defaults rules to ['required']. Passing default: null preserves an explicit empty default. Pass label to override the validation attribute and native select label. Use position: 'trailing' when the currency selector should render after the amount input.

Button addons render native submit buttons inside the joined input control. The helper arguments are the visible label, the native submitter name, and the native submitter value:

php
TextInput::make('coupon_code')
    ->trailingButtonAddon(
        label: 'Apply',
        name: 'text_input_action',
        value: 'apply_coupon',
    );

// Clicking Apply submits:
// ['coupon_code' => 'SAVE20', 'text_input_action' => 'apply_coupon']

The rendered button uses type="submit". The generated <Form> adds the clicked button's name and value to the request, just like multiple submit actions. This is submitter behavior, not a custom frontend callback. Button addon values are not part of the form's initial data.

Using the Component Directly

You may render TextInput outside the generated <Form> by passing serialized form.getField(...) props or local state.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { TextInput } from '@inertiaui/form-vue/components'
import { Check, DollarSign, Search } from '@lucide/vue'
import type { CurrencyFormat, IconResolverFn, InputAddonSegment } from '@inertiaui/form-vue'

const query = ref('winter catalog')
const amount = ref(99)
const phone = ref('')
const password = ref('')
const apiKey = ref('sk_live_123')

const icons = { CheckIcon: Check, DollarSignIcon: DollarSign, SearchIcon: Search }
const iconResolver: IconResolverFn = (icon) => icons[icon as keyof typeof icons] ?? null

const currency: CurrencyFormat = { locale: 'en-US', currency: null, decimals: 2 }
const amountAddons: InputAddonSegment[] = [
    {
        position: 'leading',
        type: 'icon',
        icon: 'DollarSignIcon',
    },
    {
        position: 'leading',
        type: 'select',
        name: 'currency',
        label: 'Currency',
        default: 'usd',
        required: true,
        options: [
            { value: 'usd', label: 'USD' },
            { value: 'eur', label: 'EUR' },
        ],
    },
    {
        position: 'trailing',
        type: 'text',
        content: 'net',
    },
    {
        position: 'trailing',
        type: 'button',
        content: 'Apply',
        name: 'text_input_action',
        value: 'apply_amount',
    },
]

const fieldParts = {
    input: 'font-mono',
    addon: 'min-w-16 justify-center',
    action: 'text-blue-600',
}

const trackBlur = () => {}
</script>

<template>
    <div class="grid gap-4">
        <TextInput
            v-model="query"
            id="product-search"
            name="query"
            label="Search"
            help="Search by product name or SKU."
            placeholder="Search products..."
            input-type="search"
            autocomplete="off"
            autofocus
            clearable
            kbd="Ctrl+K"
            leading-icon="SearchIcon"
            trailing-icon="CheckIcon"
            :icon-resolver="iconResolver"
            badge="Live"
            badge-class="bg-zinc-100 text-zinc-700"
            label-trailing="Optional"
            label-trailing-class="text-zinc-500"
            tooltip="Filters the product list."
            help-position="below"
            layout="stacked"
            control-position="end"
            label-class="font-medium"
            help-class="text-zinc-500"
            error-class="text-red-600"
            wrapper-class="max-w-xl"
            control-class="bg-white"
            class="font-medium"
            :part-classes="fieldParts"
            data-testid="product-search"
            aria-label="Product search"
            @blur="trackBlur"
        />

        <TextInput
            v-model="amount"
            name="amount"
            label="Amount"
            input-type="number"
            :min="0"
            :max="9999"
            :step="0.01"
            pattern="[0-9]+([.,][0-9]{2})?"
            :currency="currency"
            :addons="amountAddons"
            enterkeyhint="done"
            required
            precognitive
        />

        <TextInput
            v-model="phone"
            name="phone"
            label="Phone"
            input-type="tel"
            mask="(999) 999-9999"
            :min-length="14"
            :max-length="14"
            autocomplete="tel"
            enterkeyhint="next"
        />

        <TextInput
            v-model="password"
            name="password"
            label="Password"
            input-type="password"
            placeholder="Enter password"
            viewable
            required
            invalid
            error="Password is required."
        />

        <TextInput
            v-model="apiKey"
            name="api_key"
            label="API key"
            readonly
            disabled
            copyable
            label-sr-only
        />
    </div>
</template>
tsx
import { useState } from 'react'
import { TextInput } from '@inertiaui/form-react/components'
import { CheckIcon, DollarSignIcon, SearchIcon } from 'lucide-react'
import type { CurrencyFormat, IconResolverFn, InputAddonSegment } from '@inertiaui/form-react'

const icons = { CheckIcon, DollarSignIcon, SearchIcon }
const iconResolver: IconResolverFn = (icon) => icons[icon as keyof typeof icons] ?? null

const currency: CurrencyFormat = { locale: 'en-US', currency: null, decimals: 2 }
const amountAddons: InputAddonSegment[] = [
    {
        position: 'leading',
        type: 'icon',
        icon: 'DollarSignIcon',
    },
    {
        position: 'leading',
        type: 'select',
        name: 'currency',
        label: 'Currency',
        default: 'usd',
        required: true,
        options: [
            { value: 'usd', label: 'USD' },
            { value: 'eur', label: 'EUR' },
        ],
    },
    {
        position: 'trailing',
        type: 'text',
        content: 'net',
    },
    {
        position: 'trailing',
        type: 'button',
        content: 'Apply',
        name: 'text_input_action',
        value: 'apply_amount',
    },
]

const fieldParts = {
    input: 'font-mono',
    addon: 'min-w-16 justify-center',
    action: 'text-blue-600',
}

export function DirectTextInputs() {
    const [query, setQuery] = useState('winter catalog')
    const [amount, setAmount] = useState(99)
    const [phone, setPhone] = useState('')
    const [password, setPassword] = useState('')
    const [apiKey, setApiKey] = useState('sk_live_123')

    const trackBlur = () => {}

    return (
        <div className="grid gap-4">
            <TextInput
                value={query}
                onValueChange={setQuery}
                id="product-search"
                name="query"
                label="Search"
                help="Search by product name or SKU."
                placeholder="Search products..."
                inputType="search"
                autocomplete="off"
                autofocus
                clearable
                kbd="Ctrl+K"
                leadingIcon="SearchIcon"
                trailingIcon="CheckIcon"
                iconResolver={iconResolver}
                badge="Live"
                badgeClass="bg-zinc-100 text-zinc-700"
                labelTrailing="Optional"
                labelTrailingClass="text-zinc-500"
                tooltip="Filters the product list."
                helpPosition="below"
                layout="stacked"
                controlPosition="end"
                labelClass="font-medium"
                helpClass="text-zinc-500"
                errorClass="text-red-600"
                wrapperClass="max-w-xl"
                controlClass="bg-white"
                class="font-medium"
                partClasses={fieldParts}
                data-testid="product-search"
                aria-label="Product search"
                onBlur={trackBlur}
            />

            <TextInput
                value={amount}
                onValueChange={setAmount}
                name="amount"
                label="Amount"
                inputType="number"
                min={0}
                max={9999}
                step={0.01}
                pattern="[0-9]+([.,][0-9]{2})?"
                currency={currency}
                addons={amountAddons}
                enterkeyhint="done"
                required
                precognitive
            />

            <TextInput
                value={phone}
                onValueChange={setPhone}
                name="phone"
                label="Phone"
                inputType="tel"
                mask="(999) 999-9999"
                minLength={14}
                maxLength={14}
                autocomplete="tel"
                enterkeyhint="next"
            />

            <TextInput
                value={password}
                onValueChange={setPassword}
                name="password"
                label="Password"
                inputType="password"
                placeholder="Enter password"
                viewable
                required
                invalid
                error="Password is required."
            />

            <TextInput
                value={apiKey}
                onValueChange={setApiKey}
                name="api_key"
                label="API key"
                readonly
                disabled
                copyable
                labelSrOnly
            />
        </div>
    )
}

Vue uses v-model. React uses controlled value plus onValueChange. Native attributes and listeners target the underlying <input>; see Native Attributes And Events.