CSS ID and Class Selectors

Class and ID selectors are the two most important tools you have for targeting specific elements in CSS, rather than every element of a given type. A class selector (written with a leading dot, like .card) lets you apply the same styling to many elements at once, while an ID selector (written with a leading hash, like #header) targets exactly one, uniquely identified element on the page. Understanding how these two selectors work — and how they compete with each other through specificity — is essential to writing CSS that behaves predictably as a project grows.

Overview / How it works

Every HTML element can carry a class attribute and an id attribute. These attributes don’t do anything visually by themselves — they exist purely so that CSS (or JavaScript) can reference the element. When the browser’s rendering engine parses your stylesheet, it builds a list of rules, each made up of a selector and a declaration block. During the layout and paint phase, the engine walks the DOM tree and, for each element, figures out which rules match it based on its tag name, class list, ID, attributes, and position in the tree.

A class selector matches any element whose class attribute contains that class name as one of possibly several space-separated tokens. This is why one element can have multiple classes — class=\"card highlight\" — and be matched by both .card and .highlight rules simultaneously. An ID selector matches an element whose id attribute equals that exact value. IDs are meant to be unique within a document; while browsers won’t crash if you duplicate an ID, an ID selector will match every element that happens to share it, and tools like document.getElementById() in JavaScript will only ever find the first one. That mismatch between what CSS does and what JavaScript expects is a common source of bugs.

Behind the scenes, when the browser resolves which rule ‘wins’ for a given element and property, it uses a scoring system called specificity. Class selectors and ID selectors sit at different tiers of this scoring system, which is why ID rules are much harder to override than class rules — more on that in the ‘Under the hood’ section below.

Syntax

The general form of each selector is straightforward:

Selector Matches Example
.classname Any element with classname in its class attribute .card { }
#idname The element whose id attribute equals idname #header { }
.class1.class2 An element that has both classes (no space between them) .btn.btn-primary { }
tag.classname Only elements of that tag with the class button.btn { }
  • Dot prefix (.) — required for every class selector; omitting it turns the selector into a (usually wrong) element type selector.
  • Hash prefix (#) — required for every ID selector.
  • Class names and ID values — can contain letters, digits, hyphens, and underscores, but must not start with a digit, and are case-sensitive in HTML documents.
  • Chaining — writing two class selectors back-to-back with no space (.a.b) means ‘has both classes’; a space (.a .b) means ‘a .b descendant of an .a‘ — a completely different, and very easy to mistype, selector.

Examples

Example 1: Styling a reusable component with a class

Applied to two elements: <div class=\"card\">...</div> and <div class=\"card card-highlight\">...</div>.

.card {\n  background-color: #ffffff;\n  border: 1px solid #d0d0d0;\n  border-radius: 8px;\n  padding: 16px;\n}\n\n.card-highlight {\n  border-color: #ff9800;\n  box-shadow: 0 0 0 2px rgba(255, 152, 0, 0.3);\n}

Result: Both <div> elements get a white background, light gray border, rounded corners, and internal padding from .card. The second element additionally picks up an orange border color and a soft orange glow from .card-highlight, because it carries both classes at once.

This is the core strength of class selectors: the same visual ‘recipe’ can be reused on any number of elements, and you can layer additional classes on top to create variations without duplicating styles.

Example 2: Targeting a unique page landmark with an ID

Applied to: <header id=\"site-header\">...</header>.

#site-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 12px 24px;\n  background-color: #1f2937;\n  color: #ffffff;\n}

Result: The single <header> element becomes a flex container with its children spread to opposite ends and vertically centered, on a dark slate background with white text. Because id values are meant to be unique, this rule is written with the expectation that it applies to exactly one element on the page — a natural fit for one-off structural landmarks like a header, footer, or main navigation bar.

Example 3: Combining classes and an ID, and seeing specificity in action

Applied to: <button class=\"btn btn-primary\" id=\"cta-button\">Sign up</button>.

.btn {\n  display: inline-block;\n  padding: 8px 16px;\n  border-radius: 4px;\n  border: none;\n  font-weight: 600;\n  cursor: pointer;\n}\n\n.btn.btn-primary {\n  background-color: #2563eb;\n  color: #ffffff;\n}\n\n#cta-button {\n  padding: 12px 24px;\n  font-size: 1.125rem;\n}

Result: The button is rendered as an inline-block with rounded corners, bold text, and a pointer cursor from .btn; it becomes blue with white text from .btn.btn-primary; and even though .btn already set padding, the #cta-button rule overrides it with larger padding and a bigger font size, because the ID selector outscores the class selectors regardless of source order.

How it works step by step / Under the hood

Every selector is scored with a specificity value made of three numbers, conventionally written as (IDs, classes, elements). The browser calculates this for each matching rule:

  • ID selectors contribute 1 to the first number: #cta-button scores (1, 0, 0).
  • Class selectors (and attribute selectors and pseudo-classes) contribute 1 to the second number: .btn-primary scores (0, 1, 0).
  • Type/tag selectors (and pseudo-elements) contribute 1 to the third number: button scores (0, 0, 1).

Combined selectors add their parts together: .btn.btn-primary scores (0, 2, 0) because it has two classes. When two rules apply to the same element and set the same property, the browser compares these triples left to right — any ID beats any number of classes, and any class beats any number of type selectors. Only when specificity is exactly tied does the browser fall back to source order, letting the later rule in the stylesheet win. This is precisely why, in Example 3, #cta-button‘s padding beats .btn‘s padding even though .btn appears first and would otherwise seem to have the ‘last word’ visually in your source file — specificity is checked before source order, not instead of it.

Common Mistakes

Mistake 1: Forgetting the selector prefix

It’s easy to drop the leading dot or hash, especially when refactoring. Without it, the selector silently becomes a type selector, matching a nonexistent HTML tag instead of your class:

card {\n  background-color: #ffffff;\n  border-radius: 8px;\n}

This rule targets an element literally named <card>, which doesn’t exist in standard HTML, so it matches nothing and silently does nothing — no error, just a style that never applies. The fix is to add the dot so it targets the class instead:

.card {\n  background-color: #ffffff;\n  border-radius: 8px;\n}

Mistake 2: Styling with IDs and then fighting specificity with !important

Because ID selectors are so specific, teams that style directly on IDs often find themselves unable to override a later rule without resorting to !important, which then cascades into more !important declarations elsewhere:

#main-nav a {\n  color: #333333;\n}\n\n.nav-link-active {\n  color: #2563eb !important;\n}

Here, .nav-link-active only wins because of !important, since (0, 1, 0) is naturally weaker than #main-nav a‘s (1, 0, 1). Once !important is in play, the next developer who needs to override this rule has no clean option left. The more maintainable fix is to avoid styling directly on the ID and use classes at comparable specificity on both sides:

.nav-link {\n  color: #333333;\n}\n\n.nav-link-active {\n  color: #2563eb;\n}

Best Practices

  • Prefer classes for anything you style — they’re reusable, composable, and have low, predictable specificity.
  • Reserve IDs for truly unique elements and prefer using them for JavaScript hooks, fragment links (#section-two), and form label associations rather than for visual styling.
  • Avoid chaining an ID with descendant selectors for styling (like #sidebar .title) since it makes overriding later next to impossible without another ID or !important.
  • Keep class names semantic and reusable (.card, .btn-primary) rather than presentational and one-off (.blue-box-14px).
  • Use multiple classes to compose variations (class=\"btn btn-primary btn-large\") instead of writing a new class for every combination.
  • Never rely on duplicate id values in a document — even if a browser renders it ‘fine’, it breaks accessibility tools, JavaScript lookups, and fragment navigation.

Practice Exercises

Exercise 1: Write a class named .alert that gives an element a yellow background, dark text, and 12px of padding. Then write a second class, .alert-error, that only changes the background to red when combined with .alert on the same element.

Exercise 2: Given an element <section id=\"pricing\"> and a class .section shared by several sections on the page, write CSS so that #pricing gets a distinct background color while all sections (including #pricing) share the same padding and max-width from .section. Explain in a sentence why the background color rule needs to be an ID rule and not just placed after .section in the file.

Exercise 3: Two rules, .card.featured and #promo-card, both set a border-color on the same element. Without running any code, work out which one wins by calculating each selector’s specificity triple, and explain your reasoning.

Summary

  • Class selectors (.name) match any element carrying that class and are ideal for reusable, composable styling.
  • ID selectors (#name) match a single, uniquely identified element and score much higher in specificity.
  • Specificity is compared as an (IDs, classes, elements) triple, left to right; ties fall back to source order.
  • Styling directly on IDs tends to create specificity problems that push teams toward !important — prefer classes for visual styling.
  • Chaining classes (.a.b) means ‘has both classes on one element’; a space (.a .b) means a completely different descendant relationship.