useId Hook
Accessible HTML requires that form inputs have unique IDs that match their labels via htmlFor. When a component renders multiple times on the same page, you need a way to generate unique IDs for each instance. React 18 introduced useId to solve this problem correctly — including across server-side rendering, where naive approaches like Math.random() or incrementing counters cause hydration mismatches.
The Problem: Why Not Math.random()?
When React renders on the server (SSR) and again on the client during hydration, both renders must produce identical output. Math.random() generates a different value on the server and on the client — React notices the mismatch and throws a hydration error.
// ❌ Math.random() — different on server vs client = hydration error
function Field({ label }) {
const id = Math.random().toString(36).slice(2) // 'x2f4p' on server, 'k9a2m' on client
return (
<div>
<label htmlFor={id}>{label}</label> {/* server: htmlFor="x2f4p" */}
<input id={id} /> {/* client: id="k9a2m" — MISMATCH */}
</div>
)
}
// ❌ Module-level counter — resets between server and client
let counter = 0
function Field({ label }) {
const id = `field-${counter++}` // may be 'field-0' on server, 'field-3' on client
// (if other components called this before, the count differs)
...
}The Solution: useId
import { useId } from 'react'
function Field({ label }) {
// Stable, unique ID — same on server and client for this component instance
const id = useId()
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
)
}
// Renders four times → four different, stable IDs:
// :r0: :r1: :r2: :r3:
<Field label="Name" />
<Field label="Email" />
<Field label="Password" />
<Field label="Bio" />useId generates a unique string like :r0:, :r1:, etc. — the colon characters are intentional and valid in HTML id attributes (they make the IDs impossible to accidentally collide with CSS selectors, which use : as a pseudo-class delimiter).
A Reusable FormField Component
The real power of useId is building reusable form field components that you can drop anywhere on a page without ID collisions.
import { useId } from 'react'
type FormFieldProps = {
label: string
type?: string
required?: boolean
helpText?: string
error?: string
}
function FormField({ label, type = 'text', required, helpText, error }: FormFieldProps) {
const id = useId()
const helpId = `${id}-help`
const errorId = `${id}-error`
return (
<div style={{ marginBottom: 16 }}>
<label htmlFor={id}>
{label}
{required && <span aria-hidden="true"> *</span>}
</label>
<input
id={id}
type={type}
required={required}
aria-describedby={[helpText && helpId, error && errorId]
.filter(Boolean)
.join(' ') || undefined}
aria-invalid={error ? 'true' : undefined}
style={{ display: 'block', width: '100%' }}
/>
{helpText && (
<p id={helpId} style={{ fontSize: '0.85em', color: '#666' }}>
{helpText}
</p>
)}
{error && (
<p id={errorId} role="alert" style={{ color: 'red', fontSize: '0.85em' }}>
{error}
</p>
)}
</div>
)
}
// Usage — each instance gets its own unique, accessible IDs
function RegistrationForm() {
return (
<form>
<FormField
label="Full name"
required
helpText="As it appears on your ID"
/>
<FormField
label="Email"
type="email"
required
error="Please enter a valid email address"
/>
<FormField
label="Password"
type="password"
required
helpText="At least 8 characters"
/>
<button type="submit">Register</button>
</form>
)
}Generating Multiple IDs from One useId Call
You only need to call useId once per component. To generate multiple related IDs, suffix the base ID string. This keeps the hook calls minimal and the relationship between IDs explicit.
import { useId } from 'react'
function DateRangePicker({ label }) {
// One useId call, multiple derived IDs
const id = useId()
const startId = `${id}-start`
const endId = `${id}-end`
const legendId = `${id}-legend`
return (
<fieldset aria-labelledby={legendId}>
<legend id={legendId}>{label}</legend>
<div>
<label htmlFor={startId}>Start date</label>
<input id={startId} type="date" />
</div>
<div>
<label htmlFor={endId}>End date</label>
<input id={endId} type="date" />
</div>
</fieldset>
)
}
// Both DateRangePickers get completely independent sets of IDs:
// First: ":r0:-start", ":r0:-end", ":r0:-legend"
// Second: ":r1:-start", ":r1:-end", ":r1:-legend"
<DateRangePicker label="Booking dates" />
<DateRangePicker label="Availability window" />Why Not Use the Array Index?
Using the index from a .map() as an ID is tempting but fragile:
Approach | Problem |
|---|---|
| Different on server vs client — hydration mismatch |
Module counter ( | Resets between server/client renders — mismatch |
Array index | Changes when items are reordered — IDs shift, breaking label associations |
| Stable, unique, SSR-safe — correct in all cases |
useId with aria- Attributes
useId is useful beyond htmlFor/id pairs. Any ARIA relationship that links elements by ID benefits from it:
import { useId } from 'react'
// Expandable accordion with proper ARIA
function Accordion({ title, children }) {
const [open, setOpen] = useState(false)
const id = useId()
const headerId = `${id}-header`
const panelId = `${id}-panel`
return (
<div>
<button
id={headerId}
aria-expanded={open}
aria-controls={panelId}
onClick={() => setOpen(o => !o)}
>
{title}
</button>
<div
id={panelId}
role="region"
aria-labelledby={headerId}
hidden={!open}
>
{children}
</div>
</div>
)
}
// Combobox / autocomplete
function Combobox({ label, options }) {
const id = useId()
const listboxId = `${id}-listbox`
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
role="combobox"
aria-autocomplete="list"
aria-controls={listboxId}
/>
<ul id={listboxId} role="listbox">
{options.map(opt => (
<li key={opt.value} role="option">{opt.label}</li>
))}
</ul>
</div>
)
}What useId Does NOT Do
Not for list keys — use stable data IDs or index for
keyprops, notuseId()Not a UUID generator — do not use it for database IDs or unique entity identifiers
Not for CSS selectors — the
:characters in generated IDs break CSS selectors (this is intentional to prevent accidental styling)