Articles

Improving React Accessibility with `useId`

Kent C. Dodds
Kent C. Dodds

When building accessible React applications, one of the most common requirements is to ensure that form fields and their labels are properly associated. This usually means giving each input a unique id and making sure the corresponding <label> uses the same value in its htmlFor attribute. But if you've ever tried to build a reusable form component, you know that generating unique IDs can be a pain.

Let's talk about why this matters, the problems with common approaches, and how React's useId hook makes it all so much easier.

Why Unique IDs Matter

Imagine you have a form with several fields:


function ContactForm() {
return (
<form>
<label htmlFor="name">Name</label>
<input id="name" type="text" />
<label htmlFor="email">Email</label>
<input id="email" type="email" />
</form>
)
}

This works fine—until you want to reuse a field component multiple times, or render a list of fields dynamically. If you hardcode the id and then reuse that component, you end up with duplicate IDs, which is invalid HTML and breaks accessibility for screen readers.

One easy way to tell whether a label is associated with an input is to click on the label and see if the input is focused. If it is, then the label is associated with the input. If it's not, then you've got work to do.

The Temptation of Random IDs (and Why It's a Trap)

A common workaround is to generate a random string for each field:


function Field({ label, ...props }) {
const id = Math.random().toString(36).slice(2)
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} {...props} />
</div>
)
}

This seems to work, but it introduces subtle bugs:

  • The ID changes on every render, which can confuse screen readers.
  • If you're doing server-side rendering (SSR), the IDs generated on the server won't match the ones on the client, causing hydration errors.

Enter useId: The React Way

React 18 introduced the useId hook to solve this exact problem. It generates a unique, stable ID for each component instance, and it works seamlessly with SSR.

Here's how you use it:


import { useId } from 'react'
function Field({ label, ...props }) {
const id = useId()
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} {...props} />
</div>
)
}

Now, every time you render <Field label="Name" />, it gets a unique, stable ID. You can safely use this component multiple times—even in lists or dynamic forms—without worrying about duplicate IDs or hydration mismatches.

When Not to Use useId

It's important to note that useId is only for DOM relationships, like associating a label with an input. Don't use it for React keys in lists:


const key = useId()
// ❌ Don't do this!
return <div key={key}>{item}</div>

React keys should come from your data, not from useId. The ID from useId is only stable for the lifetime of a component instance, not across renders or data changes.

Takeaways

  • Unique IDs are essential for accessible forms and DOM relationships.
  • Generating IDs manually is error-prone, especially with SSR.
  • useId gives you a stable, unique ID for each component instance, the React way.
  • Use useId for DOM relationships, not for React keys.

For more, check out the official React docs on useId.

You'll learn this and other accessibility tips in the workshops on EpicReact.dev.

Get my free 7-part email course on React!

Delivered straight to your inbox.