An id and a class can both select an element, which is why they look interchangeable in a tiny CSS demo. They are not. An ID is a document-wide identity that other features can point to; a class is reusable membership that describes a role, state, or styling hook.

id and class side by side

  • An id value must be unique among all elements in the document.

  • One element has one id attribute value; it is not a whitespace-separated list of IDs.

  • A class attribute is a whitespace-separated set of tokens.

  • The same class token can appear on any number of elements.

  • An element may have both an ID and multiple classes.

  • In CSS, #profile selects an ID and .card selects a class.

  • IDs participate in fragment navigation and reference relationships; classes do not.

Cardinality is the useful distinction

  • Ask whether other markup needs to identify exactly one element or a group of elements.

  • Do not choose ID merely because the element happens to appear once in today’s design.

  • Do not choose class for a relationship whose syntax explicitly requires an ID reference.

  • An element can have both when it has a unique reference role and reusable component styling.

A valid example with both

index.htmlhtml
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Account cards</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main>
    <article id="primary-account" class="account-card account-card--featured">
      <h2>Primary account</h2>
      <p class="account-card__status is-active">Active</p>
    </article>
 
    <article class="account-card">
      <h2>Backup account</h2>
      <p class="account-card__status">Paused</p>
    </article>
  </main>
</body>
</html>

The attributes express different relationships

  • primary-account identifies one article within this document.

  • Both articles share account-card, so one component rule can style them.

  • account-card--featured is a reusable variant token, even if only one element currently uses it.

  • account-card__status describes a component part; is-active describes state.

  • The document has one logical page title and semantic landmarks independent of ID/class styling.

styles.csscss
.account-card {
  padding: 1rem;
  border: 1px solid #b8c2cc;
  border-radius: 0.5rem;
}
 
.account-card--featured {
  border-color: #2563eb;
}
 
.account-card__status {
  color: #5b6470;
}
 
.account-card__status.is-active {
  color: #14733b;
  font-weight: 700;
}

Classes keep component CSS composable

  • A class selector begins with ., but the HTML attribute stores only the token without the dot.

  • .account-card__status.is-active matches an element carrying both class tokens.

  • The rules do not depend on one page-specific identifier, so the component can be reused.

  • Names are conventions, not browser keywords; choose a consistent project naming system.

  • Visual state must not be conveyed by color alone; visible text communicates Active and Paused here.

IDs connect document features

documentation.htmlhtml
<nav aria-label="On this page">
  <a href="#installation">Installation</a>
</nav>
 
<section aria-labelledby="installation">
  <h2 id="installation">Installation</h2>
  <p>Install the package from your approved registry.</p>
</section>

One identifier serves two reference systems

  • The URL fragment #installation targets the element whose ID is installation.

  • The browser can scroll to the target and expose it as :target in CSS.

  • aria-labelledby="installation" uses an ID reference to give the section an accessible name.

  • Duplicate IDs make references ambiguous and can cause assistive technology to associate the wrong content.

  • Use stable, readable IDs for deep links because changing them breaks saved and inbound fragment URLs.

Labels and form controls

profile-form.htmlhtml
<div class="form-field">
  <label for="display-name">Display name</label>
  <input id="display-name" class="text-input" name="display_name" type="text">
</div>

The ID is functional, the classes are reusable

  • for="display-name" references the input ID, making the visible label activate/focus that control.

  • The name attribute—not the ID—is the key normally submitted with form data.

  • form-field and text-input can style many controls without creating unique rules.

  • Generate unique control IDs when rendering repeated form rows.

  • A wrapping <label> is another valid association pattern, but explicit for/id remains useful in many layouts.

JavaScript selection is singular versus plural by intent

account-cards.jsjavascript
const primaryAccount = document.getElementById("primary-account");
const cards = document.querySelectorAll(".account-card");
 
primaryAccount?.setAttribute("data-loaded", "true");
 
for (const card of cards) {
  card.classList.add("is-enhanced");
}

Choose an API that matches cardinality

  • getElementById() returns the element with that document ID or null.

  • Optional chaining avoids dereferencing null; missing required markup may instead deserve an explicit error.

  • querySelectorAll() returns a static NodeList containing every class match at query time.

  • classList.add() changes a class token without reparsing the whole class string.

  • Class selectors are ideal for event delegation or repeated components; data attributes can carry behavior-specific configuration.

CSS specificity is not a reason to style everything by ID

specificity.csscss
#checkout {
  background: red;
}
 
.panel.is-ready {
  background: green;
}

The ID rule wins despite fewer selectors

  • An ID selector contributes to the ID specificity column, commonly written 1-0-0.

  • Two class selectors contribute 0-2-0, which still loses to 1-0-0.

  • High-specificity ID rules are harder to override in component variants and responsive contexts.

  • Source order breaks ties only when competing declarations have equal origin, importance, layer, and specificity.

  • Cascade layers and low-specificity helpers such as :where() can improve architecture, but they do not change the semantic uniqueness of IDs.

A maintainable override strategy

  • Keep component selectors class-based and intentionally shallow.

  • Order cascade layers and component variants rather than escalating selector weight.

  • Reserve !important for narrowly defined architecture cases, not routine specificity fights.

  • Test variants in their real page context because inherited and competing styles may differ from an isolated demo.

ID syntax has two practical layers

HTML requires an ID to contain at least one character and no ASCII whitespace. Other characters can be valid in HTML but awkward in CSS or JavaScript selectors because CSS identifier syntax may require escaping. A predictable convention such as letters, digits after the first character, hyphens, and underscores avoids needless escaping.

escaped-selector.jsjavascript
const rawId = "invoice:2026.08";
const element = document.querySelector(`#${CSS.escape(rawId)}`);

getElementById avoids selector escaping

  • CSS.escape() converts an arbitrary ID value into a safe CSS identifier fragment.

  • Template literals assemble the escaped #id selector.

  • document.getElementById(rawId) is simpler when only an ID lookup is needed.

  • Never interpolate untrusted strings into selectors without understanding escaping and the intended lookup scope.

  • Choosing selector-friendly IDs at authoring time keeps URLs, tests, styles, and scripts readable.

Multiple classes are one attribute value

button.htmlhtml
<button class="button button--primary is-loading" type="button">
  Save changes
</button>

Whitespace separates class tokens

  • The element has one class attribute containing three tokens.

  • Do not repeat the class attribute; combine tokens into one attribute.

  • Order of tokens in HTML does not determine which CSS declaration wins.

  • Classes can represent component, variant, and state, but state changes should also update accessible semantics such as disabled or aria-busy when appropriate.

  • Avoid class names tied only to appearance, such as red-text, when they actually represent domain state.

Invalid patterns and their repairs

  • Two `id` attributes on one element: invalid markup; keep one unique ID.

  • `id="one two"`: contains whitespace and is not two IDs; use two class tokens or redesign the references.

  • The same ID on two elements: assign distinct IDs and share a class where common styling/behavior is needed.

  • Every CSS rule uses IDs: move reusable component styling to classes to reduce specificity pressure.

  • A class is used as a form label target: for, ARIA ID references, and fragments require IDs, not class selectors.

  • JavaScript assumes an element exists: handle null or fail explicitly with a useful diagnostic.

  • Dynamic UI creates duplicates: include a stable unique key in generated IDs and validate rendered output.

Repeated components need page-level validation

  • Render several component instances together, not only one Storybook/example instance.

  • Check server-rendered and hydrated markup for the same identifier scheme.

  • Exercise conditional branches that may render two dialogs, forms, or navigation regions simultaneously.

  • Verify every for, aria-labelledby, aria-describedby, and fragment reference resolves to its intended unique target.

How to choose in real projects

  • Use an ID for a deep-link target, a label/control association, an ARIA ID reference, or a genuinely unique DOM lookup.

  • Use a class for component styling, repeated elements, variants, state hooks, and selecting groups.

  • Use data-* attributes for application-specific data or behavior hooks when a class would misleadingly imply styling.

  • Use semantic elements and native attributes before adding either; an ID/class does not replace headings, buttons, labels, or landmarks.

  • Keep IDs unique across server rendering, client hydration, templates, and repeated components.

  • Treat public fragment IDs as part of URL compatibility.

Validate instead of trusting the browser

Project directorybash
npx --yes html-validate index.html
✔ 1 file(s) linted successfully

Use a reviewed validator in the project toolchain

  • The example invokes a package through npm; review and pin the tool/version in a real project instead of relying on an unpinned network install.

  • A validator can catch duplicate attributes and many duplicate-ID cases before deployment.

  • Component-level validation may miss duplicates created only when several rendered components share a page.

  • Add browser and accessibility tests for fragment focus, form labels, and ARIA relationships.

  • Valid HTML is the baseline, not proof of usable interaction or accessible design.

A compact review checklist

  • Every non-empty ID is unique in the rendered document.

  • IDs contain no ASCII whitespace and follow a selector-friendly convention.

  • Repeated components share classes rather than IDs.

  • CSS avoids unnecessary ID specificity.

  • Form for values and ARIA ID references resolve to the intended elements.

  • Fragment targets are stable, meaningful, and tested with fixed/sticky headers.

  • JavaScript selection matches expected cardinality and handles missing elements.

  • Dynamic templates cannot generate collisions.

Standards and references