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
idvalue must be unique among all elements in the document.One element has one
idattribute value; it is not a whitespace-separated list of IDs.A
classattribute 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,
#profileselects an ID and.cardselects 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
<!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-accountidentifies one article within this document.Both articles share
account-card, so one component rule can style them.account-card--featuredis a reusable variant token, even if only one element currently uses it.account-card__statusdescribes a component part;is-activedescribes state.The document has one logical page title and semantic landmarks independent of ID/class styling.
.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-activematches 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
Fragment links and headings
<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
#installationtargets the element whose ID isinstallation.The browser can scroll to the target and expose it as
:targetin 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
<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
nameattribute—not the ID—is the key normally submitted with form data.form-fieldandtext-inputcan 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 explicitfor/idremains useful in many layouts.
JavaScript selection is singular versus plural by intent
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 ornull.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
#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 to1-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
!importantfor 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.
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
#idselector.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 class="button button--primary is-loading" type="button">
Save changes
</button>Whitespace separates class tokens
The element has one
classattribute containing three tokens.Do not repeat the
classattribute; 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
disabledoraria-busywhen 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
nullor 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
npx --yes html-validate index.html✔ 1 file(s) linted successfullyUse 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
forvalues 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
WHATWG global attributes: id defines uniqueness and ID value requirements.
WHATWG global attributes: class defines the set of space-separated class tokens.
Selectors Level 4 defines ID/class selectors and specificity.
MDN id gives practical fragment, CSS, and identifier guidance.
MDN class explains reusable class tokens and naming considerations.
WCAG parsing guidance provides accessibility context for unique IDs in applicable conformance targets.
Comments and corrections