Components · Forms

Text field

A single-line input with the label baked into the container. One component covers text, email, URL, search, password, number, and tel — plus a paired TextArea for multi-line values. The label sits at the top of the box, the value sits below; nothing animates.

Documentedby Derek Fidler

Use a memorable, unique name customers will recognise.

Default + label + help

Overview

The Text field is the canonical single-line input. Its signature move is the always-floating label: the field name sits at the top of the container in small grey text; the value sits below it in body type. No animation, no Material-style transition — the label is just always there, in its place. Use it everywhere a form asks the user for one line.

One component, six native types

TextField takes a type prop (text / email / url / search / password / tel / number). Four preconfigured wrappers — EmailField, UrlField, PasswordField, NumberField— set the right type, autocomplete, inputMode, and behaviour up-front so you don't have to remember the combination at every call site. For free-text or filtered search use Search — it's a dedicated combobox, not a TextField variant.

States

Six visible states. Hover, active, and focus are pseudo-class driven — they come for free on the real components. Held still here for review.

Empty

Default resting state. Label visible at top, value area blank.

Filled

User has typed. Value sits below the floating label.

Use a memorable, unique name.

Hover

Pointer over the container; border darkens to border-outline-hover. Hover the field to see it on this page.

Tab into the field with the keyboard to see the ring.

Focus

Keyboard or click focus. Border switches to border-foreground; keyboard adds the ring-ring ring. Click into the field below to confirm.

Set during onboarding; not editable here.

Disabled

Inert. Container fills bg-disabled; text drops to text-disabled-foreground.

A store with this name already exists.

Invalid

Container fills bg-destructive, border switches to border-destructive-border, error message replaces help text.

Variants

One component, five type-specific wrappers. Reach for the wrapper that matches the data — the wrapper sets autoComplete, inputMode, and (where relevant) the affordance (e.g. password reveal eye).

Use a memorable name customers will recognise.

type="text"

The default. Plain single-line text.

We'll send a one-time link to sign in.

<EmailField>

type=email + autoComplete=email. Mobile keyboards switch to the @ layout.

Public URL to your store, blog, or landing page.

<UrlField>

type=url + inputMode=url + autoComplete=url. The browser validates protocol on submit.

At least 12 characters.

<PasswordField>

type=password + an eye toggle that reveals the value. Toggle is keyboard-accessible.

Hour in 24-hour clock.

<NumberField>

type=number + tabular-nums. Mobile keyboards show the numeric pad.

Money is a separate page

MoneyField deserves its own treatment — European format, slashed zero, currency suffix, and the hero metric size. See Money field for the rules.

Compositions

Prefix, suffix, help text, error message — the same four slots handle every common composition. Reach for them via props rather than nesting buttons inside the input's children.

Prefix

Use prefix for ≤3-character adornments — currency codes, country dialling codes, sigils. Anything longer competes with the value and belongs in a separate field.

Copy

Set `copy` (or use <CopyField>) to lift a value out of the form. The surface drops to disabled tokens; click anywhere to copy. The icon flips to a check for ~1.5 s on success.

Used to verify push events from Flatpay reach you intact.

Help text

Belongs below the field. Sets context the label can't carry alone.

Couldn't find this repo. Check the owner/name spelling.

Error message

Set the error prop to flip the surface red, set aria-invalid, and replace the help text.

Multiline

For values that need more than one line — a description, a cancellation reason, a long note — reach for TextArea. Same visual language as TextField; rendered as a real <textarea> element with native vertical resize.

Visible to customers on the storefront.

<TextArea>

Multi-line text. Same visual language as TextField — floating label, 8 px radius. Vertical resize is on by default.

Locked height

Set resize="none" when the surrounding layout shouldn't reflow.

Anatomy

Five named parts. Only the box is required — label, prefix / suffix, description, and error are opt-in.

Use a memorable name.

  1. Container

    Rounded surface (8 px). Carries the border, focus ring, and disabled / invalid background.

  2. Label

    Inter Tight 12 / Regular at 60% foreground. Sentence case. Always at the top of the container.

  3. Value

    Inter Tight 16 / Regular. The actual text the user types. Placeholder colour drops to 40%.

  4. Suffix slot

    Optional. Currency code, copy button, eye toggle, search clear button. Inherits text colour from the container — set it via the suffix prop.

  5. Help / error

    Sits below the container. Help text in 60% foreground; error switches to text-destructive-foreground with an alert glyph.

Behavior

  • Click anywhere in the container focuses the input. The whole tile is wrapped in a <label> tied to the input via htmlFor. Don't intercept clicks on the label area.
  • Border darkens on focus, ring shows on keyboard only. focus-within switches the border to border-foreground for any focus; has-[:focus-visible] adds the ring-ring ring only when the user arrived via keyboard.
  • Help text and errors are linked via aria-describedby. The component generates the IDs, attaches them, and switches from description to errorwhen the error prop is set. Don't hand-roll a separate paragraph.
  • Password reveal is opt-in. PasswordField ships with the eye toggle on by default; pass showPasswordToggle={false} for fields where revealing the value would be unsafe (keypad entry, shared screens).
  • Search clear button only appears when there's content. No empty × glyph cluttering the resting state. The button focuses the field after clearing.

Accessibility

  • Native semantics. Renders an <input> inside a <label> via htmlFor. No custom ARIA needed for the basic field.
  • Description and error are linked via aria-describedby. When the error prop is set, the input also gets aria-invalid="true".
  • Use the right type. Mobile keyboards and assistive tech adapt to type="email", type="url", and inputMode="decimal". Reach for the typed wrappers — they pre-set everything.
  • Autocomplete tokens. The wrappers set sensible defaults (autoComplete="email", autoComplete="current-password"). Override per-field when the form context calls for it (e.g. new-password on a registration form).
  • Don't hide the label. The always-floating label is the design — never set it to sr-only or rely on the placeholder for context. Placeholders disappear on type; labels stay.

Code

Import from @flatpay-dk/ui. Renders a real native input — every standard input attribute (name, required, pattern,minLength) passes through.

tsx

import {
  TextField,
  EmailField,
  UrlField,
  PasswordField,
  NumberField,
  TextArea,
} from "@flatpay-dk/ui";

// Default — text input
<TextField
  label="Store name"
  description="Use a memorable name customers will recognise."
  name="store_name"
  required
/>

// Email — pre-set type, autoComplete, mobile keyboard
<EmailField
  label="Email address"
  placeholder="you@flatpay.com"
  description="We'll send a one-time link to sign in."
/>

// Password — eye toggle on by default
<PasswordField label="Password" autoComplete="new-password" />

// URL — type=url + inputMode=url
<UrlField label="Website" placeholder="https://example.com" />

// Number — tabular-nums + decimal keyboard
<NumberField label="Daily settlement cutoff" min={0} max={23} />

// Prefix slot — ≤3-character adornments only
<TextField
  label="Phone number"
  type="tel"
  prefix={<span className="font-mono text-[13px]">+45</span>}
  defaultValue="20 12 34 56"
/>

// Multiline
<TextArea
  label="Description"
  rows={4}
  description="Visible to customers on the storefront."
/>

// Invalid + error message
<TextField
  label="Repository"
  error="Couldn't find this repo. Check the owner/name spelling."
/>

Best practices

A handful of habits keep forms predictable across the product.

Do

Use the typed wrapper that matches your data — it sets autoComplete, inputMode, and the right keyboard for free.

Don't

Don't hide the label. The always-floating label is the design — placeholders disappear on type.

Couldn't find this repo.

Do

Set the error prop instead of a sibling paragraph. The component handles aria-invalid and the alert glyph.

Couldn't find this repo.

Don't

Don't lean on red text below an unstyled field. The user shouldn't have to read the message to know which field failed.

Do

Use prefix for short adornments — country code, currency, sigil. Three characters or fewer.

Don't

Don't pack co-text into the prefix. A protocol or domain belongs in a separate field — anything longer than 3 characters fights the value.

Props

PropTypeDefaultDescription
type"text" | "email" | "url" | "search" | "password" | "tel" | "number""text"Native input type. Reach for the typed wrappers (EmailField, etc.) to set this and the matching defaults at the same time.
labelReactNodeFloating label rendered at the top of the container. Sentence case. Always visible.
descriptionReactNodeHelp text below the container. Hidden when the error prop is set.
errorReactNodeWhen set, flips the surface to error tones, sets aria-invalid, and replaces the help text with the alert message.
prefixReactNode≤3-character adornment rendered before the value — icon, currency code, country dialling code, sigil. Text content longer than 3 characters logs a dev-mode warning. Inherits text colour.
suffixReactNodeElement rendered after the input value (currency code, unit, eye toggle). Ignored when `copy` is set — the copy button takes over the trailing slot.
size"md" | "hero""md"Vertical density and value typography. Hero is reserved for big metric inputs (used by MoneyField for funding-amount surfaces).
showPasswordTogglebooleanfalse (true on PasswordField)When type=password, render an eye toggle that switches the visible value between dots and plaintext.
showClearButtonbooleanfalseRender an × button when the field has content. Clicking it clears the value and refocuses the field.
copybooleanfalseLift the value out of the form. Forces readOnly, takes over the trailing slot with a 24 px copy button, and applies the readonly-copy surface (disabled bg + border tokens, but value text in primary). Clicking anywhere on the field copies the value; the icon flips to a check for ~1.5 s. `<CopyField>` is the ergonomic shortcut. `disabled` wins over `copy`. Ignored at size=hero.
...restInputHTMLAttributes<HTMLInputElement>All standard input attributes (name, required, pattern, autoComplete, etc.) pass through to the underlying input.