Design system/Components
Text field
TextField is the field in every Flowra form: a label that sits inside the field and rises as you type, and a line beneath it that ripples like water on focus.
In codecomponents/text-field.tsx
Live example
Type, focus, and leave. When the field is filled or focused, the label moves to the top edge and stays there, so someone coming back mid-form still knows what each box was for.
The invitation goes to this address.
At least 8 characters
Anatomy
- Icon16px at the start of the field; says what the field takes.
- Floating labelInside the field when it's empty; on the top edge when it's filled or focused.
- valuesThe text the user types.
- Trailing controltrailing for one action (like "Apply"), controls for a few quiet buttons, or the password eye.
- UnderlineGray by default, orange on focus, red on error.
- HelpOne line under the field: the rule or the reason for an error.
States
An error sits only on the field it's about, and its reason is written under that same field. A wrong password is about the username and password pair, so both get marked; an invalid username is only about itself.
This email address isn't complete.
Variants
- floating: the label inside the field; the default for all forms.
- compact: 44px instead of 58, for toolbars, the sidebar, and search.
- multiline: long text wraps but stays one value; Enter submits the form. For titles, not notes.
- TextAreaField: multi-line notes; Enter adds a new line, and it grows with the text up to 320px.
- type="password": the eye button for showing the password is added automatically.
Rules
Do
Short, noun labels: "Email", "Amount".
Don't
A full sentence as the label; it gets cut off when it moves up.
Do
A button that belongs to the field's value should be trailing.
Don't
A button outside and next to the field; a long value scrolls under nothing, and the two pieces don't read as one control.
Do
Write the reason for the error in hint.
Don't
Only turning the line red; color is never the only carrier of meaning.
Accessibility
- The label is connected to the field with htmlFor, even when it floats.
- On touch screens, the field font is 16px so iOS doesn't zoom the page.
- The underline ripple doesn't run with reduced motion.
Code
<TextField
floating
icon={paths.mail}
label={tr("ایمیل")}
value={email}
onChange={(e) => setEmail(e.target.value)}
error={!!emailError}
hint={emailError}
/>
<TextField floating compact type="search" icon={paths.search}
label={tr("جستوجو")} value={q} onChange={(e) => setQ(e.target.value)} />Sizes
58px · 16px / 500Touch 62px · 17px44px · 14pxTouch 48px · 16px40px · 13.5px · radius 10pxTouch 48px · 16px · 12px; phone 46px · 16px16px · 13px from the start edge42pxTrailing control side 48px14px / 500 · 42px from the startWithout icon 14px11px / 600 · padding 1px 7px · radius 6pxcompact: 10.5pxstroke 1.5 · bottom -4px40 × 40px · 5px from the edgecompact 34px30px · 12px / 700Text leaves 86px11.5px · margin-top 5pxmin 58px · up to 320pxline-height 1.8States, precisely
The floating field has no separate disabled state; it's just the browser's disabled input. If a field is truly locked, plain text is better than a field.
Line --border-strong · label --text-tertiaryLine --accent · icon --accent and scale(1.35) · label --accent-textLabel on top · --text-secondarydata-error · line, icon and label --redOne wave on error1350ms · ease 1 − (1 − t)^1.7With focus floatingall 0.18s cubic-bezier(0.4, 0, 0.2, 1)--text on --mutedPress scale(0.88)--text-tertiary on --mutedAccessibility, precisely
- The label (floating or plain) is connected with htmlFor to an id generated with useId; screen readers read the label as the field's name.
- hint isn't connected to the field with aria-describedby, and error doesn't set aria-invalid on its own; if you need them, pass aria-invalid and aria-describedby yourself, since ...rest is spread onto the input.
- The password eye button has the aria-label “Show password” or “Hide password” and tabIndex=-1; you can't reach it with the keyboard.
- The underline is aria-hidden; it's decoration.
- The raised label is --text-secondary on the card: 5.14 to 1 in light and 5.8 in dark, AA in both. On focus, the label becomes --accent-text (5.3 and 6.19). The resting label is --text-tertiary (2.16 and 2.99) and doesn't meet AA, so it's not the place for essential guidance.
- The underline at rest is 1.32 to 1; the field's boundary is shown by the icon and label, not the line alone.
Right to left
The floating field is laid out with data-dir, not the page's dir. In right-to-left the icon gets right: 13px and the trailing control left: 5px; in left-to-right it's the reverse. So an email field with dir="ltr" in the middle of a Persian form still has its icon and label on the correct side. The controls' space is measured with ResizeObserver, and padding is added on the side where the field's text ends.
Do
Email, address, and code with dir="ltr".
Don't
A right-aligned email; the @ and the dot appear out of place.
Do
A long title with multiline; Enter submits.
Don't
TextAreaField for a title; Enter creates a new line and a two-line title gets saved.
Do
For numeric input, inputMode="numeric" and normalizeDigits.
Don't
type="number"; it rejects Persian digits and changes with the mouse wheel.
Related pages