Skip to content

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; }

API reference

React propFlutter parameterTypeDefaultMeaning
labellabelstringrequiredDrawn above the control and read by screen readers.
typekeyboardTypetext, search, email, numbertextThe kind of input.
multilinemaxLinesboolean / intfalseA textarea of three rows (web), or more lines (Flutter).
placeholderhintTextstringnoneShown while empty.
helphelperTextstringnoneOne line under the field.
errorerrorTextstringnoneReplaces the help text and turns the border red.
glyphiconGlyphName / IconDatanoneLeading icon.
sizecompactsm / boolean55 tallSmall is 34 tall.
tonetoneAstrTonedeep-spaceThe colour of the focus border.
value, onChangecontroller, onChangedstringnoneControlled text; the web also takes defaultValue.
disabledenabled: falsebooleanfalseNot 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.

RoleCSS tokenDart tokenApp widget today
Control height--spacing-f55, small --spacing-f34AstrSize.control, controlCompactnew
Radius--radius-f13, small --radius-f8AstrRadius.f13, f8new
Padding0 --spacing-f13, small f8AstrSpace.f13, f8new
Label--text-s-1, 500AstrType.sNeg1new
Text--text-s0 (16, no zoom on phones)AstrType.s0new
Help, error--text-s-2; error --color-oxygen-airglow-400AstrType.sNeg2; AstrColors.oxygenAirglow[400]new
Surface--sheen over --fill, border --hair-strongMag.m5, m7new
Focusborder the tone, ring tone at --mag-3 (--spacing-f3)tone.colornew
Disabledopacity --mag-2Mag.m2new