Text input
Text fields
Lets people enter text: filled and outlined, with labels, icons, prefixes and suffixes, counters and multiple lines.
- COMPONENT_SPEC §26
- CSS · custom elements · React
- Open in the playground
Examples
Filled and outlined
The label floats inside the field (filled), in the outline's notch (outlined) or above it (labelPosition="above").
The live demo runs with JavaScript. Its code is below.
import { TextField } from '@kyralabs/m3exp-web/react';
export default function Variants() {
return (
<div style={{ display: 'grid', gap: 'var(--m3e-sys-space-300)', inlineSize: '100%' }}>
<TextField variant="filled" label="First name" supportingText="As on your passport" />
<TextField variant="outlined" label="Last name" />
<TextField variant="outlined" label="Nickname" labelPosition="above" placeholder="Optional" />
</div>
);
}
<div style="display: grid; gap: var(--m3e-sys-space-300); inline-size: 100%">
<m3e-text-field
variant="filled"
label="First name"
supporting-text="As on your passport"
></m3e-text-field>
<m3e-text-field variant="outlined" label="Last name"></m3e-text-field>
<m3e-text-field
variant="outlined"
label="Nickname"
label-position="above"
placeholder="Optional"
></m3e-text-field>
</div>
Run the HTML version (custom elements, no framework).
Errors, affixes and counters
An error message that replaces the supporting text and is announced, a prefix and suffix, a multiline field with a live character counter, and a disabled field.
The live demo runs with JavaScript. Its code is below.
import { Icon, TextField } from '@kyralabs/m3exp-web/react';
export default function States() {
return (
<div style={{ display: 'grid', gap: 'var(--m3e-sys-space-300)', inlineSize: '100%' }}>
<TextField
variant="outlined"
label="Email"
type="email"
defaultValue="ada@example"
isInvalid
errorMessage="Enter a full address, like ada@example.com"
trailingIcon={<Icon name="error" />}
/>
<TextField
variant="filled"
label="Amount"
inputMode="decimal"
prefix="€"
suffix="EUR"
leadingIcon={<Icon name="payments" />}
/>
<TextField variant="outlined" label="Bio" multiline maxLength={120} />
<TextField variant="filled" label="Account" defaultValue="ada-lovelace" isDisabled />
</div>
);
}
<div style="display: grid; gap: var(--m3e-sys-space-300); inline-size: 100%">
<m3e-text-field
variant="outlined"
label="Email"
type="email"
value="ada@example"
invalid
error-text="Enter a full address, like ada@example.com"
trailing-icon="error"
></m3e-text-field>
<m3e-text-field
variant="filled"
label="Amount"
inputmode="decimal"
prefix-text="€"
suffix-text="EUR"
leading-icon="payments"
></m3e-text-field>
<m3e-text-field variant="outlined" label="Bio" multiline maxlength="120"></m3e-text-field>
<m3e-text-field variant="filled" label="Account" value="ada-lovelace" disabled></m3e-text-field>
</div>
Run the HTML version (custom elements, no framework).
API
React
<TextField>
Text field (COMPONENT_SPEC §26, Compose TextField / OutlinedTextField): React Aria useTextField on the m3e-css .m3e-text-field contract, the same DOM as <m3e-text-field>. The ref is the native input (or textarea). The label float, colors and the outlined notch are CSS (text-field.css); this adds the live counter and, where CSS field-sizing is missing, the growing textarea.
| Prop | Type | Default | Description |
|---|---|---|---|
variant
|
"filled" | "outlined"
|
Filled (default) or outlined (Compose TextField / OutlinedTextField).
|
|
label
|
ReactNode
|
The visible label (a <label for>); without it name the field with aria-label.
|
|
labelPosition
|
"above" | "inside" | "cutout"
|
Compose's TextFieldLabelPosition: inside (filled default) and cutout (outlined default)
follow the variant; above puts the label over the container, always minimized.
|
|
description
|
ReactNode
|
Supporting text under the field (description in React Aria terms).
|
|
supportingText
|
ReactNode
|
Alias of description (Compose supportingText).
|
|
errorMessage
|
ReactNode
|
Shown instead of the supporting text while invalid, with the "alert" role. | |
isInvalid
|
boolean
|
Error colors and aria-invalid (Compose isError).
|
|
isRequired
|
boolean
|
aria-required and a "*" after the label (m3.material.io).
|
|
leadingIcon
|
ReactNode
|
A Material Symbols name (a decorative icon) or a node (e.g. an icon button). | |
prefix
|
ReactNode
|
Text before / after the input, shown while the label is minimized. | |
maxLength
|
number
|
Limits the input and shows the character counter ("n/max"). | |
counterLabel
|
string
|
The counter's accessible prefix (default "Character count", m3.material.io). | |
multiline
|
boolean
|
A <textarea> that grows with its text (Compose TextFieldLineLimits.MultiLine).
|
|
isDisabled
|
boolean
|
||
isReadOnly
|
boolean
|
||
trailingIcon
|
ReactNode
|
||
suffix
|
ReactNode
|
||
value
|
string
|
||
defaultValue
|
string
|
||
placeholder
|
string
|
||
type
|
'text' | 'search' | 'url' | 'tel' | 'email' | 'password'
|
||
inputMode
|
'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'
|
||
name
|
string
|
||
autoComplete
|
string
|
||
autoFocus
|
boolean
|
||
id
|
string
|
||
className
|
string
|
||
style
|
CSSProperties
|
||
aria-label
|
string
|
||
aria-labelledby
|
string
|
||
aria-describedby
|
string
|
||
onChange
|
(value: string) => void
|
||
onFocus
|
(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void
|
||
onBlur
|
(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void
|
||
onKeyDown
|
(e: React.KeyboardEvent) => void
|
Custom elements
<m3e-text-field>
import '@kyralabs/m3e-elements/text-field.js';
<m3e-text-field label="Email" variant="outlined"> (COMPONENT_SPEC §26): renders the m3e-css text field inside itself (the host is display: contents) around a native <input> (or, with multiline, a <textarea>) that takes part in forms and fires the native input / change events. The label float, colors and outlined notch are CSS; the element adds what CSS cannot:
- the live character counter (
maxlength: "n/max", read as "Character count, n/max"); - a growing textarea where CSS
field-sizing: contentis missing (Compose's multi-line field grows with its lines); - the
valueproperty kept in step with what the user types.
Attributes: variant (filled, the default, or outlined), label, label-position (above; inside / cutout follow the variant), value, placeholder, type, name, autocomplete, inputmode, supporting-text, error-text (shown instead of the supporting text while invalid), invalid (aria-invalid: error colors), disabled, readonly, required (aria-required and the "*"), maxlength (with the counter), multiline, leading-icon, trailing-icon (Material Symbols names), prefix-text, suffix-text, counter-label.
| Attribute | Property | Type | Notes |
|---|---|---|---|
variant
|
variant
|
string
|
|
label
|
label
|
string
|
|
label-position
|
labelPosition
|
string
|
|
value
|
value
|
string
|
|
placeholder
|
placeholder
|
string
|
|
type
|
type
|
string
|
|
name
|
name
|
string
|
|
autocomplete
|
autocomplete
|
string
|
|
inputmode
|
inputmode
|
string
|
|
supporting-text
|
supportingText
|
string
|
|
error-text
|
errorText
|
string
|
|
invalid
|
invalid
|
boolean
|
|
disabled
|
disabled
|
boolean
|
|
readonly
|
readonly
|
boolean
|
|
required
|
required
|
boolean
|
|
maxlength
|
maxlength
|
number
|
|
multiline
|
multiline
|
boolean
|
|
leading-icon
|
leadingIcon
|
string
|
|
trailing-icon
|
trailingIcon
|
string
|
|
prefix-text
|
prefixText
|
string
|
|
suffix-text
|
suffixText
|
string
|
|
counter-label
|
counterLabel
|
string
|
CSS
text-field.css
@import '@kyralabs/m3e-css/text-field.css';
Text fields (COMPONENT_SPEC §26): filled (default) and outlined, single-line <input> or multi-line <textarea>, label, placeholder, leading / trailing icons, prefix / suffix, supporting text, character counter, error, disabled, read-only. No JavaScript needed: the label floats from CSS alone (below); <m3e-text-field> / m3e-react only add the live counter and the textarea growth where field-sizing is missing.
<div class="m3e-text-field m3e-text-field--outlined">
<div class="m3e-text-field__container">
<span class="m3e-text-field__leading" aria-hidden="true"><span class="m3e-icon">search</span></span>
<label class="m3e-text-field__label" for="email">Email</label>
<span class="m3e-text-field__prefix" id="email-prefix">@</span>
<input class="m3e-text-field__input" id="email" placeholder=" " maxlength="20"
aria-describedby="email-supporting email-counter" />
<span class="m3e-text-field__suffix">.com</span>
<span class="m3e-text-field__trailing"><button class="m3e-icon-button …" aria-label="Clear">…</button></span>
<fieldset class="m3e-text-field__outline" aria-hidden="true"> (outlined only)
<legend class="m3e-text-field__notch"><span>Email</span></legend> (the label text again)
</fieldset>
</div>
<div class="m3e-text-field__supporting">
<span class="m3e-text-field__supporting-text" id="email-supporting">Work address</span>
<span class="m3e-text-field__counter" id="email-counter">0/20</span>
</div>
</div>
Every part but the container and the input is optional. Modifiers: --outlined, --label-above (TextFieldLabelPosition.Above, owner decision M8 #25), --multiline (a <textarea>). States come from the input: :focus, :disabled, aria-invalid="true" (error colors), :required / aria-required="true" (a "*" after the label), placeholder text (below).
The zero-JavaScript label float: the label is minimized while the input is focused, not empty or autofilled, :has(.m3e-text-field__input:is(:focus, :not(:placeholder-shown), :autofill)). :placeholder-shown only matches an input with a placeholder, so give one (placeholder=" " when no placeholder text is wanted); without any the label simply stays minimized. The label progress is a registered number (--_m3e-text-field-progress) transitioned with the fast spatial spring's linear() easing, and the label's font-size / line-height / letter-spacing, position and the outlined notch are calc()s of it: Compose lerps the label TextStyle (bodyLarge → bodySmall) and its placement by one labelProgress (owner decision M8 #25). font-optical-sizing stays auto: the glyphs follow the animated size each frame like a static label of that size in Chromium, Firefox and WebKit (checked; WebKit's auto opsz differs from the px size everywhere, not only here).
The outlined notch: a fieldset draws the outline and its legend cuts it, so it follows the writing direction and forced colors with no script. The legend holds the label text again (aria-hidden, invisible) at the label's lerped size × progress, padded by the cutout padding while the progress is positive: Compose's outlineCutout (label width × progress + 2 × 4dp from the start padding − 4dp, nothing at progress ≤ 0).
164 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-filled-text-field-container-shape-end-end
|
var(--m3e-sys-shape-corner-extra-small-top-end-end) |
--m3e-comp-filled-text-field-container-shape-end-start
|
var(--m3e-sys-shape-corner-extra-small-top-end-start) |
--m3e-comp-filled-text-field-container-shape-start-end
|
var(--m3e-sys-shape-corner-extra-small-top-start-end) |
--m3e-comp-filled-text-field-container-shape-start-start
|
var(--m3e-sys-shape-corner-extra-small-top-start-start) |
--m3e-comp-filled-text-field-input-font
|
var(--m3e-sys-typescale-body-large-font) |
--m3e-comp-filled-text-field-input-line-height
|
var(--m3e-sys-typescale-body-large-line-height) |
--m3e-comp-filled-text-field-input-size
|
var(--m3e-sys-typescale-body-large-size) |
--m3e-comp-filled-text-field-input-tracking
|
var(--m3e-sys-typescale-body-large-tracking) |
--m3e-comp-filled-text-field-input-weight
|
var(--m3e-sys-typescale-body-large-weight) |
--m3e-comp-filled-text-field-leading-icon-size
|
24px |
--m3e-comp-filled-text-field-trailing-icon-size
|
24px |
--m3e-comp-minimum-interactive-component-size
|
48px |
--m3e-comp-outlined-text-field-container-shape
|
var(--m3e-sys-shape-corner-extra-small) |
--m3e-comp-outlined-text-field-input-font
|
var(--m3e-sys-typescale-body-large-font) |
--m3e-comp-outlined-text-field-input-line-height
|
var(--m3e-sys-typescale-body-large-line-height) |
--m3e-comp-outlined-text-field-input-size
|
var(--m3e-sys-typescale-body-large-size) |
--m3e-comp-outlined-text-field-input-tracking
|
var(--m3e-sys-typescale-body-large-tracking) |
--m3e-comp-outlined-text-field-input-weight
|
var(--m3e-sys-typescale-body-large-weight) |
--m3e-comp-outlined-text-field-leading-icon-size
|
24px |
--m3e-comp-outlined-text-field-trailing-icon-size
|
24px |
--m3e-comp-text-field-above-label-bottom-padding
|
4px |
--m3e-comp-text-field-above-label-horizontal-padding
|
4px |
--m3e-comp-text-field-affix-alpha-duration
|
var(--m3e-sys-motion-spring-fast-effects-duration) |
--m3e-comp-text-field-affix-alpha-easing
|
var(--m3e-sys-motion-spring-fast-effects-easing) |
--m3e-comp-text-field-affix-font
|
var(--m3e-sys-typescale-body-large-font) |
--m3e-comp-text-field-affix-line-height
|
var(--m3e-sys-typescale-body-large-line-height) |
--m3e-comp-text-field-affix-padding
|
2px |
--m3e-comp-text-field-affix-size
|
var(--m3e-sys-typescale-body-large-size) |
--m3e-comp-text-field-affix-tracking
|
var(--m3e-sys-typescale-body-large-tracking) |
--m3e-comp-text-field-affix-weight
|
var(--m3e-sys-typescale-body-large-weight) |
--m3e-comp-text-field-container-color-duration
|
var(--m3e-sys-motion-spring-fast-effects-duration) |
--m3e-comp-text-field-container-color-easing
|
var(--m3e-sys-motion-spring-fast-effects-easing) |
--m3e-comp-text-field-counter-space
|
16px |
--m3e-comp-text-field-cutout-label-space
|
calc(var(--m3e-sys-typescale-body-small-line-height) / 2) |
--m3e-comp-text-field-cutout-padding
|
4px |
--m3e-comp-text-field-filled-container-color
|
var(--m3e-sys-color-surface-container-highest) |
--m3e-comp-text-field-filled-container-color-disabled
|
var(--m3e-sys-color-surface-container-highest) |
--m3e-comp-text-field-filled-container-color-error
|
var(--m3e-sys-color-surface-container-highest) |
--m3e-comp-text-field-filled-container-color-focused
|
var(--m3e-sys-color-surface-container-highest) |
--m3e-comp-text-field-filled-cursor-color
|
var(--m3e-sys-color-primary) |
--m3e-comp-text-field-filled-cursor-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-filled-indicator-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-indicator-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-indicator-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-filled-indicator-color-focused
|
var(--m3e-sys-color-primary) |
--m3e-comp-text-field-filled-label-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-label-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-label-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-filled-label-color-focused
|
var(--m3e-sys-color-primary) |
--m3e-comp-text-field-filled-leading-icon-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-leading-icon-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-leading-icon-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-leading-icon-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-placeholder-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-placeholder-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-placeholder-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-placeholder-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-prefix-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-prefix-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface-variant) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-prefix-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-prefix-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-suffix-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-suffix-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface-variant) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-suffix-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-suffix-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-supporting-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-supporting-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-supporting-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-filled-supporting-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-text-color
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-text-field-filled-text-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-text-color-error
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-text-field-filled-text-color-focused
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-text-field-filled-trailing-icon-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-filled-trailing-icon-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-filled-trailing-icon-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-filled-trailing-icon-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-icon-padding
|
calc((48px - 24px) / 2) |
--m3e-comp-text-field-indicator-color-duration
|
var(--m3e-sys-motion-spring-fast-effects-duration) |
--m3e-comp-text-field-indicator-color-easing
|
var(--m3e-sys-motion-spring-fast-effects-easing) |
--m3e-comp-text-field-indicator-thickness
|
1px |
--m3e-comp-text-field-indicator-thickness-focused
|
2px |
--m3e-comp-text-field-indicator-width-duration
|
var(--m3e-sys-motion-spring-fast-spatial-duration) |
--m3e-comp-text-field-indicator-width-easing
|
var(--m3e-sys-motion-spring-fast-spatial-easing) |
--m3e-comp-text-field-label-color-duration
|
var(--m3e-sys-motion-spring-fast-effects-duration) |
--m3e-comp-text-field-label-color-easing
|
var(--m3e-sys-motion-spring-fast-effects-easing) |
--m3e-comp-text-field-label-duration
|
var(--m3e-sys-motion-spring-fast-spatial-duration) |
--m3e-comp-text-field-label-easing
|
var(--m3e-sys-motion-spring-fast-spatial-easing) |
--m3e-comp-text-field-label-expanded-font
|
var(--m3e-sys-typescale-body-large-font) |
--m3e-comp-text-field-label-expanded-line-height
|
var(--m3e-sys-typescale-body-large-line-height) |
--m3e-comp-text-field-label-expanded-size
|
var(--m3e-sys-typescale-body-large-size) |
--m3e-comp-text-field-label-expanded-tracking
|
var(--m3e-sys-typescale-body-large-tracking) |
--m3e-comp-text-field-label-expanded-weight
|
var(--m3e-sys-typescale-body-large-weight) |
--m3e-comp-text-field-label-minimized-line-height
|
var(--m3e-sys-typescale-body-small-line-height) |
--m3e-comp-text-field-label-minimized-size
|
var(--m3e-sys-typescale-body-small-size) |
--m3e-comp-text-field-label-minimized-tracking
|
var(--m3e-sys-typescale-body-small-tracking) |
--m3e-comp-text-field-label-vertical-padding
|
8px |
--m3e-comp-text-field-min-height
|
56px |
--m3e-comp-text-field-min-supporting-line-height
|
16px |
--m3e-comp-text-field-min-text-line-height
|
24px |
--m3e-comp-text-field-min-width
|
280px |
--m3e-comp-text-field-outline-thickness
|
1px |
--m3e-comp-text-field-outline-thickness-focused
|
2px |
--m3e-comp-text-field-outlined-container-color
|
transparent |
--m3e-comp-text-field-outlined-container-color-disabled
|
transparent |
--m3e-comp-text-field-outlined-container-color-error
|
transparent |
--m3e-comp-text-field-outlined-container-color-focused
|
transparent |
--m3e-comp-text-field-outlined-cursor-color
|
var(--m3e-sys-color-primary) |
--m3e-comp-text-field-outlined-cursor-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-outlined-indicator-color
|
var(--m3e-sys-color-outline) |
--m3e-comp-text-field-outlined-indicator-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.12 * 100%), transparent) |
--m3e-comp-text-field-outlined-indicator-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-outlined-indicator-color-focused
|
var(--m3e-sys-color-primary) |
--m3e-comp-text-field-outlined-label-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-label-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-label-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-outlined-label-color-focused
|
var(--m3e-sys-color-primary) |
--m3e-comp-text-field-outlined-leading-icon-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-leading-icon-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-leading-icon-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-leading-icon-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-min-height
|
56px |
--m3e-comp-text-field-outlined-min-width
|
280px |
--m3e-comp-text-field-outlined-placeholder-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-placeholder-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-placeholder-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-placeholder-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-prefix-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-prefix-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface-variant) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-prefix-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-prefix-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-suffix-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-suffix-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface-variant) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-suffix-color-error
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-suffix-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-supporting-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-supporting-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-supporting-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-outlined-supporting-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-text-color
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-text-field-outlined-text-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-text-color-error
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-text-field-outlined-text-color-focused
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-text-field-outlined-trailing-icon-color
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-outlined-trailing-icon-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-text-field-outlined-trailing-icon-color-error
|
var(--m3e-sys-color-error) |
--m3e-comp-text-field-outlined-trailing-icon-color-focused
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-text-field-padding
|
16px |
--m3e-comp-text-field-placeholder-font
|
var(--m3e-sys-typescale-body-large-font) |
--m3e-comp-text-field-placeholder-in-duration
|
var(--m3e-sys-motion-spring-slow-effects-duration) |
--m3e-comp-text-field-placeholder-in-easing
|
var(--m3e-sys-motion-spring-slow-effects-easing) |
--m3e-comp-text-field-placeholder-line-height
|
var(--m3e-sys-typescale-body-large-line-height) |
--m3e-comp-text-field-placeholder-out-duration
|
var(--m3e-sys-motion-spring-fast-effects-duration) |
--m3e-comp-text-field-placeholder-out-easing
|
var(--m3e-sys-motion-spring-fast-effects-easing) |
--m3e-comp-text-field-placeholder-size
|
var(--m3e-sys-typescale-body-large-size) |
--m3e-comp-text-field-placeholder-tracking
|
var(--m3e-sys-typescale-body-large-tracking) |
--m3e-comp-text-field-placeholder-weight
|
var(--m3e-sys-typescale-body-large-weight) |
--m3e-comp-text-field-selection-color
|
color-mix(in srgb, var(--m3e-sys-color-primary) calc(0.4 * 100%), transparent) |
--m3e-comp-text-field-supporting-font
|
var(--m3e-sys-typescale-body-small-font) |
--m3e-comp-text-field-supporting-line-height
|
var(--m3e-sys-typescale-body-small-line-height) |
--m3e-comp-text-field-supporting-size
|
var(--m3e-sys-typescale-body-small-size) |
--m3e-comp-text-field-supporting-top-padding
|
4px |
--m3e-comp-text-field-supporting-tracking
|
var(--m3e-sys-typescale-body-small-tracking) |
--m3e-comp-text-field-supporting-weight
|
var(--m3e-sys-typescale-body-small-weight) |
Accessibility
native <input> / <textarea> with a <label for> (React Aria also sets aria-labelledby), aria-describedby → the supporting (or error) text and the counter, aria-invalid="true" for errors (the web's form of Compose's error() semantics; the platform announces the invalid state, so Compose's default "Invalid input" string is not injected), aria-required="true" (React Aria's default validation behavior) with the "*" in the label's accessible name (m3.material.io). While invalid the error text replaces the supporting text (Compose's TextFieldWithErrorState) and has the "alert" role (m3.material.io). Read-only fields stay focusable. Focus shows as the 2dp primary indicator / outline and primary label (Compose); trailing icon buttons keep their own focus ring.
Every interactive part has a target of at least 48 × 48 px, a visible focus ring for
keyboard focus only, and works under forced-colors and
prefers-reduced-motion.
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §26 and the m3e-css sources.
FilledTextFieldTokens.kt / OutlinedTextFieldTokens.kt@19991f2c plus the TextField.kt / OutlinedTextField.kt / TextFieldDefaults / OutlinedTextFieldDefaults / internal/TextFieldImpl.kt values Compose renders (components/text-field-defaults.css; SOURCES register #43). Container: min 280 × 56dp (MinWidth × MinHeight; FilledTextFieldTokens has no height), filled ContainerShape (extra small, top corners) with a bottom indicator line 1dp, 2dp while focused and enabled (Unfocused/FocusedIndicatorThickness, drawn over the content, clipped to the shape; the token DB's 3dp focus thickness is not rendered), outlined extra small with a 1dp / 2dp outline drawn inside the bounds. Paddings (contentPaddingWithLabel only for an Inside label): filled with a label 16dp start / end and TextFieldWithLabelVerticalPadding 8dp top / bottom, the input under the 16dp minimized label line; otherwise 16dp all round (TextFieldPadding). Label (DecoratedLabel): lerp(bodyLarge, bodySmall, progress); filled from the vertical centre (top padding + half the minimized line height, 16dp) to the 8dp top padding, at the text start; outlined from 16dp (after the leading icon) to centred on the top outline (−height / 2) at the 16dp start padding, with half the minimized line height (8dp, topPaddingForLabelCutout) of space above the field and a notch of label width × progress + 2 × 4dp (OutlinedTextFieldInnerPadding) from 12dp, none at progress ≤ 0 (outlineCutout). Icons (24dp) in 48dp boxes, the text 16 − 12 = 4dp after them (textFieldHorizontalIconPadding). Prefix / suffix 2dp from the text (PrefixSuffixTextPadding), body large, shown while the label is minimized; placeholder body large, only while the label is minimized or absent. Supporting text body small, 4dp below (SupportingTopPadding), 16dp in, min 16dp line. Label Above (owner decision M8 #25): TextFieldLabelPosition.Above, always minimized, 4dp in and 4dp above the container, with the no-label paddings. Colors (defaultTextFieldColors / defaultOutlinedTextFieldColors; disabled, then error, then focused, then unfocused): focused indicator / outline, label and caret primary; error (aria-invalid="true") indicator, label, supporting text, trailing icon and caret error, text on-surface, leading icon on-surface-variant (Compose uses the Error*, not the ErrorFocus* tokens); disabled translucent copies (on-surface 0.38, outlined outline on-surface 0.12), and the filled disabled container keeps ContainerColor (not the token file's on-surface 4 %). Hover changes nothing (TextFieldColors has no hover; owner decision M8 #3) and there is no state layer (Compose draws none). Selection: primary at 0.4 (rememberTextSelectionColors). Not in Compose, from m3.material.io (owner rule): the character counter ("n/max", read as "Character count, n/max") at the end of the supporting line, 16dp after the supporting text; the required "*" after the label. The expressive tonal / rounded variant (tonalColors(), roundedShape) is out of scope (owner decision M8 #24).
text-field.css sources
(compose/material3 @ 19991f2c; values in text-field-defaults.css, lines in PARITY.md):
- TextField.kt / OutlinedTextField.kt: Inside (filled) and Cutout (outlined) label by default, MinWidth × MinHeight (280 × 56dp), the indicator line (bottom, 1dp → 2dp while focused and enabled, color FastEffects / width FastSpatial, snapping while disabled; drawn over the content, clipped to the shape), the Cutout space above an outlined field (bodySmall line height / 2) and the cutout itself (OutlinedTextFieldInnerPadding 4dp).
- TextFieldImpl.kt: InputPhase (focused / unfocused empty / not empty); label progress FastSpatial; label TextStyle lerp(bodyLarge, bodySmall); filled label from the vertical centre (top padding + half the minimized line height) to the 8dp top padding, the text below it; outlined label from the 16dp padding to −height / 2 (centred on the outline) and from after the leading icon to the 16dp start padding; icons in 48dp boxes, the text 16 − 12 = 4dp after them; prefix / suffix 2dp from the text, shown while the label is minimized (FastEffects); placeholder only while the label is minimized or absent (SlowEffects in, FastEffects out); supporting text 4dp below, 16dp in, bodySmall; Above: the label 4dp in and 4dp above the container, always minimized, with the no-label paddings (16dp).
- TextFieldDefaults.kt: contentPaddingWithLabel (16 / 8dp) only for an Inside label, else 16dp; defaultTextFieldColors / defaultOutlinedTextFieldColors: disabled, then error, then focused, then unfocused. No hover colors (TextFieldColors has none; owner decision M8 #3) and no state layer (Compose draws none on a text field). The filled disabled container keeps ContainerColor.
- MaterialTheme.kt: the selection highlight (primary at 0.4).
- Not Compose (m3.material.io Text fields, per the owner's rule): the character counter at the end of the supporting line, 16dp after the supporting text; the required "*".