Repository navigation
Consider collapsing outlined/filled text inputs into one text-input element with a variant prop #26
Description
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
-
src/Elements/TextInput.php— mergeOutlined/Filledinto one element with avariantprop; reduce the two existing classes to alias subclasses setting a default variant -
src/Components/TextInput.php— Blade component for the newtext_inputtype -
nativephp.json— addtext_input; repointoutlined_text_input/filled_text_inputat the same renderers - iOS — merge the two renderers into
NativeUITextInputRendererwith aswitch variantaround the chrome layer, sharingNativeUITextInputCore - Android — one renderer with
when (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 here - mobile-air:
src/Testing/TestableComponent.php:1169-1172(type switch) andsrc/Validation/BladeTemplateAnalyzer.php:12,40,45(element /SUPPORTS_CHANGE/SUPPORTS_SUBMITlists) need the new type - Docs, boost guidelines,
nativephp-mobileskill
Open questions
- Do we take the
text_inputname, or keep the unified element under a different tag to avoid any confusion with core's benchmark-onlyEdge\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.
Activity
simonhamp commentedon Sep 4, 2026
Didn't we determine that this wasn't going to be viable?
Give the outlined text input a fill color of its own (#60)