Skip to main content
useId is a React Hook for generating unique IDs that are stable across the server and client.
function useId(): string

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

Form fields with labels

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

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 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>
  );
}

Reusable form components

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>
  );
}
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>;
}

Extract reusable form components

// 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.