Skip to content

PlTextField

Single- or multi-line text input. The label, the helper text and the error message are part of the component rather than three elements you wire together yourself.

React
tsx
import { PlTextField } from 'plass-ui';

<PlTextField label="Email" type="email" description="We never share it." />;
dart
import 'package:plass_ui/plass_ui.dart';

PlTextField(
  controller: email,
  label: const Text('Email'),
  keyboardType: TextInputType.emailAddress,
  description: const Text('We never share it.'),
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the surface is made of. On a field solid is not a tinted pane but a well cut into the sheet — a field holds user data
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale. The same heights as PlButton, so a row's baseline holds
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. The glass is never dyed, so it reaches the edge, the focus ring and the caret
densityshared'default' | 'compact''default'Padding only — never the height, never the type scale
elevationshared0 | 1 | 2 | 30Drop shadow depth. A field is a well rather than a surface that floats, so the default is 0
labelReactNodeLabel above the control, wired to it by Base UI's Field
descriptionReactNodeHelper text below the control
errorReactNodeError message below the control. Its presence also turns the field invalid
invalidboolean!!errorForces the invalid state without a message, for when a form library owns validity
multilinebooleanfalseRenders a textarea instead of an input. Every other axis stays identical
rowsnumber3Visible rows in multiline mode
resize'none' | 'vertical' | 'horizontal' | 'both''vertical'Which way the user may drag a multiline control. Horizontal resizing breaks a form's column, so only the vertical axis is on
startIconReactNodeContent before the control. Sized in em, so it tracks the text
endIconReactNodeContent after the control
loadingbooleanfalseSpinner in place of endIcon, and the field is marked busy. Typing is still allowed
readOnlybooleanfalseThe value can be read and copied but not rewritten. Goes flat and loses most of its saturation
disabledbooleanfalseUnavailable. The page shows through the sheet, and it leaves the tab order
fullWidthbooleanfalseStretches to the width of the container
hotKeysRecord<string, () => void>Chords this control answers to, spelled the way PlHotKeys draws them — { 'Mod+Enter': save, Escape: cancel }. A chord that matches is **consumed**
classNames{ label?, control?, description?, error?: string }Classes on the parts a className does not reach. control is the part a reader acts on
PropTypeDefaultDescription
controllerTextEditingController?The text being edited — where Flutter keeps text. Left out, the field owns one of its own
onChangedValueChanged<String>?Called on every change
onSubmittedValueChanged<String>?Called when the field is submitted from the keyboard
variantsharedPlassVariantPlassVariant.solidWhat the surface is made of. On a field solid is not a tinted pane but a well cut into the sheet — a field holds user data
sizesharedPlassSizePlassSize.mdHeight and type scale. The same heights as PlButton, so a row's baseline holds
colorsharedPlassColorPlassColor.primarySemantic colour role. The glass is never dyed, so it reaches the edge, the focus ring and the caret
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint0Drop shadow depth. A field is a well rather than a surface that floats, so the default is 0
multilineboolfalseRenders a textarea instead of an input. Every other axis stays identical
rowsint3Visible rows in multiline mode
labelWidget?Label above the control, wired to it by Base UI's Field
descriptionWidget?Helper text below the control
errorWidget?Error message below the control. Its presence also turns the field invalid
invalidbool?!!errorForces the invalid state without a message, for when a form library owns validity
placeholderString?What is shown while the field is empty. A native attribute in React, so it has no row there
startIconWidget?Content before the control. Sized in em, so it tracks the text
endIconWidget?Content after the control
loadingboolfalseSpinner in place of endIcon, and the field is marked busy. Typing is still allowed
fullWidthboolfalseStretches to the width of the container
readOnlyboolfalseThe value can be read and copied but not rewritten. Goes flat and loses most of its saturation
disabledboolfalseUnavailable. The page shows through the sheet, and it leaves the tab order
obscureTextboolfalseHides what is typed, for a password
keyboardTypeTextInputType?Which keyboard to raise on a touch device
maxLengthint?How many characters the field will take. A formatter rather than a counter: nothing is drawn under the field
semanticLabelString?The name a screen reader announces, for a field with no visible label. A placeholder is not a name
hotKeysPlassHotKeys?Chords this control answers to, spelled the way PlHotKeys draws them — { 'Mod+Enter': save, Escape: cancel }. A chord that matches is **consumed**

Every native <input> attribute passes straight through, and in multiline mode every <textarea> attribute does. The exceptions are color and size, which are the shared axes above.

A className lands on the stack that holds the label, the control and the two lines under it. classNames reaches the four parts inside it: label, control (the box the text goes in) description and error.

The value lives in a TextEditingController, which is where Flutter keeps text. Left out, the field owns one, but a field whose value the app needs is a field the app should hand a controller to.

Under it is EditableText rather than a TextField: the latter is Material, and this package imports neither Material nor Cupertino. What Material adds on top (the decoration, the counter, the ripple) is what this component is instead of.

What the shared axes mean across the library is in prop conventions.

Examples

variant

glass is the default: a sheet with a hairline round it, drawn in --plass-border rather than the sheet's own white edge. A field usually sits on a card, and a white line round a near-white box on a white card has no visible shape. solid is the well, the glass at its most opaque with the library's one inset shadow in it, for a field that should read as recessed. ghost has no surface until the pointer is on it, for a field in a table cell.

A solid field is deliberately not a tinted pane. A gradient under a caret, a text selection and a placeholder is not legible, so the family shows up in the hairline, the ring and the caret instead.

React

size

The same ladder PlButton uses, xs 24px · sm 32px · md 40px · lg 48px · xl 56px, so a field and a button of the same size line up on one row.

React

label, description and error

All three are nodes, and all three are wired to the control by Base UI's Field: the label points at it, and both messages end up in its aria-describedby.

All three are widgets, and all three are part of the field's own semantics node, so a screen reader reads the label, the field and the message as one thing rather than as three.

There is no floating-label variant. A floating label needs a transform on the thing being typed into, and a label that moves under the caret is the one effect this library rules out on a control.

Validation

error carries a message and turns the field invalid, which re-points the whole slot family at danger, the hairline, the focus ring, the caret and the message all turn over together.

Two escape hatches, for when a form library owns the validity: invalid forces the state without a message, and invalid={false}invalid: false shows a message without it.

React

multiline

Every other axis is identical, and a one-row multiline field is exactly as tall as the single-line field of the same size. The vertical padding is derived from the height ladder, so density never touches it.

Renders a <textarea>. resize decides which way the user may drag it; horizontal resizing breaks a form's column, so only the vertical axis is on by default.

There is no resize. A textarea's drag handle is the browser's, and Flutter has no equivalent to offer, a field that has to change size is one the layout around it resizes.

React

startIcon and endIcon

Sized against the text rather than against the row. They sit on the shell rather than inside the control, and they answer its focus. An adornment goes from muted to the accent colour when the field is focused.

An adornment is centred on the control's first line, so it stays where it is when a multiline field grows.

React

loading · readOnly · disabled

propAppearanceTypingFocus
loadingA spinner takes the endIcon slotAllowedKept
readOnlyKeeps its colour, goes flat, drains saturationBlocked, but selectableKept
disabledThe page shows through the sheetBlockedLost

loading deliberately still allows typing: a field is usually loading because of what was typed into it.

React

hotKeys

A field that saves on Mod+Enter and clears on Escape is a form of keyboard affordance that has nowhere else to live. hotKeys is a map from a chord to what pressing it does, written in the same vocabulary PlHotKeys draws, so the cap printed beside the field and the key that actually works come from one string, and cannot drift apart.

Mod resolves per platform: one entry is ⌘ on a Mac and Ctrl everywhere else. Esc, Return, Cmd and Option fold onto the same keys their caps do.

A chord that matches is consumed. The handler runs and the key goes no further, so Escape bound here does not also close the dialog around the field, and Enter does not also submit the form. That is what binding a key means, and it is why these are chords rather than letters: { a: … } is a field that cannot type an a.

React

Controlled

value and onChange behave exactly as they do on a native input; onChange is typed to accept either element so the same handler works in multiline mode.

The controller is the value, and onChanged is told about every change. maxLength is a formatter rather than a counter: it stops the twenty-fifth character from arriving, and nothing is drawn under the field unless you draw it.

React

Accessibility

  • Renders a native <input>, or a <textarea> under multiline. Both take every attribute their element takes.
  • label is a real <label> pointing at the control. Without one, give the field an aria-label or a placeholder that is not the only name it has.
  • description and error both land in aria-describedby, so a screen reader reads the message with the field rather than after it.
  • error and invalid set aria-invalid.
  • The focus ring is drawn on the shell rather than on the control, so it traces the glass edge instead of a rectangle floating inside it. It appears on :focus-visible only.
  • Clicking the shell's padding puts the caret in the field, the way clicking inside a native input does.
  • Announced as a text field, and as read-only or unavailable when it is.
  • The label, the field, the description and the message are one semantics node, so a screen reader reads them together rather than one after another. Without a visible label, give the field a semanticLabel. A placeholder is not a name.
  • The focus ring is drawn on the shell rather than on the editor, so it traces the glass edge instead of a rectangle floating inside it. It appears only on what CSS calls :focus-visible.
  • Pressing the shell's padding puts the caret in the field, the way pressing inside a native input does.
  • A selection is made by dragging and has no handles to adjust afterwards: the drag handles a touch platform puts under one belong to Material and Cupertino, and this package imports neither.

Differences from the React build

ReactFlutterWhy
value / onChangecontroller / onChangedA TextEditingController is where Flutter keeps text, and it is what a caller already has.
type="email"keyboardTypeFlutter's way of saying which keyboard to raise.
resizeA textarea's drag handle is the browser's, and there is no equivalent to offer.
a <textarea> under multilinethe same widget, tallerThere is one editor either way, so switching to multiline genuinely changes nothing but the height.
aria-describedby wiringone merged semantics nodeThe same result by a different route.
selection handles on touchThey belong to Material and Cupertino, which this package does not import.
className, style, native attributesThere is no class list and no style attribute to pass through.

Released under the MIT License