Components
Text field
A text field on the glass material: a label above, the control with an optional glyph, and a line of help or error text below. Focus draws its tone on the border.
Preview
Variants
Pick a variant with the controls. The preview and both code blocks follow.
Type
Size
State
Focus tone
Icon
Help text
Variant.tsx
<AppTextField label="Location" placeholder="Where are you observing from?" glyph="pin" help="Shown under the field" />Implementation
The React tab is this site's own component, the same source that draws the preview, styled by the CSS below it. The Flutter tab is the Dart a screen builds it with, from the app's tokens.
Usage
<AppTextField label="Location" placeholder="Where are you observing from?" glyph="pin" /><AppTextField label="Email" type="email" error="This value is not valid" /><AppTextField label="Notes" multiline />app-text-field.tsx · AppTextField
/** * A text field on the glass material: a label above, the control with an optional leading glyph, and a line of help * or error text below. Focus draws the tone on the border. `multiline` makes it a textarea. Pass `value` and * `onChange` to control it, or `defaultValue` to let it keep its own text. */export function AppTextField({ label, type = "text", placeholder, help, error, glyph, size, tone = "deep-space", multiline, value, defaultValue, onChange, disabled,}: { label: string; type?: "text" | "search" | "email" | "number"; placeholder?: string; help?: string; error?: string; glyph?: GlyphName; size?: "sm"; tone?: Tone; multiline?: boolean; value?: string; defaultValue?: string; onChange?: (value: string) => void; disabled?: boolean;}) { const state = value === undefined ? { defaultValue } : { value }; const common = { placeholder, disabled, "aria-invalid": error ? true : undefined, onChange: onChange ? (e: { target: { value: string } }) => onChange(e.target.value) : undefined, ...state, }; const cls = ["app-ui app-field", size && `is-${size}`, error && "is-error"].filter(Boolean).join(" "); return ( <label className={cls} style={{ ["--tone" as string]: `var(--color-${TONE_STOP[tone]})` }}> <span className="app-label-md">{label}</span> <span className="app-field-control"> {glyph && <Glyph name={glyph} />} {multiline ? <textarea rows={3} {...common} /> : <input type={type} {...common} />} </span> {(error || help) && <span className="app-label-sm app-field-help">{error ?? help}</span>} </label> );}app-ui.css
.app-field { --tone: var(--color-deep-space-200); display: grid; gap: var(--spacing-f5); min-width: 0; width: min(100%, var(--container-f377));}.app-field-control { display: flex; align-items: center; gap: var(--spacing-f8); min-height: var(--spacing-f55); padding: 0 var(--spacing-f13); border-radius: var(--radius-f13); border: var(--spacing-f1) solid var(--hair-strong); background: var(--sheen), var(--fill); box-shadow: var(--highlight); box-sizing: border-box; transition: border-color 0.3s, box-shadow 0.3s;}.app-field.is-sm .app-field-control { min-height: var(--spacing-f34); padding: 0 var(--spacing-f8); border-radius: var(--radius-f8); }.app-field-control:focus-within { border-color: var(--tone); box-shadow: var(--highlight), 0 0 0 var(--spacing-f3) color-mix(in srgb, var(--tone) var(--mag-3), transparent);}.app-field.is-error .app-field-control { border-color: var(--c-error); }.app-field svg { flex: none; width: var(--spacing-f21); height: var(--spacing-f21); fill: var(--c-muted); }.app-field-help { margin: 0; }Usage
AstrGlassTextField(label: 'Location',hintText: 'Where are you observing from?',icon: Icons.place,helperText: 'Shown under the field',onChanged: (String text) {},)astr_glass.dart · AstrGlassTextField
/// A text field on the glass material: a label above, the control with an/// optional leading icon, and a line of help or error text below. The control/// is [AstrSize.control] tall (55), or [AstrSize.controlCompact] (34); focus/// draws [tone] on the border.class AstrGlassTextField extends StatelessWidget { const AstrGlassTextField({ super.key, required this.label, this.controller, this.hintText, this.helperText, this.errorText, this.icon, this.tone = AstrTone.deepSpace, this.compact = false, this.maxLines = 1, this.enabled = true, this.onChanged, this.keyboardType, }); final String label; final TextEditingController? controller; final String? hintText; final String? helperText; final String? errorText; final IconData? icon; final AstrTone tone; final bool compact; /// 1 for a single line, more for a multiline field. final int maxLines; final bool enabled; final ValueChanged<String>? onChanged; final TextInputType? keyboardType; @override Widget build(BuildContext context) { final Color text = AstrColors.spaceGrey[50]!; final Color muted = AstrColors.spaceGrey[300]!; final Color error = AstrColors.oxygenAirglow[400]!; final double radius = compact ? AstrRadius.f8 : AstrRadius.f13; OutlineInputBorder border(Color color) => OutlineInputBorder( borderRadius: BorderRadius.circular(radius), borderSide: BorderSide(color: color), ); return Opacity( opacity: enabled ? 1 : Mag.m2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( label, style: TextStyle(fontFamily: 'Satoshi', fontSize: AstrType.sNeg1, fontWeight: FontWeight.w500, color: text), ), const SizedBox(height: AstrSpace.f5), TextField( controller: controller, enabled: enabled, onChanged: onChanged, keyboardType: keyboardType, maxLines: maxLines, style: TextStyle(fontSize: compact ? AstrType.sNeg1 : AstrType.s0, color: text), cursorColor: tone.color, decoration: InputDecoration( hintText: hintText, hintStyle: TextStyle(color: muted.withValues(alpha: Mag.m1)), helperText: helperText, helperStyle: TextStyle(fontSize: AstrType.sNeg2, color: muted), errorText: errorText, errorStyle: TextStyle(fontSize: AstrType.sNeg2, color: error), prefixIcon: icon == null ? null : Icon(icon, size: compact ? AstrSpace.f13 : AstrSize.icon, color: muted), filled: true, fillColor: text.withValues(alpha: Mag.m7), constraints: BoxConstraints(minHeight: compact ? AstrSize.controlCompact : AstrSize.control), contentPadding: EdgeInsets.symmetric(horizontal: compact ? AstrSpace.f8 : AstrSpace.f13, vertical: AstrSpace.f13), enabledBorder: border(text.withValues(alpha: Mag.m5)), focusedBorder: border(tone.color), errorBorder: border(error), focusedErrorBorder: border(error), disabledBorder: border(text.withValues(alpha: Mag.m5)), ), ), ], ), ); }}API reference
| React prop | Flutter parameter | Type | Default | Meaning |
|---|---|---|---|---|
| label | label | string | required | Drawn above the control and read by screen readers. |
| type | keyboardType | text, search, email, number | text | The kind of input. |
| multiline | maxLines | boolean / int | false | A textarea of three rows (web), or more lines (Flutter). |
| placeholder | hintText | string | none | Shown while empty. |
| help | helperText | string | none | One line under the field. |
| error | errorText | string | none | Replaces the help text and turns the border red. |
| glyph | icon | GlyphName / IconData | none | Leading icon. |
| size | compact | sm / boolean | 55 tall | Small is 34 tall. |
| tone | tone | AstrTone | deep-space | The colour of the focus border. |
| value, onChange | controller, onChanged | string | none | Controlled text; the web also takes defaultValue. |
| disabled | enabled: false | boolean | false | Not editable. |
Design tokens
Every value on this component is a system token. The last column is what the app widget uses today, so the gap to close is visible.
| Role | CSS token | Dart token | App widget today |
|---|---|---|---|
| Control height | --spacing-f55, small --spacing-f34 | AstrSize.control, controlCompact | new |
| Radius | --radius-f13, small --radius-f8 | AstrRadius.f13, f8 | new |
| Padding | 0 --spacing-f13, small f8 | AstrSpace.f13, f8 | new |
| Label | --text-s-1, 500 | AstrType.sNeg1 | new |
| Text | --text-s0 (16, no zoom on phones) | AstrType.s0 | new |
| Help, error | --text-s-2; error --color-oxygen-airglow-400 | AstrType.sNeg2; AstrColors.oxygenAirglow[400] | new |
| Surface | --sheen over --fill, border --hair-strong | Mag.m5, m7 | new |
| Focus | border the tone, ring tone at --mag-3 (--spacing-f3) | tone.color | new |
| Disabled | opacity --mag-2 | Mag.m2 | new |