useId is a React Hook for generating unique IDs that are stable across the server and client.
Parameters
useId does not take any parameters.
Returns
useId returns a unique ID string associated with this particular useId call in this particular component.
Usage
Generating unique IDs for accessibility attributes
Call useId at the top level of your component to generate a unique ID:
import { useId } from 'react';
function PasswordField() {
const passwordHintId = useId();
return (
<>
<label>
Password:
<input
type="password"
aria-describedby={passwordHintId}
/>
</label>
<p id={passwordHintId}>
The password should contain at least 8 characters
</p>
</>
);
}
useId generates a string that includes a colon (:). This ensures the ID is unique but it’s not a valid CSS selector. If you need to use the ID in a CSS selector, add a prefix like id-${useId()}.
If you need to give IDs to multiple related elements, you can call useId once and generate multiple IDs with suffixes:
import { useId } from 'react';
function Form() {
const id = useId();
return (
<form>
<label htmlFor={id + '-firstName'}>First Name:</label>
<input id={id + '-firstName'} type="text" />
<label htmlFor={id + '-lastName'}>Last Name:</label>
<input id={id + '-lastName'} type="text" />
</form>
);
}
Specifying a shared prefix for all generated IDs
If you render multiple independent React applications on a single page, pass unique identifierPrefix options to your createRoot or hydrateRoot calls:
import { createRoot } from 'react-dom/client';
const root1 = createRoot(document.getElementById('root1'), {
identifierPrefix: 'app1-'
});
root1.render(<App />);
const root2 = createRoot(document.getElementById('root2'), {
identifierPrefix: 'app2-'
});
root2.render(<App />);
This ensures that the IDs generated by the two apps will never clash.
Common Use Cases
function TextField({ label, type = 'text' }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type={type} />
</div>
);
}
function RegistrationForm() {
return (
<form>
<TextField label="Username" />
<TextField label="Email" type="email" />
<TextField label="Password" type="password" />
</form>
);
}
ARIA relationships
aria-describedby
aria-labelledby
aria-controls
function FormField({ label, helpText, error }) {
const id = useId();
const helpId = id + '-help';
const errorId = id + '-error';
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
aria-describedby={`${helpId} ${error ? errorId : ''}`}
aria-invalid={!!error}
/>
<p id={helpId}>{helpText}</p>
{error && <p id={errorId} role="alert">{error}</p>}
</div>
);
}
function RadioGroup({ legend, options }) {
const id = useId();
return (
<fieldset>
<legend id={id + '-legend'}>{legend}</legend>
<div role="radiogroup" aria-labelledby={id + '-legend'}>
{options.map((option, index) => (
<label key={index}>
<input
type="radio"
name={id}
value={option.value}
/>
{option.label}
</label>
))}
</div>
</fieldset>
);
}
function Accordion({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
const id = useId();
const contentId = id + '-content';
return (
<div>
<button
aria-expanded={isOpen}
aria-controls={contentId}
onClick={() => setIsOpen(!isOpen)}
>
{title}
</button>
<div id={contentId} hidden={!isOpen}>
{children}
</div>
</div>
);
}
function ComboBox({ label, options }) {
const id = useId();
const inputId = id + '-input';
const listId = id + '-list';
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<label htmlFor={inputId}>{label}</label>
<input
id={inputId}
role="combobox"
aria-expanded={isOpen}
aria-controls={listId}
aria-autocomplete="list"
onFocus={() => setIsOpen(true)}
/>
{isOpen && (
<ul id={listId} role="listbox">
{options.map((option, index) => (
<li key={index} role="option">
{option}
</li>
))}
</ul>
)}
</div>
);
}
function Checkbox({ label, checked, onChange }) {
const id = useId();
return (
<div>
<input
id={id}
type="checkbox"
checked={checked}
onChange={onChange}
/>
<label htmlFor={id}>{label}</label>
</div>
);
}
function Settings() {
const [notifications, setNotifications] = useState(true);
const [newsletter, setNewsletter] = useState(false);
return (
<form>
<Checkbox
label="Enable notifications"
checked={notifications}
onChange={e => setNotifications(e.target.checked)}
/>
<Checkbox
label="Subscribe to newsletter"
checked={newsletter}
onChange={e => setNewsletter(e.target.checked)}
/>
</form>
);
}
Modal dialogs
function Modal({ title, children, isOpen, onClose }) {
const id = useId();
const titleId = id + '-title';
const descId = id + '-desc';
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descId}
>
<h2 id={titleId}>{title}</h2>
<div id={descId}>{children}</div>
<button onClick={onClose}>Close</button>
</div>
);
}
TypeScript
import { useId } from 'react';
interface TextFieldProps {
label: string;
type?: 'text' | 'email' | 'password';
helpText?: string;
}
function TextField({ label, type = 'text', helpText }: TextFieldProps) {
const id = useId();
const helpId = helpText ? `${id}-help` : undefined;
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
type={type}
aria-describedby={helpId}
/>
{helpText && <p id={helpId}>{helpText}</p>}
</div>
);
}
Troubleshooting
Why is useId better than an incrementing counter?
You might wonder why useId is better than incrementing a global counter:
// ❌ Don't do this
let nextId = 0;
function Form() {
const id = nextId++; // Different on server and client!
return (
<>
<label htmlFor={`field-${id}`}>Name:</label>
<input id={`field-${id}`} />
</>
);
}
Problem: This doesn’t work with server-side rendering. During hydration, the component on the server will have a different ID than on the client, causing a mismatch.
// ✅ Do this instead
function Form() {
const id = useId(); // Same on server and client!
return (
<>
<label htmlFor={id}>Name:</label>
<input id={id} />
</>
);
}
Can I use useId to generate keys in a list?
No, keys should be generated from your data:
// ❌ Bad: Don't use useId for keys
function List({ items }) {
return items.map(item => {
const id = useId(); // ❌ Wrong!
return <li key={id}>{item}</li>;
});
}
// ✅ Good: Use stable identifiers from data
function List({ items }) {
return items.map(item => (
<li key={item.id}>{item.name}</li>
));
}
// ✅ Acceptable: Use index if items never reorder
function List({ items }) {
return items.map((item, index) => (
<li key={index}>{item}</li>
));
}
The ID contains a colon - can I use it in CSS?
The generated ID contains a colon (:) which is valid in HTML but needs escaping in CSS:
function Component() {
const id = useId(); // Returns something like ":r1:"
return (
<div>
<div id={id}>Content</div>
{/* CSS selector needs escaping */}
<style>{`
#${CSS.escape(id)} {
color: blue;
}
`}</style>
</div>
);
}
Or add a prefix to make it CSS-friendly:
function Component() {
const id = 'id-' + useId();
return (
<div>
<div id={id}>Content</div>
<style>{`
#${id} {
color: blue;
}
`}</style>
</div>
);
}
Do I need a different ID for every element?
No! Reuse the same ID for related elements:
// ❌ Wasteful: Too many useId calls
function Form() {
const inputId = useId();
const labelId = useId();
const helpId = useId();
return (
<div>
<label id={labelId} htmlFor={inputId}>Name:</label>
<input id={inputId} aria-describedby={helpId} />
<p id={helpId}>Enter your full name</p>
</div>
);
}
// ✅ Efficient: One useId with suffixes
function Form() {
const id = useId();
return (
<div>
<label htmlFor={id}>Name:</label>
<input id={id} aria-describedby={`${id}-help`} />
<p id={`${id}-help`}>Enter your full name</p>
</div>
);
}
Best Practices
Use for accessibility, not styling
// ✅ Good: Using for accessibility
function Field() {
const id = useId();
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} />
</>
);
}
// ❌ Bad: Using for styling (use classes instead)
function Field() {
const id = useId();
return <div id={id} className="field">Content</div>;
}
// Create reusable components with useId
function TextField({ label, ...props }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} {...props} />
</div>
);
}
function TextArea({ label, ...props }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<textarea id={id} {...props} />
</div>
);
}
// Use throughout your app
function ContactForm() {
return (
<form>
<TextField label="Name" required />
<TextField label="Email" type="email" required />
<TextArea label="Message" rows={5} required />
</form>
);
}
Combine with other accessibility features
function AccessibleField({ label, error, helpText, ...props }) {
const id = useId();
const errorId = `${id}-error`;
const helpId = `${id}-help`;
const describedBy = [
helpText && helpId,
error && errorId
].filter(Boolean).join(' ');
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
aria-describedby={describedBy || undefined}
aria-invalid={!!error}
{...props}
/>
{helpText && <p id={helpId}>{helpText}</p>}
{error && <p id={errorId} role="alert">{error}</p>}
</div>
);
}
When to use useId
Use useId when:
- ✅ Connecting form labels to inputs
- ✅ Implementing ARIA relationships (aria-describedby, aria-labelledby, etc.)
- ✅ Creating reusable form components
- ✅ Building accessible custom controls
Don’t use useId for:
- ❌ List keys (use data IDs or index)
- ❌ Styling selectors (use classes)
- ❌ Database identifiers (use UUIDs or backend-generated IDs)
- ❌ Analytics tracking (use stable identifiers)
Server-Side Rendering
useId is specifically designed to work with SSR. The IDs generated on the server will match the IDs generated during client-side hydration:
// Server renders:
<input id=":r1:" />
// Client hydrates with same ID:
<input id=":r1:" />
// No hydration mismatch!
This is why useId is better than generating random IDs or using a counter.