Summary
We currently ship three separate text input elements — outlined-text-input, filled-text-input, bare-text-input — each with its own element class, Blade component, manifest entry, iOS renderer and Android renderer. outlined and filled are pure cosmetic variants of an identical contract and could collapse into a single <native:text-input variant="outlined|filled">, matching the pattern button already uses.
Filing this for awareness rather than as an urgent fix. Nothing is broken — this is API surface + maintenance cost.
Current state
| Tag |
Chrome |
Honors element styles |
Per-instance color |
outlined-text-input |
border, floating label, supporting text, icons |
❌ |
❌ |
filled-text-input |
surface fill + bottom indicator, same decorations |
❌ |
❌ |
bare-text-input |
none |
✅ |
✅ |
text-input (core) |
n/a — see below |
n/a |
n/a |
All three inherit behaviour (value sync, echo prevention, sync modes, secure/multiline, keyboard type, submit, disabled/readOnly) from BaseTextInput. OutlinedTextInput and FilledTextInput are 15-line subclasses that only override $type.
Why this looks cheap
1. button is the precedent. One button type, one renderer, a semantic variant prop, a switch variant inside NativeUIButtonRenderer. Same shape applies here.
2. The text_input type is unclaimed. Core explicitly reserves it for us:
// mobile-air src/NativeServiceProvider.php:682
// `button`, `text_input`, `toggle`, `activity_indicator`, `bottom_sheet`
// are registered by UI plugins (see nativephp/native-ui). Plugin
// discovery can't override an existing registration, so these must NOT be registered here.
Confirmed: nothing registers text_input in ElementRegistry, and no "text_input" renderer exists in a generated app on either platform. Edge/Elements/TextInput.php in core is benchmark-only. The name is free.
3. The native code is already mostly shared. NativeUIOutlinedTextInputRenderer.swift (179 loc) vs NativeUIFilledTextInputRenderer.swift (182 loc) differ by roughly 30 lines — border stroke vs surfaceVariant fill + indicator rectangle, plus slightly different SizeMetrics padding. Both already wrap NativeUITextInputCore.swift. On Android, TextInputShared.kt (249 loc) already holds parseTextInputProps, the echo-prevention sync and the sync-mode dispatcher for all three.
4. Dispatch is a generated string map. nativephp.json → PluginRendererRegistration.{swift,kt}. Collapsing types is a manifest edit, not build-system surgery.
5. Back-compat is close to free. The manifest can keep outlined_text_input / filled_text_input registered against the same renderer, with the existing 15-line subclasses changing from "set $type" to "set default variant". Old tags keep working.
The caveat: bare should probably stay separate
bare isn't a visual variant — it has a different contract. BaseTextInput enforces a Model 3 style lockout:
// src/Elements/BaseTextInput.php:393
public function getStyle(): array { return []; }
public function getLayout(): array { $layout = parent::getLayout(); unset($layout['padding']); return $layout; }
BareTextInput (src/Elements/BareTextInput.php:79-87) overrides both to let element styles and padding through, and additionally accepts a per-instance color. Meanwhile it drops label, supporting, error, icons and prefix/suffix entirely — the iOS renderer is 88 loc precisely because it renders none of them.
Folding that into a variant means <text-input variant="bare" label="Email"> silently discards the label, and <text-input variant="outlined" class="rounded-full"> silently discards the radius. Which half of your attributes are live would depend on one other attribute, with no error. Three tags with honest contracts arguably document themselves better than one tag with a hidden mode switch.
Proposed shape
Unify outlined + filled, leave bare alone:
<native:text-input variant="outlined" label="Email" native:model.blur="email" />
<native:text-input variant="filled" label="Subject" native:model="subject" />
<native:bare-text-input class="flex-1 glass rounded-full px-4 py-2" placeholder="Message" native:model="draft" />
variant defaults to outlined. <outlined-text-input> / <filled-text-input> stay as deprecated aliases.
If we'd rather collapse all three anyway, the mitigation is to make the ignored props loud — throw or dev-warn when variant="bare" is combined with label / supporting / icons, turning the footgun into a build-time error.
Work sketch
Open questions
- Do we take the
text_input name, or keep the unified element under a different tag to avoid any confusion with core's benchmark-only Edge\Elements\TextInput?
- Deprecate the old tags with a warning, or leave the aliases indefinitely?
- Same question applies to other variant-ish element families — worth a quick audit for consistency with
button.
Summary
We currently ship three separate text input elements —
outlined-text-input,filled-text-input,bare-text-input— each with its own element class, Blade component, manifest entry, iOS renderer and Android renderer.outlinedandfilledare pure cosmetic variants of an identical contract and could collapse into a single<native:text-input variant="outlined|filled">, matching the patternbuttonalready uses.Filing this for awareness rather than as an urgent fix. Nothing is broken — this is API surface + maintenance cost.
Current state
coloroutlined-text-inputfilled-text-inputbare-text-inputtext-input(core)All three inherit behaviour (value sync, echo prevention, sync modes,
secure/multiline, keyboard type, submit, disabled/readOnly) fromBaseTextInput.OutlinedTextInputandFilledTextInputare 15-line subclasses that only override$type.Why this looks cheap
1.
buttonis the precedent. Onebuttontype, one renderer, a semanticvariantprop, aswitch variantinsideNativeUIButtonRenderer. Same shape applies here.2. The
text_inputtype is unclaimed. Core explicitly reserves it for us:Confirmed: nothing registers
text_inputinElementRegistry, and no"text_input"renderer exists in a generated app on either platform.Edge/Elements/TextInput.phpin core is benchmark-only. The name is free.3. The native code is already mostly shared.
NativeUIOutlinedTextInputRenderer.swift(179 loc) vsNativeUIFilledTextInputRenderer.swift(182 loc) differ by roughly 30 lines — border stroke vssurfaceVariantfill + indicator rectangle, plus slightly differentSizeMetricspadding. Both already wrapNativeUITextInputCore.swift. On Android,TextInputShared.kt(249 loc) already holdsparseTextInputProps, the echo-prevention sync and the sync-mode dispatcher for all three.4. Dispatch is a generated string map.
nativephp.json→PluginRendererRegistration.{swift,kt}. Collapsing types is a manifest edit, not build-system surgery.5. Back-compat is close to free. The manifest can keep
outlined_text_input/filled_text_inputregistered against the same renderer, with the existing 15-line subclasses changing from "set$type" to "set defaultvariant". Old tags keep working.The caveat:
bareshould probably stay separatebareisn't a visual variant — it has a different contract.BaseTextInputenforces a Model 3 style lockout:BareTextInput(src/Elements/BareTextInput.php:79-87) overrides both to let element styles and padding through, and additionally accepts a per-instancecolor. Meanwhile it dropslabel,supporting,error, icons andprefix/suffixentirely — the iOS renderer is 88 loc precisely because it renders none of them.Folding that into a
variantmeans<text-input variant="bare" label="Email">silently discards the label, and<text-input variant="outlined" class="rounded-full">silently discards the radius. Which half of your attributes are live would depend on one other attribute, with no error. Three tags with honest contracts arguably document themselves better than one tag with a hidden mode switch.Proposed shape
Unify
outlined+filled, leavebarealone:variantdefaults tooutlined.<outlined-text-input>/<filled-text-input>stay as deprecated aliases.If we'd rather collapse all three anyway, the mitigation is to make the ignored props loud — throw or dev-warn when
variant="bare"is combined withlabel/supporting/ icons, turning the footgun into a build-time error.Work sketch
src/Elements/TextInput.php— mergeOutlined/Filledinto one element with avariantprop; reduce the two existing classes to alias subclasses setting a default variantsrc/Components/TextInput.php— Blade component for the newtext_inputtypenativephp.json— addtext_input; repointoutlined_text_input/filled_text_inputat the same renderersNativeUITextInputRendererwith aswitch variantaround the chrome layer, sharingNativeUITextInputCorewhen (variant)over the two composable call sites. NoteOutlinedTextFieldandTextFieldare distinct composables with distinctDefaults.colors()types, so this branch can't be parameterized away; the real dedup already lives inTextInputShared.kt, so Android gains least heresrc/Testing/TestableComponent.php:1169-1172(type switch) andsrc/Validation/BladeTemplateAnalyzer.php:12,40,45(element /SUPPORTS_CHANGE/SUPPORTS_SUBMITlists) need the new typenativephp-mobileskillOpen questions
text_inputname, or keep the unified element under a different tag to avoid any confusion with core's benchmark-onlyEdge\Elements\TextInput?button.