Field
StableLabel above, hint under it, error in words between the hint and the control, with the ids and aria wiring done for you.
Field draws the label row, the hint and the error, then calls its child with the props to spread on the control: id, aria-describedby and aria-invalid. It works with any control that takes them: TextInput, Select, Textarea, PasswordInput, NumberField and the rest. ErrorText is the same error line on its own, for controls that carry their own label.
Profile form
import { useRef, useState, type FormEvent } from 'react'import { checkEmail, checkName } from '@rchursin/product/forms'import { Button, Field, TextInput } from '@rchursin/product/ui'
/** * Label above, hint under it, the error between the hint and the field. The form starts as if * Save was pressed once, so the email shows its error; after that, errors update as you type. */export default function FieldProfile() { const [name, setName] = useState('Alex Rivera') const [email, setEmail] = useState('alex@example') const [website, setWebsite] = useState('') const [tried, setTried] = useState(true) const [saved, setSaved] = useState(false) const nameRef = useRef<HTMLInputElement>(null) const emailRef = useRef<HTMLInputElement>(null)
const nameError = tried ? checkName(name) : undefined const emailError = tried ? checkEmail(email) : undefined
Usage
import { Field, ErrorText, type FieldControlProps } from '@rchursin/product/ui'<Field label="Email" hint="We send sign-in codes here." error={emailError}> {(p) => <TextInput {...p} type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} />}</Field>Examples
Label action
labelAction adds a small control at the end of the label row. This one fills the select with the device’s time zone.
import { useState } from 'react'import { Field, Select } from '@rchursin/product/ui'
const zones = [ { value: 'America/Los_Angeles', label: 'Pacific Time (Los Angeles)' }, { value: 'America/Denver', label: 'Mountain Time (Denver)' }, { value: 'America/Chicago', label: 'Central Time (Chicago)' }, { value: 'America/New_York', label: 'Eastern Time (New York)' }, { value: 'Europe/London', label: 'London' }, { value: 'Europe/Berlin', label: 'Berlin' }, { value: 'Asia/Tokyo', label: 'Tokyo' },]
/** * labelAction puts a small control at the end of the label row: here, a button that reads the * device's time zone (in the click, never during render) and chooses it. */export default function FieldLabelAction() { const [zone, setZone] = useState('America/Los_Angeles') const [extra, setExtra] = useState<string | null>(null)ErrorText on its own
A checkbox has its own label, so it sits outside Field. Press Create account without ticking it: the error appears above and focus moves to the box.
import { useId, useRef, useState, type FormEvent } from 'react'import { messages } from '@rchursin/product/forms'import { Button, Checkbox, ErrorText } from '@rchursin/product/ui'
/** * A checkbox carries its own label, so it doesn't go inside Field. Put ErrorText above it and * wire aria-invalid and aria-describedby yourself. Press the button without ticking the box. */export default function FieldErrorText() { const errorId = useId() const box = useRef<HTMLInputElement>(null) const [agreed, setAgreed] = useState(false) const [tried, setTried] = useState(false) const [done, setDone] = useState(false) const error = tried && !agreed ? messages.termsEmpty : undefined
function submit(e: FormEvent) { e.preventDefault() // a sample: nothing leaves the page setTried(true) if (!agreed) return box.current?.focus()API
Field props
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | ReactNode | – | The visible <label>, tied to the control through the id. |
childrenrequired | (control: FieldControlProps) => ReactNode | – | Render prop. Spread its argument onto the control: {(p) => <TextInput {...p} />}. |
hint | ReactNode | – | A line of help under the label. Its id goes first in aria-describedby. |
error | ReactNode | – | The error in words. Shows ErrorText between the hint and the control, adds its id to aria-describedby and sets aria-invalid. Leave it undefined when there is no error. |
optional | boolean | false | Adds “(optional)” after the label. Required fields carry no mark. |
id | string | useId() | The control’s id. The hint and error get `${id}-hint` and `${id}-error`. Pass one when you need to find the control later. |
labelAction | ReactNode | – | Something at the end of the label row, such as a “Forgot password?” link or a small button. |
className | string | – | Classes on the wrapper, a column with a 6px gap. |
FieldControlProps (what children receive)
| Prop | Type | Default | Description |
|---|---|---|---|
idrequired | string | – | Matches the label’s htmlFor. |
aria-describedby | string | – | The hint id, then the error id, separated by a space. Undefined when there are neither. |
aria-invalid | boolean | – | true while there is an error, otherwise undefined (the attribute is left off). |
ErrorText props
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The message. Say what to do: “Enter your email address”. |
id | string | – | Point the control’s aria-describedby at it. |
className | string | – | Extra classes on the <p>. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Field adds no stops of its own: Tab goes to the control (and to a labelAction control, which comes first, in the label row). |
- The label is a real <label for>, so clicking it focuses the control and screen readers read it as the name.
- The hint and the error are linked with aria-describedby, hint first, so they are read after the name.
- An error sets aria-invalid="true", which draws a firmer ring on lux-input fields. The message starts with an alert icon and a hidden “Error:”, so it never relies on colour or the icon alone.
- Errors are not a live region. Show them after a submit and move focus to the first field to fix, as the examples do, or list them in an ErrorSummary on long forms.
Guidelines
Do
- Keep every label visible; a placeholder is not a label.
- Say what to do in the error: “Enter an email address in the correct format, like name@example.com”.
- Mark the optional fields and leave required ones unmarked.
- Check on submit, then keep the errors up to date as people fix them.
Don’t
- Don’t show errors while someone is still typing their first attempt.
- Don’t write “Invalid”, “Please” or “This field is required”.
- Don’t hide the hint once there is an error: people still need it.
In use
Related
- Text inputA one-line field in the inset lux-input finish, in two sizes, with an optional icon inside and a slot on the right.
- Error summaryA “There is a problem” box at the top of a form that takes focus after a failed submit and links each message to its field.
- Form toolkituseForm for values, errors and validation timing, plus the checks and GOV.UK-style messages every form in the kit shares.
- Password inputA password field with a Show / Hide button and a caps-lock warning, where paste and password managers always work.
Source
The component’s own files, exactly as they run on this site.
/** * Form fields. Labels sit above inputs, hints under the label, errors between the hint and the * input with an icon and the word "Error:" for screen readers (never colour alone). * Optional fields say "(optional)"; required ones carry no mark. * * <Field label="Email" hint="We send the sign-in code here." error={err}> * {(p) => <TextInput {...p} type="email" autoComplete="email" />} * </Field> */import { forwardRef, useId, useLayoutEffect, useRef, useState, type InputHTMLAttributes, type KeyboardEvent, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes,