useId Hook
Generate a unique, stable ID for accessibility attributes that is consistent between server and client.
Recipe
Quick-reference recipe card — copy-paste ready.
const id = useId();
// Use for accessible form elements
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
// Derive multiple IDs from one
<label htmlFor={`${id}-first`}>First Name</label>
<input id={`${id}-first`} />
<label htmlFor={`${id}-last`}>Last Name</label>
<input id={`${id}-last`} />When to reach for this: You need a unique ID for htmlFor, aria-labelledby, aria-describedby, or any DOM attribute that requires an ID — especially in SSR or reusable components.
Working Example
"use client";
import { useId, useState } from "react";
interface TextFieldProps {
label: string;
helpText?: string;
}
export function TextField({ label, helpText }: TextFieldProps) {
const id = useId();
const helpId = `${id}-help`;
const [value, setValue] = useState("");
return (
<div className="space-y-1">
<label htmlFor={id} className="block text-sm font-medium">
{label}
</label>
<input
id={id}
value={value}
onChange={(e) => setValue(e.target.value)}
aria-describedby={helpText ? helpId : undefined}
className="border rounded px-3 py-2 w-full"
/>
{helpText && (
<p id={helpId} className="text-xs text-gray-500">
{helpText}
</p>
)}
</div>
);
}
export function SignupForm() {
return (
<form className="space-y-4 max-w-sm">
<TextField label="Email" helpText="We'll never share your email." />
<TextField label="Password" helpText="At least 8 characters." />
<TextField label="Username" />
</form>
);
}What this demonstrates:
- Each
TextFieldinstance gets its own unique ID fromuseId - The ID links
<label>to<input>viahtmlForand links help text viaaria-describedby - Multiple instances of the same component coexist without ID collisions
- IDs are stable across server and client renders, avoiding hydration mismatches
Deep Dive
How It Works
useIdgenerates an ID based on the component's position in the React tree- The ID is deterministic — the same component in the same tree position always gets the same ID
- The generated string includes a
:character (e.g.,:r1:) to avoid collisions with user-defined IDs - On the server, React generates the same IDs as on the client, ensuring hydration consistency
- Multiple
useIdcalls in the same component produce different IDs
Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
| (none) | — | useId takes no parameters |
| Return | Type | Description |
|---|---|---|
id | string | Unique ID string (e.g., :r1:, :r2:) |
Variations
Accessible listbox:
function Listbox({ label, options }: ListboxProps) {
const id = useId();
const labelId = `${id}-label`;
const listId = `${id}-list`;
return (
<div>
<span id={labelId}>{label}</span>
<ul id={listId} role="listbox" aria-labelledby={labelId}>
{options.map((opt, i) => (
<li key={opt.value} id={`${id}-option-${i}`} role="option">
{opt.label}
</li>
))}
</ul>
</div>
);
}Custom identifierPrefix for micro-frontends:
// In createRoot or hydrateRoot
const root = createRoot(container, {
identifierPrefix: "app1-",
});
// Generated IDs: ":app1-r1:", ":app1-r2:", etc.TypeScript Notes
// useId always returns a string — no generic needed
const id: string = useId();
// When building a component library, accept an optional override
interface InputProps {
id?: string;
label: string;
}
function Input({ id: propId, label }: InputProps) {
const generatedId = useId();
const inputId = propId ?? generatedId;
return (
<>
<label htmlFor={inputId}>{label}</label>
<input id={inputId} />
</>
);
}Gotchas
-
Do not use for list keys —
useIdgenerates a single ID per hook call, not per list item. It is not suitable forkeyprops. Fix: Use data-driven keys (item.id) or stable identifiers for list keys. -
IDs contain colons — The generated ID format (
:r1:) includes colons, which are valid in HTMLidattributes but may cause issues with CSS selectors like#\:r1\:. Fix: Use[id="value"]attribute selectors or escape colons in CSS. -
Cannot use conditionally — Like all hooks,
useIdcannot be called inside conditions or loops. Fix: CalluseIdat the top level and derive sub-IDs with string concatenation. -
Multiple roots without prefix — Two separate React roots on the same page may generate colliding IDs. Fix: Use the
identifierPrefixoption increateRootorhydrateRoot.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
crypto.randomUUID() | Client-only app, no SSR | SSR is involved — causes hydration mismatches |
| Counter-based ID | Outside React (utility functions) | Inside components — not deterministic across server/client |
useRef with lazy init | You need a stable random value, not a tree-position-based ID | You need SSR-safe accessibility attributes |
HTML <label> wrapping | Label wraps the input directly, no id needed | Input and label are separated in the DOM |
Why not just use a random ID? Random IDs generated during render differ between server and client, causing hydration mismatches and React warnings. useId solves this by deriving IDs from the component tree.
FAQs
Why can't I just use Math.random() or crypto.randomUUID() for element IDs?
- Random IDs generated during render differ between server and client, causing hydration mismatches.
useIdderives IDs from the component's position in the React tree, so server and client produce the same ID.- Use
useIdwhenever SSR is involved.
Can I use useId to generate keys for list items?
- No.
useIdgenerates one ID per hook call, not per list item. - It is designed for accessibility attributes (
htmlFor,aria-labelledby), not forkeyprops. - Use data-driven keys (
item.id) or stable identifiers for lists.
How do I derive multiple IDs from a single useId call?
const id = useId();
const nameId = `${id}-name`;
const emailId = `${id}-email`;
const helpId = `${id}-help`;- Use string concatenation to create sub-IDs from one
useIdcall.
Gotcha: Why do generated IDs contain colons, and can that cause CSS issues?
useIdgenerates IDs like:r1:with colons to avoid collisions with user-defined IDs.- Colons are valid in HTML
idattributes but must be escaped in CSS selectors:#\:r1\:. - Prefer attribute selectors
[id="value"]instead of ID selectors in CSS.
What happens if two separate React roots on the same page generate conflicting IDs?
- Without configuration, two roots may produce the same IDs (
:r1:,:r2:, etc.). - Use the
identifierPrefixoption increateRootorhydrateRootto namespace IDs.
const root = createRoot(container, { identifierPrefix: "app1-" });
// IDs: ":app1-r1:", ":app1-r2:", etc.Can I call useId conditionally or inside a loop?
- No. Like all hooks,
useIdmust be called at the top level of your component. - Call it once and derive sub-IDs with string concatenation for conditional or repeated elements.
How do I type useId in TypeScript?
// useId always returns string -- no generic needed
const id: string = useId();- There is no generic parameter. The return type is always
string.
How do I build a reusable component that accepts an optional id prop but falls back to useId?
interface InputProps {
id?: string;
label: string;
}
function Input({ id: propId, label }: InputProps) {
const generatedId = useId();
const inputId = propId ?? generatedId;
return (
<>
<label htmlFor={inputId}>{label}</label>
<input id={inputId} />
</>
);
}Gotcha: Is the generated ID format stable across React versions?
- The exact format (e.g.,
:r1:) is an implementation detail and may change between React versions. - Do not parse or depend on the internal structure of the generated ID.
- Treat it as an opaque string.
Does calling useId multiple times in the same component produce different IDs?
- Yes. Each
useId()call in the same component returns a unique ID. - This is useful when a component needs multiple unrelated ID attributes.
- All generated IDs remain stable across server and client renders.
Related
- useRef — access DOM elements directly without IDs
- Custom Hooks — build accessible form primitives using
useId - use — React 19 primitives that work alongside
useId