Persistent label, bound to the field via htmlFor / id. Never a placeholder.
value *
string
n/a
Current value (controlled).
onChange
(value: string) => void
n/a
Change handler. Ignored while disabled or read-only.
placeholder
string
n/a
Ghost prompt INSIDE the well. It is supplementary, never the label.
helperText
string
n/a
Helper text under the well. Replaced by errorText / statusText when set.
errorText
string
n/a
When set, the area renders its error treatment (red border + this message).
status
FieldStatus
n/a
The validation ladder: warning / success (or error) border + a matching, icon-carrying help line showing statusText. errorText (and an over-cap counter) still win if set. Color is never the only signal. A glyph rides along.
statusText
string
n/a
The message shown for status (ignored when errorText / over-cap wins).
required
boolean
false
Marks the field required: renders the status-colored * after the label.
disabled
boolean
false
Inert + clearly-seen (never invisible).
readOnly
boolean
false
Value shown, selectable, not editable.
maxLength
number
n/a
Optional max length; drives the character counter + over-cap error.
minRows
number
3
Rows the field starts at before it auto-grows.
maxRows
number
8
Rows it grows to before it starts scrolling instead.
Also accepts the native <textarea> attributes (e.g. onClick, disabled, id, aria-*, data-*), forwarded straight through, plus a forwarded ref.
The Kotlin tab documents the multi-line field idiom rather than an extracted component:
the same OutlinedTextField without singleLine, with these parameters. The recessed-well
treatment and the counter are not part of that idiom (M3 outlined boxes, and the counter is
a manual supportingText); tracked in drift below.
Parameter
Type
Default
Description
value *
String
n/a
Current text value.
onValueChange *
(String) -> Unit
n/a
Invoked on every edit. Enforce a length cap here (no built-in maxLength).
label
@Composable (() -> Unit)?
null
Floating label slot: the persistent label.
maxLines
Int
Int.MAX_VALUE
The area auto-grows up to this many lines, then scrolls. (Leave singleLine off.)
minLines
Int
1
Starting height in lines.
isError
Boolean
false
Renders the error treatment (red outline + error-colored label/supporting text).
supportingText
@Composable (() -> Unit)?
null
Helper / error line, and where the current / max counter is rendered.
shape
Shape
NockerlControlShape
Pass the NockerlControlShape token (12dp) so the area shares the one control radius.
colors
TextFieldColors
OutlinedTextFieldDefaults.colors(…)
Set focusedBorderColor = colorScheme.primary for the brand-cyan focus edge.
SwiftUI applies the area chrome as the .nockerlFieldWell() view modifier on a
vertical-axis TextField. State (focus, editing) comes from the SwiftUI environment.
There are no explicit state parameters, and the label, counter, and error are sibling
views (no bundled slots yet).
Style
Type
Default
Description
.nockerlFieldWell()
ViewModifier
n/a
The published recessed input chrome: the canvasAlt plane, an inner top shade, the control radius, and the cardHairline edge. Wrap a .textFieldStyle(.plain) field with it.
axis: .vertical
TextField init arg
n/a
Makes the TextField multi-line (macOS 13+) so it can grow.
.lineLimit(min...max)
modifier
n/a
Auto-grow bounds: grows between the two line counts, then scrolls.
.foregroundStyle(NockerlTheme.onSurface)
modifier
n/a
Binds the typed-text color to the on-surface token.
Cross-platform drift (tracked for reconciliation)
The recessed-well spec is the target; these are the real divergences between the shipped
platforms, tracked in the Review queue:
No single shared component: each platform reaches the area by its own route. Web exposes
NockerlTextArea from @dizyx/nockerl-react; Voice has the shared
.nockerlFieldWell() modifier (reused for multi-line); Android repeats
OutlinedTextField inline. A common extraction with a bundled counter is still owed.
Recessed vs outlined: Voice + the spec are recessed wells; Android’s
OutlinedTextField is an outlined box (no inset). It should adopt the inset surface.
Control radius: Voice hardcodes 8pt; Android uses the 12dp token; web’s
shadcn textarea is rounded-md (6px). The well must be the 12px control radius
everywhere.
Counter: only web (canonical) bundles the current / max counter + over-cap error.
Android/Voice render it manually (or omit it). It should be a first-class prop.
Auto-grow: web (JS), Compose (maxLines), and Swift (.lineLimit) each auto-grow,
but via three different mechanisms; behavior is aligned, implementation is not.