Telerik blogs

Learn how to use useId in Vue 3 to generate stable, unique IDs for reusable components, improve accessibility and avoid SSR hydration issues.

If you’ve ever built a reusable form component in Vue, you’ve probably hardcoded an id without much thought. For a single form field, that works as expected: the label is correctly associated with the input, and clicking the label focuses the field.

The challenge begins when that input becomes a reusable component.

What worked perfectly once can quickly become a problem when the same component is rendered multiple times. Suddenly, every instance produces the same id, leaving you with duplicate IDs, broken label associations and components that are no longer as accessible as they should be.

A common reaction is to generate IDs yourself using Math.random(), a counter or a UUID library. While those approaches solve one problem, they can introduce another, especially in applications that use server-side rendering.

This is where Vue’s useId comes in.

Rather than asking you to manage IDs manually, useId generates unique, stable identifiers that work seamlessly across reusable components and server-side rendering. In this article, you’ll learn why it exists, how it works and when you should reach for it in your own applications.

The Hidden Problem with IDs

Hardcoding an id isn’t inherently wrong. In fact, it’s the simplest solution when you’re building a one-off Vue page. The challenge starts when you turn that markup into a reusable component.

Consider this simple BaseInput component:

<script setup>
defineProps({
  label: String,
})
</script>

<template>
  <label for="input">
    {{ label }}
  </label>

  <input id="input" />
</template>

If you render the component once, everything works as expected.

<BaseInput label="Email Address" />

Now imagine using the same component to build a registration form.

<BaseInput label="First Name" />
<BaseInput label="Last Name" />
<BaseInput label="Email Address" />

Each component instance renders the same id, leaving your page with multiple elements that all look like this:

<input id="input" />

This creates invalid HTML because an id is meant to be unique within a page. More importantly, it breaks the relationship between the label and its corresponding input. Clicking a label may no longer focus the correct field, and assistive technologies like screen readers can struggle to determine which label belongs to which input.

You could solve this by passing a unique id to every component instance, but that also means every developer using your component now has one more thing to remember. Forget it once or accidentally reuse the same value, and the problem returns.

Ideally, a reusable component should handle this for you. The question is: how do you generate unique IDs automatically without making your component harder to use?

That’s exactly the challenge we’ll solve next.

Why Common Solutions Fall Short

Once you’ve decided that hardcoding an id isn’t a good fit for a reusable component, the next step seems obvious: generate one automatically.

A common approach is to use Math.random().

<script setup>
const inputId = Math.random().toString(36).slice(2)
</script>

<template>
  <label :for="inputId">Email Address</label>
  <input :id="inputId" />
</template>

Another option is to use a UUID library.

<script setup>
import { v4 as uuid } from 'uuid'

const inputId = uuid()
</script>

Both approaches solve the duplicate ID problem because every component instance receives a different identifier. If your application only runs in the browser, this may be all you need.

However, modern Vue applications don’t always render exclusively on the client. Frameworks like Nuxt often render components on the server first and then hydrate them in the browser. For hydration to work correctly, the HTML generated on the server must match what Vue expects on the client.

Random ID generators can’t guarantee that consistency. Each render produces a different value, so the server and client may generate different IDs for the same component. That’s where hydration issues can begin.

The goal isn’t just to generate unique IDs—it’s to generate IDs that are both unique and predictable across different rendering environments.

That’s exactly the problem useId was designed to solve.

Understanding useId

Now that we’ve seen why manually managing IDs doesn’t scale well, let’s look at Vue’s solution.

useId is a Composition API utility that generates a unique, stable ID for the current component instance. Instead of creating IDs yourself, you simply ask Vue for one and use it wherever your component needs an identifier.

Here’s the basic syntax:

<script setup>
import { useId } from 'vue'

const inputId = useId()
</script>

<template>
  <label :for="inputId">
    Email Address
  </label>

  <input :id="inputId" />
</template>

Every time a new instance of this component is rendered, Vue generates a different ID. That means you can render the component as many times as you like without worrying about duplicate IDs.

What makes useId different from Math.random() or a UUID generator isn’t just that the IDs are unique—it’s that Vue controls how they’re generated. Because the framework manages the rendering process, it can produce IDs that remain consistent across different rendering environments, making them OK in reusable components and applications that use server-side rendering.

It’s also worth noting what useId is not designed for.

The value it returns is meant for the DOM. Use it to connect labels with inputs, associate helper text with form fields or create relationships between HTML elements. It isn’t intended to replace database IDs, API identifiers or v-for keys.

Think of useId as a small utility with a very specific responsibility: helping your components generate reliable IDs automatically so you don’t have to manage them yourself.

Now that we understand what it does, let’s see how it fits into a real component you’ll likely build in almost every application: a reusable input component.

Practical Example: Building a Reusable Input Component

Let’s put useId into practice by building a reusable input component.

We’ll start with a simple version that accepts a label and renders an input field:

<script setup>
defineProps({
  label: String,
})
</script>

<template>
  <label for="input">
    {{ label }}
  </label>

  <input id="input" />
</template>

At first, this component looks perfectly fine. If you render it once, the label is correctly associated with the input.

The problem appears when the component is reused.

<BaseInput label="First Name" />
<BaseInput label="Last Name" />
<BaseInput label="Email Address" />

Because every instance renders the same id, all three inputs end up sharing ”input” as their identifier. That’s exactly the issue we discussed earlier.

Instead of asking every developer to provide a unique id, we can let the component generate one automatically with useId

<script setup>
import { useId } from 'vue'

defineProps({
  label: String,
})

const inputId = useId()
</script>

<template>
  <label :for="inputId">
    {{ label }}
  </label>

  <input :id="inputId" />
</template>

Now every instance of BaseInput receives its own unique identifier. The component is just as easy to use, but you no longer have to generate or manage IDs yourself.

Taking It a Step Further

Most reusable input components need more than just a label. It’s common to display helper text that provides additional guidance to the user.

For example, you might want to explain the password requirements.

Password
[_______________]

Must contain at least 8 characters.

To make this accessible, the helper text should be associated with the input using the aria-describedby attribute. This allows screen readers to announce the extra information when input receives focus.

With useId, creating that relationship is straightforward:

<script setup>
import { useId } from 'vue'

defineProps({
  label: String,
  helperText: String,
})

const inputId = useId()
const helperTextId = useId()
</script>

<template>
  <label :for="inputId">
    {{ label }}
  </label>

  <input
    :id="inputId"
    :aria-describedby="helperTextId"
  />

  <small :id="helperTextId">
    {{ helperText }}
  </small>
</template>

Here, each element has a clear purpose:

  • inputId connects the label and input.
  • helperTextId connects the helper text to the input.
  • Every component instance automatically gets its own set of IDs.

The result is a component that’s easier to reuse, more accessible, and far less prone to subtle bugs caused by duplicate IDs.

And the best part is that none of this complexity is exposed to the developer using your component.

<BaseInput
  label="Password"
  helper-text="Must contain at least 8 characters."
/>

From the outside, the component has a clean and simple API. Internally, it handles generating the IDs needed to keep everything correctly connected.

That’s one of the biggest advantages of useId. It allows your components to manage an implementation detail that consumers shouldn’t have to think about.

Why useId Is SSR-Safe

Earlier, we briefly mentioned that random ID generators can cause problems in applications that use server-side rendering (SSR). Now that you’ve seen useId in action, it’s easier to understand why.

When an application uses SSR, Vue renders your components on the server to generate the initial HTML. That HTML is then sent to the browser, where Vue “hydrates” it by attaching interactivity.

For hydration to succeed, the HTML generated on the server must match what Vue expects to render on the client. Random ID generators don’t guarantee that.

Imagine this component uses Math.random() to create an ID. The server might generate:

<input id="a8f3k2">

When the browser renders the same component during hydration, it could generate:

<input id="x9m7p1">

The component hasn’t changed, but the IDs no longer match. Vue now has to reconcile the difference, which can lead to hydration warnings or unnecessary DOM updates.

useId avoids this problem by predictably generating IDs. Because Vue controls the rendering process, it can ensure the same component receives the same ID during both the server render and client hydration.

The important takeaway isn’t that you need to understand SSR internals. It’s that useId lets you build reusable components without worrying about whether they’ll continue to work correctly in applications that use server-side rendering.

When to Use (and Avoid) useId

Like most Vue utilities, useId solves a specific problem. It’s not something you’ll use in every component, but when you need it, it removes a surprising amount of manual work.

A good rule of thumb is this: if your component needs to create a relationship between DOM elements using an id, useId is usually the right tool.

Some common examples include:

  • Associating a <label> with an <input>
  • Connecting helper text or error messages using aria-describedby
  • Linking elements with accessibility attributes such as aria-describedby
  • Building reusable form components that may be rendered multiple times on the same page

On the other hand, useId isn’t intended for application or business data. For example, you shouldn’t use it for:

  • Database record IDs
  • Order or invoice numbers
  • API resource identifiers
  • v-for keys
  • Anything that needs to be stored or shared outside the component

Those values represent your application’s data, while useId exists purely to generate stable IDs for the DOM.

If you’re unsure whether to use it, ask yourself a simple question:

“Am I identifying application data, or am I connecting HTML elements?”

If it’s about connecting elements in the DOM, useId is likely the right choice. If it’s about identifying business data, you should use an identifier that’s designed for that purpose.

Best Practices for useId

Before you start using useId throughout your application, keep these practical guidelines in mind:

  • Use useId only when your component needs a DOM ID. It’s ideal for linking labels, inputs, helper text and other accessibility attributes.
  • Generate IDs inside the component. Let the component handle its own implementation details instead of expecting every consumer to provide unique IDs.
  • Reuse the same ID for related elements. A label and its corresponding input should reference the same identifier. Generate additional IDs only when you’re creating a different relationship, such as linking helper text with aria-describedby.
  • Don’t use useId for application data. Database records, API resources and v-for keys all have different requirements and should use identifiers designed for those purposes.
  • Keep accessibility in mind. useId isn’t just about avoiding duplicate IDs—it’s also a simple way to build components that are easier for everyone to use.

These practices will help you write components that are easier to reuse, easier to maintain and ready for more complex applications as your project grows.

Wrapping Up

At first glance, useId seems like a small utility. It generates an ID, and that’s about it. But once you start building reusable components, you quickly realize it’s solving a much bigger problem than simply avoiding duplicate IDs. It removes a repetitive implementation detail and helps to preserve accessibility and consistency—even in applications that use server-side rendering.

More importantly, useId reflects a broader idea you’ll see throughout Vue: the framework handles common problems so you can focus on building features instead of reinventing infrastructure.

The next time you find yourself hardcoding an id inside a reusable component, take a moment to ask whether the component should be responsible for generating it instead. In many cases, that’s exactly what useId was designed for.


david-adeneye-sq
About the Author

David Adeneye Abiodun

David Adeneye is a software developer and a technical writer passionate about making the web accessible for everyone. When he is not writing code or creating technical content, he spends time researching about how to design and develop good software products.

Related Posts

Comments

Comments are disabled in preview mode.