The label, hint, error line, length count and Changed chip that make up every field.
Parts
A field is a grid of parts, 8px apart. Label names the control, Description is the hint under it, and the clay chip marks an unsaved value. Type in the box to change it.
Example
Example
'use client';
import { TextField } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';
const SAVED = 'Northwind Studio';
export default function FieldParts(): ReactElement {
const [value, setValue] = useState(SAVED);
return (
<TextField
label="Workspace name"
description="Shown in the header and in every invite. Edit it to see the chip."
value={value}
onChange={setValue}
isChanged={value !== SAVED}
style={{ width: '100%', maxWidth: '24rem' }}
/>
);
}States
Two attributes on the field's root carry its state.
data-changed: shows the Changed chip.data-invalid: shows the error line and the control's error border, and hides the chip. Invalid outranks changed.
The first field below is changed. The second is changed and invalid.
Example
Example
'use client';
import { TextField } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function FieldStates(): ReactElement {
return (
<div className="stack" style={{ width: '100%', maxWidth: '24rem' }}>
<TextField
label="Display name"
description="Shown in the header and in every invite."
defaultValue="Northwind Studio"
isChanged
/>
<TextField
label="Short description"
defaultValue="!"
isChanged
isInvalid
errorMessage="Enter a short description."
/>
</div>
);
}Changed, with no label
A field named only by aria-label or aria-labelledby, as a cell of a review grid is, has no label row for the chip. Changed, it draws the changed dot instead: the chip's 6px dot in clay, inside its frame, before its button or at its text's end, with its text padded clear. The dot holds the word "Changed", or your changedLabel, unseen, and the field names it in aria-describedby, so a screen reader reads it after the field's name. It leaves while the field is invalid, as the chip does.
Every field with a frame draws it: Text field, Text area, Number field, Select, Combo box, Multi-select, Date range picker and Date picker. The first two fields below are changed; the third is not.
Example
Example
import { NumberField, TextField } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function FieldChangedUnlabelled(): ReactElement {
return (
<div className="stack" style={{ width: '100%', maxWidth: '20rem' }}>
{/* Named by aria-label alone, as a cell of a review grid is by its column and row. */}
<TextField aria-label="Memo, Corner Mart" defaultValue="Milk, bread and eggs" isChanged />
<NumberField aria-label="Amount, Corner Mart" money={{ symbol: '$' }} defaultValue={4218} isChanged />
<TextField aria-label="Memo, City Transit" defaultValue="Bus fare" />
</div>
);
}Length count
A count advises and never blocks. It stays empty until the value reaches 70% of the budget, then reads "34 of about 40". Past the budget it turns to ink at 700, and the value still grows and saves.
Example
Example
'use client';
import { TextField } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function LengthCounter(): ReactElement {
return (
<TextField
label="About the workspace"
description="Keep typing past 40. Nothing stops you."
defaultValue="A shared space for the design team"
budget={40}
style={{ width: '100%', maxWidth: '24rem' }}
/>
);
}Usage
Text field renders all of this in one element. For a custom layout, put the parts inside a React Aria field:
- Put the
fieldclass anddata-changedon the field's root. - Keep the chip in the label row, where the stylesheet shows or hides it.
- While the field is changed, pass the chip's
idin the field'saria-describedby.
FieldError appears only while the field is invalid.
import { useId } from 'react';
import { TextField } from 'react-aria-components';
import { Description, FieldChanged, FieldCounter, FieldError, Input, Label } from '@snapshot-ui/react';
export function ShortDescription({
value,
onChange,
isChanged,
}: {
value: string;
onChange: (value: string) => void;
isChanged: boolean;
}) {
const chipId = useId();
return (
<TextField
className="field"
data-changed={isChanged ? '' : undefined}
aria-describedby={isChanged ? chipId : undefined}
value={value}
onChange={onChange}
>
<div className="field-head">
<Label>Short description</Label>
<FieldChanged id={chipId} />
</div>
<Input />
<Description>One sentence, for search results.</Description>
<FieldError />
<FieldCounter count={value.length} budget={60} />
</TextField>
);
}In plain HTML, use the classes field, field-head, field-label, field-description, field-error, field-counter and field-changed.
Props
The count takes the numbers and the text. The chip takes its text as children. Label, Description and FieldError accept React Aria's Label, Text and FieldError props.
budget number | About how much is wanted. A budget, not a limit: nothing stops a longer value. A budget that is not a positive number shows nothing. |
|---|---|
count number | How much has been written: usually the length of the value. |
label ((state: FieldCounterState) => string) | Replaces the text. Return an empty string to show nothing. Default: ({ count, budget }) => `${count} of about ${budget}` |
showFrom number | The share of the budget at which the count first appears, from 0 to 1. Nothing shows while the count is 0, whatever this is. Default: 0.7 |
children ReactNode | The text of the chip. Default: 'Changed' |
|---|
Accessibility
Label is a real label, so a click on it focuses the control. Description and FieldError join the control's accessible description. The chip sits beside the label, not inside it, so the control keeps its name. Naming the chip in aria-describedby has a screen reader read it with the field. A hidden element named there is still read, so Text field also removes the chip while the field is invalid.
The count is a polite status region, in the page and empty from the start, so a screen reader announces it as it changes. To announce less often, pass a count that settles after a pause.
Design rules
What keeps fields in the system.
Name the field in the label, in sentence case. Keep the hint for what the label cannot say.
Show a count only where length helps the reader. It advises; a budget is not a limit.
Let the control draw the changed border whenever the chip shows. They always appear together.
Never use clay for anything but the Changed chip. It means unsaved.
Never block typing at the budget. Past it the count turns to ink and the value still saves.
Never leave the chip beside an error. Invalid outranks changed.