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
Example
'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
Example
'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
Example
'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
maxLengthmeasure it. An emoji, or an accented letter built from two code points, can count as two. maxLengthis a hard stop the count does not read. Setbudgetat 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
|
|---|---|
changedLabel string | The text of the Changed chip. Default: 'Changed' |
counter Pick<FieldCounterProps, "label" | "showFrom"> | Options for the |
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 |
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 |
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.
| Tab | Moves focus into the field. The edge firms to 1.5px ink and the 2px ring sits outside it. |
|---|---|
| Enter | Submits 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.