useId Hook
Generate a unique, stable ID for accessibility attributes that is consistent between server and client.
Search across all documentation pages
Generate a unique, stable ID for accessibility attributes that is consistent between server and client.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
TextField instance gets its own unique ID from useId<label> to <input> via htmlFor and links help text via aria-describedbyuseId generates an ID based on the component's position in the React tree: character (e.g., :r1:) to avoid collisions with user-defined IDsuseId calls in the same component produce different IDs| Parameter | Type | Description |
|---|---|---|
| (none) | - | useId takes no parameters |
| Return | Type | Description |
|---|---|---|
id | string | Unique ID string (e.g., :r1:, :r2:) |
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.// 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} />
</>
);
}Do not use for list keys - useId generates a single ID per hook call, not per list item. It is not suitable for key props. 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 HTML id attributes 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, useId cannot be called inside conditions or loops. Fix: Call useId at 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 identifierPrefix option in createRoot or hydrateRoot.
| 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.
useId derives IDs from the component's position in the React tree, so server and client produce the same ID.useId whenever SSR is involved.useId generates one ID per hook call, not per list item.htmlFor, aria-labelledby), not for key props.item.id) or stable identifiers for lists.const id = useId();
const nameId = `${id}-name`;
const emailId = `${id}-email`;
const helpId = `${id}-help`;useId call.useId generates IDs like :r1: with colons to avoid collisions with user-defined IDs.id attributes but must be escaped in CSS selectors: #\:r1\:.[id="value"] instead of ID selectors in CSS.:r1:, :r2:, etc.).identifierPrefix option in createRoot or hydrateRoot to namespace IDs.const root = createRoot(container, { identifierPrefix: "app1-" });
// IDs: ":app1-r1:", ":app1-r2:", etc.useId must be called at the top level of your component.// useId always returns string -- no generic needed
const id: string = useId();string.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} />
</>
);
}:r1:) is an implementation detail and may change between React versions.useId() call in the same component returns a unique ID.useIduseIdReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥