Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Ensure all IDs are unique

All ID attributes are unique within the document. No duplicate IDs exist on the page.

Utilities
Quick take
Typical fix time 10 min
  • Each ID must appear only once per HTML document
  • Use classes for styling, IDs for unique anchors/references
  • Check for duplicates introduced by component libraries
  • Duplicate IDs break form labels, ARIA, and getElementById
Why it matters: Duplicate IDs break form accessibility (labels don't connect), ARIA relationships fail, and JavaScript getElementById returns wrong elements—causing silent bugs.

Rule Details

ID attributes must be unique within an HTML document to ensure proper functionality of forms, accessibility features, and JavaScript interactions.

Code Example

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Unique IDs Example</title>
</head>
<body>
    <header id="main-header">
        <h1>Site Title</h1>
    </header>
 
    <nav id="main-navigation">
        <ul>
            <li><a href="#section-1">Section 1</a></li>
            <li><a href="#section-2">Section 2</a></li>
        </ul>
    </nav>
 
    <main>
        <section id="section-1">
            <h2>First Section</h2>
        </section>
 
        <section id="section-2">
            <h2>Second Section</h2>
        </section>
    </main>
</body>
</html>

Why It Matters

Duplicate IDs break form accessibility (labels don't connect), ARIA relationships fail, and JavaScript getElementById returns wrong elements—causing silent bugs.

Form Labels and IDs

<form>
    <div>
        <label for="first-name">First Name</label>
        <input type="text" id="first-name" name="firstName" required>
    </div>
 
    <div>
        <label for="last-name">Last Name</label>
        <input type="text" id="last-name" name="lastName" required>
    </div>
 
    <div>
        <label for="email-address">Email</label>
        <input type="email" id="email-address" name="email" required>
    </div>
 
    <div>
        <label for="message-text">Message</label>
        <textarea id="message-text" name="message" rows="4"></textarea>
    </div>
</form>

ARIA and Accessibility IDs

<section aria-labelledby="products-heading">
    <h2 id="products-heading">Our Products</h2>
 
    <div role="tablist" aria-labelledby="products-heading">
        <button role="tab"
                aria-controls="electronics-panel"
                aria-selected="true"
                id="electronics-tab">
            Electronics
        </button>
 
        <button role="tab"
                aria-controls="clothing-panel"
                aria-selected="false"
                id="clothing-tab">
            Clothing
        </button>
    </div>
 
    <div role="tabpanel"
         aria-labelledby="electronics-tab"
         id="electronics-panel">
        <p>Electronics content...</p>
    </div>
 
    <div role="tabpanel"
         aria-labelledby="clothing-tab"
         id="clothing-panel"
         hidden>
        <p>Clothing content...</p>
    </div>
</section>
 
<!-- Modal with proper ID relationships -->
<button aria-controls="user-menu" aria-expanded="false" id="user-menu-button">
    User Menu
</button>
 
<ul id="user-menu" role="menu" aria-labelledby="user-menu-button" hidden>
    <li role="menuitem"><a href="/profile">Profile</a></li>
    <li role="menuitem"><a href="/settings">Settings</a></li>
    <li role="menuitem"><a href="/logout">Logout</a></li>
</ul>

Framework Examples

Next.js with Unique IDs

import { useId } from 'react'
 
function ContactForm() {
  // Generate unique IDs for this component instance
  const formId = useId()
  const nameId = `${formId}-name`
  const emailId = `${formId}-email`
  const messageId = `${formId}-message`
 
  return (
    <form id={formId}>
      <div>
        <label htmlFor={nameId}>Name</label>
        <input type="text" id={nameId} name="name" />
      </div>
 
      <div>
        <label htmlFor={emailId}>Email</label>
        <input type="email" id={emailId} name="email" />
      </div>
 
      <div>
        <label htmlFor={messageId}>Message</label>
        <textarea id={messageId} name="message" />
      </div>
    </form>
  )
}
 
// Multiple instances won't have ID conflicts
export default function ContactPage() {
  return (
    <div>
      <ContactForm /> {/* IDs: :r1:-name, :r1:-email, :r1:-message */}
      <ContactForm /> {/* IDs: :r2:-name, :r2:-email, :r2:-message */}
    </div>
  )
}

React with Custom Hook for IDs

import { useRef } from 'react'
 
// Custom hook for generating unique IDs
function useUniqueId(prefix = 'id') {
  const idRef = useRef()
 
  if (!idRef.current) {
    idRef.current = `${prefix}-${Math.random().toString(36).substr(2, 9)}`
  }
 
  return idRef.current
}
 
function FormField({ label, type = 'text', name, ...props }) {
  const fieldId = useUniqueId(`field-${name}`)
 
  return (
    <div className="form-field">
      <label htmlFor={fieldId}>{label}</label>
      <input type={type} id={fieldId} name={name} {...props} />
    </div>
  )
}
 
// Usage ensures unique IDs
function UserForm() {
  return (
    <form>
      <FormField label="Username" name="username" />
      <FormField label="Email" name="email" type="email" />
      <FormField label="Password" name="password" type="password" />
    </form>
  )
}

Vue.js with Unique ID Generation

<template>
  <form>
    <div v-for="field in formFields" :key="field.name">
      <label :for="getFieldId(field.name)">{{ field.label }}</label>
      <input
        :type="field.type"
        :id="getFieldId(field.name)"
        :name="field.name"
        v-model="formData[field.name]"
      />
    </div>
  </form>
</template>
 
<script>
export default {
  data() {
    return {
      componentId: `form-${Math.random().toString(36).substr(2, 9)}`,
      formFields: [
        { name: 'firstName', label: 'First Name', type: 'text' },
        { name: 'lastName', label: 'Last Name', type: 'text' },
        { name: 'email', label: 'Email', type: 'email' }
      ],
      formData: {}
    }
  },
  methods: {
    getFieldId(fieldName) {
      return `${this.componentId}-${fieldName}`
    }
  }
}
</script>

Vue.js Composition API

<template>
  <div>
    <h2 :id="headingId">Product Reviews</h2>
    <div role="tablist" :aria-labelledby="headingId">
      <button
        v-for="tab in tabs"
        :key="tab.id"
        :id="getTabId(tab.id)"
        :aria-controls="getPanelId(tab.id)"
        :aria-selected="activeTab === tab.id"
        @click="activeTab = tab.id"
      >
        {{ tab.label }}
      </button>
    </div>
 
    <div
      v-for="tab in tabs"
      :key="tab.id"
      :id="getPanelId(tab.id)"
      :aria-labelledby="getTabId(tab.id)"
      :hidden="activeTab !== tab.id"
    >
      {{ tab.content }}
    </div>
  </div>
</template>
 
<script setup>
import { ref, computed } from 'vue'
 
const componentId = `reviews-${Math.random().toString(36).substr(2, 9)}`
const headingId = `${componentId}-heading`
const activeTab = ref('recent')
 
const tabs = [
  { id: 'recent', label: 'Recent Reviews', content: 'Recent reviews content...' },
  { id: 'helpful', label: 'Most Helpful', content: 'Helpful reviews content...' },
  { id: 'critical', label: 'Critical Reviews', content: 'Critical reviews content...' }
]
 
const getTabId = (tabId) => `${componentId}-tab-${tabId}`
const getPanelId = (tabId) => `${componentId}-panel-${tabId}`
</script>

JavaScript and ID Management

Dynamic Content with Unique IDs

class ComponentManager {
  constructor() {
    this.componentCounter = 0
  }
 
  generateUniqueId(prefix = 'component') {
    return `${prefix}-${++this.componentCounter}-${Date.now()}`
  }
 
  createFormField(label, type = 'text', name) {
    const fieldId = this.generateUniqueId('field')
 
    const container = document.createElement('div')
    container.className = 'form-field'
 
    const labelEl = document.createElement('label')
    labelEl.htmlFor = fieldId
    labelEl.textContent = label
 
    const input = document.createElement('input')
    input.type = type
    input.id = fieldId
    input.name = name
 
    container.appendChild(labelEl)
    container.appendChild(input)
 
    return { container, input, label: labelEl }
  }
 
  createModal(title, content) {
    const modalId = this.generateUniqueId('modal')
    const headingId = this.generateUniqueId('modal-heading')
    const closeButtonId = this.generateUniqueId('modal-close')
 
    const modal = document.createElement('div')
    modal.id = modalId
    modal.setAttribute('role', 'dialog')
    modal.setAttribute('aria-labelledby', headingId)
    modal.setAttribute('aria-modal', 'true')
 
    modal.innerHTML = `
      <div class="modal-content">
        <header>
          <h2 id="${headingId}">${title}</h2>
          <button id="${closeButtonId}" aria-label="Close modal">&times;</button>
        </header>
        <div class="modal-body">
          ${content}
        </div>
      </div>
    `
 
    // Add close functionality
    modal.querySelector(`#${closeButtonId}`).addEventListener('click', () => {
      modal.remove()
    })
 
    return modal
  }
}
 
// Usage
const manager = new ComponentManager()
 
// Create multiple forms without ID conflicts
const userForm = manager.createFormField('Username', 'text', 'username')
const emailForm = manager.createFormField('Email', 'email', 'email')
 
document.body.appendChild(userForm.container)
document.body.appendChild(emailForm.container)

ID Validation and Management

class IDManager {
  constructor() {
    this.usedIds = new Set()
  }
 
  isIdUnique(id) {
    return !this.usedIds.has(id) && !document.getElementById(id)
  }
 
  registerID(id) {
    if (!this.isIdUnique(id)) {
      throw new Error(`ID "${id}" is already in use`)
    }
    this.usedIds.add(id)
    return id
  }
 
  generateUniqueId(prefix = 'auto') {
    let counter = 1
    let id = `${prefix}-${counter}`
 
    while (!this.isIdUnique(id)) {
      counter++
      id = `${prefix}-${counter}`
    }
 
    this.registerID(id)
    return id
  }
 
  removeID(id) {
    this.usedIds.delete(id)
  }
 
  validatePage() {
    const elements = document.querySelectorAll('[id]')
    const foundIds = new Set()
    const duplicates = []
 
    elements.forEach(element => {
      const id = element.id
      if (foundIds.has(id)) {
        duplicates.push(id)
      } else {
        foundIds.add(id)
      }
    })
 
    return {
      valid: duplicates.length === 0,
      duplicates,
      totalElements: elements.length,
      uniqueIds: foundIds.size
    }
  }
}
 
// Usage
const idManager = new IDManager()
 
// Safe ID generation
const uniqueId = idManager.generateUniqueId('my-component')
const element = document.createElement('div')
element.id = uniqueId
 
// Validate page
const validation = idManager.validatePage()
if (!validation.valid) {
  console.error('Duplicate IDs found:', validation.duplicates)
}

CSS and ID Selectors

/* Specific styling with IDs */
#main-header {
  background-color: #333;
  color: white;
  padding: 1rem;
}
 
#main-navigation ul {
  list-style: none;
  display: flex;
  gap: 1rem;
}
 
/* Form styling */
#contact-form {
  max-width: 600px;
  margin: 0 auto;
}
 
#contact-form label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: bold;
}
 
#contact-form input,
#contact-form textarea {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}
 
/* State-specific styling */
#user-menu[aria-expanded="true"] {
  display: block;
}
 
#user-menu[aria-expanded="false"] {
  display: none;
}

Common Issues and Solutions

❌ Duplicate IDs

<!-- Bad: Same ID used multiple times -->
<div id="content">First content</div>
<div id="content">Second content</div>
<script>
  // This will only select the first element
  const content = document.getElementById('content')
</script>

✅ Unique IDs with Descriptive Names

<!-- Good: Unique, descriptive IDs -->
<div id="main-content">Main page content</div>
<div id="sidebar-content">Sidebar content</div>
<script>
  const mainContent = document.getElementById('main-content')
  const sidebarContent = document.getElementById('sidebar-content')
</script>

❌ Generic or Unclear IDs

<!-- Bad: Not descriptive -->
<div id="div1">...</div>
<div id="box">...</div>
<input id="input1">

✅ Semantic and Clear IDs

<!-- Good: Clear purpose -->
<div id="product-gallery">...</div>
<div id="shopping-cart-summary">...</div>
<input id="search-query" type="search">

Tools and Validation

HTML Validators

<!-- Use W3C Markup Validator to check for duplicate IDs -->
<!-- https://validator.w3.org/ -->

Browser DevTools

// Check for duplicate IDs in console
function findDuplicateIds() {
  const ids = {}
  const duplicates = []
 
  document.querySelectorAll('[id]').forEach(element => {
    const id = element.id
    if (ids[id]) {
      if (ids[id] === 1) {
        duplicates.push(id)
      }
      ids[id]++
    } else {
      ids[id] = 1
    }
  })
 
  return duplicates
}
 
console.log('Duplicate IDs:', findDuplicateIds())

Automated Testing

// Jest test for unique IDs
describe('Page HTML validation', () => {
  test('all IDs should be unique', () => {
    const elements = document.querySelectorAll('[id]')
    const ids = Array.from(elements).map(el => el.id)
    const uniqueIds = [...new Set(ids)]
 
    expect(ids.length).toBe(uniqueIds.length)
  })
})

Best Practices

  1. Use descriptive names: user-profile-form instead of form1
  2. Follow naming conventions: kebab-case for HTML, camelCase for JavaScript
  3. Namespace related elements: modal-login, modal-login-title, modal-login-close
  4. Validate during development: Use linters and validators to catch duplicates
  5. Document ID usage: Comment complex ID relationships in code

Verification

Automated Checks

  • Inspect the final rendered HTML in the browser or page source to confirm the rule is satisfied.
  • Validate the affected markup with browser tooling or an HTML validator where appropriate.
  • Test one representative route or template that uses the pattern.
  • Re-check shared components that emit the same markup so the fix is consistent.

Manual Checks

  • Verify the rendered browser behavior manually on representative routes and supported browsers so the user-facing outcome matches the rule.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Scan this HTML document to ensure all ID attributes are unique across the entire page and properly reference related elements.

Fix

Auto-fix issues

Remove duplicate ID values, ensure each ID is used only once per page, and update any references like labels, ARIA attributes, or JavaScript selectors.

Explain

Learn more

Explain why unique IDs are essential for HTML validity, accessibility (form labels, ARIA), JavaScript functionality, and CSS styling.

Review

Code review

Review templates, server-rendered HTML, and shared components that output markup related to Ensure all IDs are unique. Flag exact elements, attributes, and routes where the rendered HTML violates the rule.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Nu Html Checker
validator.w3.orgTool

Rules that often go hand-in-hand with this one.

Use the HTML5 doctype

The HTML5 doctype declaration must appear as the first line of every HTML document to trigger standards mode rendering in all browsers.

HTML
Use navigation landmark regions

Page navigation uses nav elements with proper ARIA labels to distinguish multiple navigation regions.

Accessibility
Use unique IDs for active elements

All focusable or active elements must have a unique ID attribute.

Accessibility
Place list items within list containers

List item elements (li) must always be direct children of a list container (ul, ol, or menu) to maintain valid HTML structure and correct screen reader announcements.

Accessibility

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385