- Getting Started
- Architecture
- The Basics
- Digging Deeper
-
EDGE Components
- Introduction
- Accordion
- Activity Indicator
- Badge
- Bottom Navigation
- Bottom Sheet
- Button
- Button Group
- Canvas
- Carousel
- Checkbox
- Chip
- Column
- Divider
- Floating Action Button
- Gesture Area
- Icon
- Image
- Lazy Grid
- Layout & Styling
- List
- Menus
- Modal
- Pressable
- Progress Bar
- Radio Group
- Refreshable
- Row
- Scroll View
- Select
- Shapes
- Side Navigation
- Slider
- Spacer
- Stack
- Tab Row
- Text
- Text Input
- Toggle
- Top Bar
- Virtual List
- Web View
- Plugins
- Testing
- Publishing Your App
Text Input
Get the Jump app
You'll need the free Jump app to preview this page on your device. Install it, then scan the code again.
Overview#
Native text input fields come in three variants:
<native:outlined-text-input>— bordered field. Default, lower emphasis.<native:filled-text-input>— surface-fill background + bottom indicator line. Higher emphasis.<native:bare-text-input>— chromeless field with no Material chrome, for chat pills, search bars, and inline editors where the surrounding container supplies the visuals. See Bare variant.
All three share the same prop set and event API. Choose the outlined / filled pair based on emphasis, not behavior; reach for bare when you want to style the input yourself.
On iOS the outlined and filled variants render as SwiftUI TextField / SecureField with Material3-style chrome; on
Android they map to OutlinedTextField / TextField (filled). Per Material 3 these two have no per-instance color or
border overrides — all chrome resolves from the theme. For fully custom input visuals reach for the bare variant, or
drop to <native:pressable> wrapping your own drawing.
@php $email = ''; @endphp <native:outlined-text-input label="Email" native:model="email" keyboard="email" leading-icon="email"/>
email is a public string property on your component — the @php line stands in for
public string $email = ''; and seeds the inline preview.
Props#
All three variants accept the same shared prop set. The bare variant adds a color attribute on top — see
Bare variant.
Content#
value- Current text value (optional, string)placeholder- Placeholder shown when empty (optional, string)label- Label rendered above the field (optional, string)supporting- Helper text rendered below the field (optional, string)
State#
disabled- Disable the input (optional, boolean, default:false)read-only- Make the input read-only (optional, boolean, default:false)is-error- Show error styling (border / indicator + supporting text turntheme.destructive)loading- Show a spinner in the trailing position (optional, boolean, default:false)
Behavior#
keyboard- Keyboard hint string:text(default),number,email,phone,url,decimal,password,numberPassword. On iOSpassworduses the standard keyboard;secureis the masking mechanism. The keyboard type also decides capitalization and autocorrect — see Capitalizationautocapitalize- Override that capitalization:none,sentences,words, orcharacters(HTML's vocabulary). Leave it unset to letkeyboarddecide (optional, string)secure- Mask input for passwords (optional, boolean, default:false)multiline- Allow multiple lines (optional, boolean, default:false)max-length- Maximum character count (optional, int)max-lines- Maximum visible lines whenmultiline(optional, int)min-lines- Minimum visible lines whenmultiline(optional, int)keep-focus-on-submit- Keep the keyboard up after@submitinstead of unfocusing the field on return — the chat "send and keep typing" pattern (optional, boolean, default:false)sync-mode- How change events dispatch back to your component:live(default),blur, ordebounce. Usually set via thenative:modelmodifiers below, but accepted directly toodebounce-ms- Milliseconds of inactivity before adebouncesync fires (optional, int, default:300)selection-debounce-ms- Coalescing window for@selectionChangeevents (optional, int, default:150).0or less means the default; positive values are floored at one frame (16ms). See Caret and selection reporting
Decorations#
prefix- Text rendered before the input (optional, string)suffix- Text rendered after the input (optional, string)leading-icon- Icon name rendered at the start (optional, string)trailing-icon- Icon name rendered at the end (optional, string)ios-leading-icon/android-leading-icon- Per-platform overrides forleading-icon(optional). See Per-platform iconsios-trailing-icon/android-trailing-icon- Per-platform overrides fortrailing-icon(optional)
Typography#
font- Custom font: aresources/fonts/file token or a config alias likeaccent(optional, string) — see Text › Custom fontsleading-*classes set line height for the typed text (multi-line only). Applies on Android; iOS inputs don't reflect it — SwiftUI's editable field ignores line spacing (it works on<native:text>)line-height/line-height-pxattributes are an alternative to theleading-*classes:line-heightis a multiplier of the font size,line-height-pxan absolute override
Sizing & accessibility#
size-sm | md (default) | lga11y-label- Accessibility label (optional)a11y-hint- Accessibility hint (optional)
Capitalization#
Declaring a keyboard type carries its typing behaviour with it, not just the key layout. Fields whose content is
case-sensitive or non-alphabetic never capitalize, and never autocorrect:
keyboard |
Capitalization | Autocorrect |
|---|---|---|
text (default) |
sentences on iOS, none on Android | on |
email, url |
none | off |
number, decimal, phone, password, numberPassword |
none | off |
{{-- Email keyboard AND no capitalized first letter — no extra attribute needed --}}<native:outlined-text-input native:model="email" label="Email" keyboard="email" />
Use autocapitalize for the cases a keyboard type can't imply:
<native:outlined-text-input native:model="name" label="Full name" autocapitalize="words" /><native:outlined-text-input native:model="code" label="Booking reference" autocapitalize="characters" />
autocapitalize always wins over the derived value, and an unrecognised value falls back to the derived behaviour
rather than erroring.
Events#
@change- Component method called when the text changes. Receives the new value@submit- Component method called when the user submits (e.g. presses return). Receives the current value@selectionChange- Component method called when the caret moves or the selection changes. Receives the full current text plus the selection start and end offsets — see Caret and selection reporting
Two-way Binding#
Use the native:model directive for automatic two-way binding with a component property. The directive expands to
:value, @change="__syncProperty(...)", and a sync-mode prop driven by the modifier chain.
@php $name = 'Ada'; $email = ''; $search = ''; @endphp <native:outlined-text-input label="Name" native:model="name" /><native:outlined-text-input label="Email" native:model.blur="email" /><native:outlined-text-input label="Search" native:model.debounce.500ms="search" /> <native:text class="text-sm text-theme-on-surface-variant">Hello, {{ $name }}!</native:text>
name, email, and search are public string properties on your component — typing syncs them back
automatically, so the {{ $name }} echo updates as you type.
sync-mode semantics:
live(default) — every keystroke fires@changeblur— only fires on focus loss / submitdebounce— fires afterdebounce-msof inactivity (300ms when unset), or immediately on blur / submit
Caret and selection reporting#
@selectionChange reports caret position and text selection back to your component — for the cases where @change
alone can't tell you where the user is typing. The handler receives the full current text plus the selection range:
public function onCaretMove(string $text, int $selectionStart, int $selectionEnd){ // $selectionStart === $selectionEnd when the caret is a plain cursor; // they differ when a range of text is selected.}
Offsets are Unicode code points into the text — not UTF-16 units or bytes — so emoji count as one character and the
values are safe to feed straight into mb_substr().
Events are coalesced on the native side: at most one every 150ms while the caret moves, and the trailing position
always fires. Tune the window per input with selection-debounce-ms (fluent: ->selectionDebounceMs()) — 0 or
less means the default, positive values are floored at one frame (16ms).
The classic use is a mention / typeahead trigger:
@php $message = ''; @endphp <native:outlined-text-input label="Message" native:model="message" @selectionChange="onCaretMove"/>
public array $suggestions = []; public function onCaretMove(string $text, int $start, int $end): void{ // Look backwards from the caret for an "@mention" trigger. $before = mb_substr($text, 0, $start, 'UTF-8'); if (preg_match('/@(\w*)$/u', $before, $m)) { $this->suggestions = $this->matchingHandles($m[1]); } else { $this->suggestions = []; }}
The handler slices the text at the caret, so typing @ja in the middle of a sentence surfaces suggestions for the
fragment under the cursor — something @change can't do, since it only carries the value.
A few contract details:
@selectionChangeis never emitted forsecureinputs. The callback isn't even serialized whensecureis set, and both renderers additionally refuse to emit — so caret telemetry can't leak password-field context.- When PHP pushes a new
valueonto the input, the field is replaced wholesale and the caret drops at the end. Both platforms report that immediately as a single(text, length, length)event, bypassing the debounce — a handler that rewrites the bound model will see one follow-up event. - Discontiguous selections (multi-range, iOS) are reported as a single span from the lowest start to the highest end, so the range can cover text the user did not select.
read-onlyinputs don't report on iOS, where read-only implies disabled and the field never focuses; they do on Android, which keeps them focusable for copy.
Per-platform icons#
The shared leading-icon / trailing-icon names render the same icon on both platforms. When each platform should
show its own symbol, prefix the attribute with the platform — the same convention as
<native:button>'s ios-icon:
<native:outlined-text-input label="Email" leading-icon="email" ios-leading-icon="envelope.badge"/>
iOS renders the envelope.badge SF Symbol; Android falls back to the shared email name. The shared attribute is
the fallback for whichever platform has no override — set only a platform-prefixed attribute and the other platform
renders no icon at all.
Bound with :, the icon attributes also accept the typed icon enums (App\Icons\Ios, App\Icons\Android,
App\Icons\AndroidOutlined) instead of strings — see Icon › Typed icon enums:
@use('App\Icons\Ios')@use('App\Icons\AndroidOutlined') <native:outlined-text-input label="Search" :ios-leading-icon="Ios::Magnifyingglass" :android-leading-icon="AndroidOutlined::Search"/>
Bare variant#
<native:bare-text-input> is a chromeless input — no outline, no fill, no label, no Material chrome, just the typing
affordance. It's built for chat input pills, search bars, and inline editors where the surrounding container provides
the visuals. On iOS it renders as a plain SwiftUI TextField; on Android as a Compose BasicTextField.
It inherits the full shared prop set — native:model, secure, multiline, keyboard, @submit,
keep-focus-on-submit, disabled, read-only, and the rest — so it behaves exactly like the other variants.
Two things set it apart:
- Class-based styling passes through. Unlike the filled / outlined variants (which resolve all chrome from the
theme), the bare variant lets element-level styling flow to the input directly:
bg,rounded-*, borders,glass, opacity, elevation, and padding. So you can style the pill on the input itself, no wrapping row needed. - A
colorattribute sets the text color — a hex value or a Tailwind token, withdark:text-*support for a light/dark pair. Useful when your wrapper overrides the background and the theme's default text color would vanish.
@php $draft = ''; @endphp <native:bare-text-input class="flex-1 glass rounded-full px-4 py-2 dark:text-slate-700" placeholder="Message" native:model="draft" @submit="send" keep-focus-on-submit/>
The color attribute can be set explicitly or picked up from a text-* class on the input:
@php $query = ''; @endphp <native:bare-text-input placeholder="Search" native:model="query" color="slate-700" /><native:bare-text-input placeholder="Search" native:model="query" class="text-slate-700 dark:text-slate-300" />
Examples#
Login form#
@php $email = ''; $password = ''; @endphp <native:column class="w-full gap-4 p-4"> <native:outlined-text-input label="Email" native:model="email" keyboard="email" leading-icon="email" /> <native:outlined-text-input label="Password" placeholder="Enter password" native:model="password" secure leading-icon="lock" /> <native:button label="Sign In" @press="login" /></native:column>
Filled variant with validation error#
@php $email = 'not-an-email'; @endphp <native:filled-text-input label="Email" native:model="email" is-error supporting="Please enter a valid email address"/>
Multiline textarea#
@php $message = ''; @endphp <native:outlined-text-input label="Message" placeholder="Type your message..." native:model="message" multiline :min-lines="3" :max-lines="8"/>
Search with submit#
@php $query = ''; @endphp <native:filled-text-input placeholder="Search..." native:model.debounce.300ms="query" @submit="submitSearch" leading-icon="search"/>
Prefix and suffix#
@php $price = '49'; @endphp <native:outlined-text-input label="Price" native:model="price" prefix="$" suffix=".00" keyboard="decimal"/>
Element#
use Nativephp\NativeUi\Elements\OutlinedTextInput;use Nativephp\NativeUi\Elements\FilledTextInput;use Nativephp\NativeUi\Elements\BareTextInput; OutlinedTextInput::make() ->label('Email') ->value($email) ->keyboard('email') ->leadingIcon('email') ->onChange('updateEmail');
All three elements share the same fluent API (defined on BaseTextInput):
value(string $text),placeholder(string $text),label(string $text),supporting(string $text)disabled(bool $value = true),readOnly(bool $value = true),error(bool $value = true),loading(bool $value = true)keyboard(string|int $type),secure(bool $value = true),maxLength(int $length)multiline(bool $value = true),maxLines(int $lines),minLines(int $lines)keepFocusOnSubmit(bool $value = true)- Keep the keyboard up after@submitprefix(string $text),suffix(string $text)leadingIcon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)- pass a shared$name, or per-platform$ios/$androidsymbols for a different icon on each platformtrailingIcon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)- same per-platform form asleadingIcon()size(string $value)-sm | md | lgfont(string $name)- Custom font (file token or config alias)a11yLabel(string $value),a11yHint(string $value)syncMode(string $mode),debounceMs(int $ms)selectionDebounceMs(int $ms)- Coalescing window for@selectionChangeevents (150ms when unset;0or less means the default, positive values floored at 16ms)onChange(string $method),onSubmit(string $method),onSelectionChange(string $method)
BareTextInput adds one method on top of the shared API:
color(string $color)- Text color as a hex value or Tailwind token (withdark:text-*support)
in no time