A single-line text field with label, hint, error line and an optional length count in one element.

Basic

Pass a label, and a hint if the label cannot say it all. The placeholder shows an example and never replaces the label.

Example

Changed
Shown in the header and in every invite.
Follows the page theme and corners
'use client';

import { TextField } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function TextFieldBasic(): ReactElement {
  return (
    <TextField
      label="Workspace name"
      description="Shown in the header and in every invite."
      placeholder="Northwind Studio"
      style={{ width: '100%', maxWidth: '24rem' }}
    />
  );
}

States

The edge carries the state.

  • Changed: a light signal orange edge and a clay chip beside the label.
  • Invalid: the error edge and an error line. Invalid outranks changed, so the chip hides.
  • Disabled: the field is recessed.
  • Read-only: looks like an enabled field and still submits.

Example

Changed
Enter a name with at least two letters.
Changed
Changed
Follows the page theme and corners
'use client';

import { TextField } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function TextFieldStates(): ReactElement {
  return (
    <div className="stack" style={{ width: '100%', maxWidth: '24rem' }}>
      <TextField label="Display name" defaultValue="Northwind Studio East" isChanged />
      <TextField
        label="Short name"
        defaultValue="!"
        isChanged
        isInvalid
        errorMessage="Enter a name with at least two letters."
      />
      <TextField label="Workspace ID" defaultValue="northwind-studio" isDisabled />
      <TextField label="Plan" defaultValue="Team plan" isReadOnly />
    </div>
  );
}

Validation

Set isRequired, a type, or a validate function, and the browser's checks run on submit. The error line shows the message, or one you build from the validation result. The first failed submit moves focus to the field.

Example

Changed
We send the invite here.

Follows the page theme and corners
'use client';

import { Button, Text, TextField } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';

export default function TextFieldValidation(): ReactElement {
  const [email, setEmail] = useState('');
  const [sentTo, setSentTo] = useState('');
  return (
    <form
      className="stack"
      style={{ width: '100%', maxWidth: '24rem' }}
      onSubmit={(event) => {
        event.preventDefault();
        setSentTo(email);
      }}
    >
      <TextField
        label="Work email"
        description="We send the invite here."
        name="email"
        type="email"
        isRequired
        value={email}
        onChange={setEmail}
        validate={(value) =>
          value === '' || value.endsWith('@northwind.studio') ? null : 'Use an address at northwind.studio.'
        }
        errorMessage={({ validationDetails, validationErrors }) =>
          validationDetails.valueMissing ? 'Enter your work email.' : validationErrors.join(' ')
        }
      />
      <div className="row">
        <Button type="submit">Send invite</Button>
      </div>
      <Text tone="muted" role="status">
        {sentTo ? `Invite sent to ${sentTo}.` : ''}
      </Text>
    </form>
  );
}

Length count

Pass budget for about how long the value should be. The count appears from 70% of the budget and never blocks typing, so it can pass the budget. Field parts shows it in use.

  • Length is in UTF-16 code units, as JavaScript and maxLength measure it. An emoji, or an accented letter built from two code points, can count as two.
  • maxLength is a hard stop the count does not read. Set budget at or below it.

To word the count another way, such as in another language, pass counter with a label function. It receives the count, the budget and whether the count is past it, and returns the text. counter.showFrom sets the share of the budget where the count appears.

<TextField
  label="Kurzbeschreibung"
  budget={60}
  counter={{ label: ({ count, budget }) => `${count} von etwa ${budget}` }}
/>

The count is announced each time it changes. To announce it less often, build the field from its parts and pass FieldCounter a count that settles after a pause, as Field parts describes.

Usage

Every value works controlled and uncontrolled. A field with a name submits in a plain form. onChange receives the string.

import { useState } from 'react';
import { TextField } from '@snapshot-ui/react';

export function ShortDescription({ saved }: { saved: string }) {
  const [value, setValue] = useState(saved);
  return (
    <TextField
      label="Short description"
      description="One sentence, for search results."
      name="description"
      value={value}
      onChange={setValue}
      isChanged={value !== saved}
      budget={60}
    />
  );
}

The types require a name: a label, an aria-label when no label is shown, or an aria-labelledby that points at a label shown elsewhere.

For a custom layout, use the parts inside React Aria's own field: Label, Input, Description and FieldError. Field parts has the example. For text of several lines, use Text area, which takes the same props.

Props

Every React Aria TextField prop is accepted. These are the ones Snapshot UI adds or shapes.

budget
number

About how long the value should be. Shows a length count from 70% of the budget. It never blocks typing. Length is in UTF-16 code units, as maxLength counts. Set budget at or below maxLength.

changedLabel
string

The text of the Changed chip.

Default: 'Changed'

counter
Pick<FieldCounterProps, "label" | "showFrom">

Options for the budget count. label returns its text from the count, the budget and whether the count is past it. showFrom is the share of the budget where the count appears, from 0 to 1 (0.7 by default).

defaultValue
string

The default value (uncontrolled).

description
ReactNode

A hint under the control. It becomes the control's accessible description.

errorMessage
((validation: ValidationResult) => ReactNode) | ReactNode

The error line, shown only while the field is invalid. A function receives the validation result. Without it the field shows its own validation messages.

isChanged
boolean

Whether the value differs from the saved one. Shows the clay Changed chip, which a screen reader reads with the field, and the light signal orange border. An invalid field shows its error instead.

isDisabled
boolean

Whether the input is disabled.

isInvalid
boolean

Whether the value is invalid.

isReadOnly
boolean

Whether the input can be selected but not changed by the user.

isRequired
boolean

Whether user input is required on the input before form submission.

label
ReactNode

The field's name, shown above the control. Required unless the field has an aria-label or an aria-labelledby.

name
string

The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname).

onChange
((value: string) => void)

Handler that is called when the value changes.

placeholder
string

Example text shown while the control is empty. It never replaces the label.

type
"email" | "password" | "search" | "tel" | "text" | "url" | (string & {})

The type of input to render. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).

Default: 'text'

validate
((value: string) => ValidationError | true | null | undefined)

A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if validationBehavior="native". For realtime validation, use the isInvalid prop instead.

value
string

The current value (controlled).

Accessibility

A native input, labelled by the label and described by the hint and the error line. While the field is changed and valid, a screen reader reads the Changed chip after the hint. An invalid field reads its hint, then its error line.

TabMoves focus into the field. The edge firms to 1.5px ink and the 2px ring sits outside it.
EnterSubmits the form the field is in.

The placeholder is taupe and quiet by design, so it never carries anything the label does not. The count is a polite status region.

A field with no name does not compile. TypeScript's message names only aria-labelledby or says the props are not assignable. Either one means a missing accessible name.

Design rules

What keeps fields in the system.

Name the field in the label, in sentence case. Use the hint for what the label cannot say.

Show the chip and the changed edge together, and only while the value is unsaved.

Say what is wrong and how to fix it in the error line.

Never use the placeholder as the label.

Never block typing at the budget. The count advises.

Never leave the chip beside an error. Invalid outranks changed.

esc

↑ ↓ to move↵ to open