Universal, Type, Class, and ID
Learn how universal, type, class, and ID selectors work in CSS. Understand specificity scores, why overusing ID selectors breaks your styles, and how to build clean, maintainable CSS.
Let me tell you something straight: if you get confused about why your CSS isn’t applying, nine times out of ten, it’s a selector issue. Either you’re targeting the wrong thing, or your selector is fighting with another one, and losing.
This lesson is all about the four most fundamental CSS selector types, the ones every single CSS rule you write is going to be built on. Before combinators, before pseudo-classes, before any of the advanced selector tricks, you need to understand these four. Really understand them, not just recognize the syntax.
We’re covering the universal selector, type selectors, class selectors, and ID selectors. By the end of this, you’ll know exactly how each one works, when to reach for each one, and why one of them (the ID selector) deserves a lot more caution than most beginners give it.
Let’s go through this properly.
01Connecting This to What We’ve Already Covered
Back in the CSS syntax lesson, we looked at the anatomy of a CSS rule: selector, property, value. We kept it light on selectors there because they deserve their own deep dive. This is that dive.
If you’ve gone through the CSS cascade lesson and the inheritance lesson, you already know that not all styles win when two rules conflict. A big part of that is specificity, and specificity is directly tied to which type of selector you use. We’ll touch on that here, and it’ll connect everything nicely.
If you haven’t read those yet, no problem, this lesson stands on its own. But keep specificity in the back of your mind as we go through each selector type. It becomes very relevant, especially with ID selectors.
02The Four CSS Selector Types
Here’s a visual overview before we go deep on each one. These four are the building blocks of all selector logic in CSS, and every more advanced selector you’ll ever learn is just a variation or combination of these fundamentals.
Universal
Targets every element on the page, no exceptions
Type
Targets all elements of a given HTML tag name
Class
Targets any element carrying a specific class name
ID
Targets one unique element with a specific ID attribute
Notice the specificity scores. They’re formatted as three numbers: (IDs, Classes, Types). The universal selector scores zero across the board. The ID selector scores a full point in the first column. That gap matters, and we’ll get to exactly why later in this lesson.
03The Universal Selector: One Symbol, Every Element
The universal selector is just an asterisk: *. That’s it. No tag name, no class, no ID, just a star that says “give me everything.”
When you write * { }, you’re writing a CSS rule that applies to literally every HTML element on the page, every <div>, every <p>, every <span>, every <button>, even the <html> and <body> elements themselves.
That sounds powerful, and it is, but it also sounds like it could be dangerous if misused. And you’d be right to think that. Let me show you how it’s actually used in practice.
The CSS Reset You’ll Write on Nearly Every Project
Different browsers apply their own default styles to HTML elements. Headings have default margins. Lists have default padding. Paragraphs have default spacing. These defaults vary slightly browser to browser, which means your page can look slightly different in Chrome vs Firefox vs Safari if you don’t reset them first.
The universal selector is the standard way to zero these out:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
These three lines are often the very first thing you’ll see in a stylesheet. They wipe out browser default margins and paddings across the board, and they set box-sizing to border-box, which changes how element dimensions are calculated.
The box-sizing Reset Deserves Its Own Mention
This one is genuinely important to understand. By default, CSS uses box-sizing: content-box, which means when you set width: 300px on an element, that 300px is just the content area. Any padding or border you add will expand the element beyond 300px.
With box-sizing: border-box, the 300px includes the padding and border. The element stays exactly 300px wide, no matter what padding you add inside it. This is so much more intuitive to work with, and using the universal selector to apply it globally is a rock-solid habit.
<!DOCTYPE html>
<html lang="en">
<head>
<style>
/* Content-box (default): width + padding = total rendered width */
.box-default {
width: 200px;
padding: 20px;
background: #1e293b;
color: #fff;
border: 2px solid #6366f1;
/* Actual rendered width: 200 + 40 + 4 = 244px */
}
/* Border-box: width already includes padding and border */
.box-border {
box-sizing: border-box;
width: 200px;
padding: 20px;
background: #1e293b;
color: #fff;
border: 2px solid #2ecc71;
/* Actual rendered width: exactly 200px */
}
</style>
</head>
<body>
<p class="box-default">content-box (wider than 200px)</p>
<br>
<p class="box-border">border-box (stays exactly 200px)</p>
</body>
</html>
One Thing to Keep in Mind With the Universal Selector
Some developers worry that * { } will slow down the browser because it applies to everything. In reality, with a small property list like a reset, this is completely fine on any modern machine. The browser is fast.
What you don’t want to do is write heavy, expensive styles on *, like complex box-shadow values or things that force the browser to recalculate layout on every element constantly. For simple resets? No problem. For everything else, be specific about what you’re targeting.
Real talk: The * { box-sizing: border-box; } reset is one of those things that will save you from a layout headache at least a few times per project. Get in the habit of including it early. You’ll notice when it’s missing.
04Type Selectors: Targeting Elements by Their Tag Name
A type selector, sometimes called an element selector, targets all HTML elements with a specific tag name. You write it exactly as the tag name, no dots, no hashes, nothing extra.
/* Targets every <p> element on the page */
p {
line-height: 1.75;
color: #cbd5e1;
}
/* Targets every <h1> element */
h1 {
font-size: 2.5rem;
font-weight: 700;
letter-spacing: -0.03em;
}
/* Targets every <a> element */
a {
color: #6366f1;
text-decoration: none;
}
These rules apply to every single instance of those tags anywhere on the page. Every paragraph, every h1, every link.
Where Type Selectors Are Perfect: Setting Base Styles
Type selectors shine for setting base typography styles across your whole document. When you want every paragraph to have a consistent line height, every heading to use a specific font, every link to have your brand color, type selectors are exactly right for that job.
Think of them as setting sensible defaults for your tag types, a floor that everything starts from before you apply more specific styles on top.
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: system-ui, sans-serif;
background: #141d34;
color: #cbd5e1;
padding: 2rem;
margin: 0;
}
h1, h2 {
color: #ffffff;
font-weight: 700;
}
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
h2 { font-size: 1.35rem; margin: 1.5rem 0 0.5rem; }
p {
line-height: 1.75;
margin-bottom: 1rem;
max-width: 65ch;
}
a {
color: #6366f1;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<h1>Base Styles with Type Selectors</h1>
<p>Every paragraph on this page inherits these styles automatically.
You don't need to add a class to every single <code><p></code> tag.</p>
<h2>A Secondary Heading</h2>
<p>That's the beauty of type selectors for base typography.
Set it once, forget it, and every matching tag just works.
<a href="#">Links pick up the color too.</a></p>
</body>
</html>
Where Type Selectors Start to Feel Limiting
The problem with type selectors is that they’re all-or-nothing for a given tag. Every <p> on your page gets the same styles. That’s fine for base defaults, but what happens when you need some paragraphs to look different from others? An intro paragraph in a larger font, or a caption paragraph in a lighter color?
You can’t target specific instances of a tag with a type selector alone. That’s where class selectors come in, and honestly, class selectors are where you’ll spend most of your time in CSS.
05Class Selectors: The Real Workhorses of Modern CSS
The class selector is the one you’ll use the most. By a large margin. If you become really good at one CSS selector, make it this one.
A class selector starts with a dot, followed by the class name. You apply a class to an HTML element using the class attribute, and then you target it in CSS with the dot notation.
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: system-ui, sans-serif;
background: #141d34;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
/* This is a class selector: starts with a dot */
.btn {
display: inline-block;
padding: 0.65rem 1.4rem;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
border: none;
text-decoration: none;
}
.btn-primary {
background: #6366f1;
color: #ffffff;
}
.btn-outline {
background: transparent;
color: #6366f1;
border: 2px solid #6366f1;
}
.btn-danger {
background: #ff5c7a;
color: #ffffff;
}
</style>
</head>
<body>
<button class="btn btn-primary">Primary Button</button>
<button class="btn btn-outline">Outline Button</button>
<button class="btn btn-danger">Danger Button</button>
</body>
</html>
Notice that all three buttons share the class btn, which gives them the common padding, border-radius, and font settings. Then each button gets one additional class, btn-primary, btn-outline, or btn-danger, that applies its unique color treatment.
This pattern is used everywhere in CSS. It’s clean, predictable, and easy to maintain.
Stacking Multiple Classes on One Element
An HTML element can carry as many classes as you want, just separate them with spaces in the class attribute. This is one of the most useful things about class selectors, and it’s what makes component-based CSS patterns like BEM work so well.
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: system-ui, sans-serif;
background: #141d34;
color: #cbd5e1;
padding: 2rem;
display: flex;
gap: 1rem;
align-items: flex-end;
flex-wrap: wrap;
}
/* Base card styles */
.card {
background: #1e293b;
border-radius: 12px;
padding: 1.25rem;
border: 1px solid rgba(255,255,255,0.07);
}
/* Size modifiers */
.card-sm { width: 140px; font-size: 0.82rem; }
.card-md { width: 200px; }
.card-lg { width: 260px; font-size: 1.05rem; }
/* Color accent modifiers */
.accent-purple { border-top: 3px solid #6366f1; }
.accent-pink { border-top: 3px solid #ec4899; }
.accent-cyan { border-top: 3px solid #06b6d4; }
</style>
</head>
<body>
<!-- Mix and match: each card uses .card plus a size and color class -->
<div class="card card-sm accent-cyan">
<strong>Small + Cyan</strong>
<p>Two classes combined.</p>
</div>
<div class="card card-md accent-purple">
<strong>Medium + Purple</strong>
<p>Base style + size + accent.</p>
</div>
<div class="card card-lg accent-pink">
<strong>Large + Pink</strong>
<p>Three separate classes doing three separate jobs.</p>
</div>
</body>
</html>
Reusability Is the Whole Point
Here’s what makes class selectors genuinely powerful: you can apply the same class to completely different HTML elements. A class doesn’t care if it’s on a <div>, a <p>, or a <span>. If an element carries the class, it gets the style.
This is how UI design systems work. You define .btn once and use it on 50 different buttons across your entire site. You want to change the button’s border radius? You change it in one CSS rule, and every button on every page updates. That’s the power of reusable classes.
If you were using type selectors or ID selectors for all of this instead, that system breaks down quickly. Type selectors are too broad and ID selectors can’t be reused, which brings us to the next selector type.
Naming classes well matters: A class name should describe what the element is or what role it plays, not how it looks. .card-highlight ages better than .card-yellow. When you change the color later, the second name is a lie.
06ID Selectors: Powerful, but Handle With Care
The ID selector targets a single, unique element on the page. You write it with a hash symbol followed by the ID name, and on the HTML side, you add it using the id attribute.
How an ID Selector Works
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: system-ui, sans-serif;
background: #141d34;
color: #cbd5e1;
margin: 0;
}
/* ID selector: uses the # symbol */
#site-header {
background: #1e293b;
padding: 1.25rem 2rem;
border-bottom: 1px solid rgba(255,255,255,0.07);
display: flex;
align-items: center;
justify-content: space-between;
}
#site-header h1 {
font-size: 1.1rem;
font-weight: 700;
color: #fff;
margin: 0;
}
</style>
</head>
<body>
<header id="site-header">
<h1>My Website</h1>
<nav>Nav goes here</nav>
</header>
<main style="padding: 2rem;">
<p>The header above is styled via an ID selector.</p>
</main>
</body>
</html>
The HTML rule for IDs is strict: every ID must be unique on a page. You can only use id="site-header" on one element. If you use it on two, the HTML is technically invalid, and CSS behavior becomes unpredictable.
The Specificity Score That Creates Real Problems
This is where things get interesting, and where a lot of beginners hit a wall without knowing why.
Remember the specificity scores from the overview cards? An ID selector scores (1, 0, 0) while a class selector scores (0, 1, 0). That means a single ID selector outranks any number of class selectors in a specificity battle. Even .one.two.three.four.five, five class selectors combined, which scores (0, 5, 0), loses to a single #id.
This is a problem when you build a UI with ID selectors and later try to modify or override those styles using classes. The classes simply won’t win.
07The Specificity Scoreboard: Seeing the Gap Clearly
Here’s a visual that shows exactly where each selector sits in the specificity hierarchy. The three columns represent: how many ID selectors, how many class selectors, and how many type selectors are in your rule. The first column carries the most weight.
*UniversalpType.clsClass#idIDThe key insight here: the three columns don’t interact. A score of (0, 10, 0) never beats (1, 0, 0). Columns don’t carry over. One ID will always outrank any number of classes. Always.
This is why experienced developers lean on classes for nearly all their styling, and reserve IDs for very specific, intentional use cases.
08Why Leaning Too Hard on ID Selectors Will Come Back to Bite You
Let me show you a concrete example of what happens when you style things with IDs and then try to override them. This is a real scenario that trips beginners and even intermediate developers up.
Using ID for Styling
/* HTML */ <p id="intro" class="text-soft"> Hello </p> /* CSS */ #intro { /* (1,0,0) */ color: #ffffff; } .text-soft { /* (0,1,0) */ color: #94a3b8; /* loses */ }
.text-soft is ignored. The ID always wins, even if it comes first in the stylesheet.
Using Class for Styling
/* HTML */ <p class="intro text-soft"> Hello </p> /* CSS */ .intro { /* (0,1,0) */ color: #ffffff; } .text-soft { /* (0,1,0) */ color: #94a3b8; /* wins ✓ */ }
Same specificity: source order decides.
.text-soft comes last, so it applies cleanly.
The issue isn’t that ID selectors are broken. It’s that they’re too strong for everyday styling work. Once you’ve styled something with an ID, the only ways to override it are: write another ID selector, use a more complex compound selector that outscores it, or reach for !important. None of those are great options to be in the habit of using.
When your whole stylesheet is classes, the cascade stays predictable. Specificity scores stay flat and comparable. You can use source order as your tiebreaker, which is exactly what the cascade is designed for. We covered all of this in detail in the CSS cascade lesson, and selectors are where you see those concepts in real action.
Here’s the same scenario in actual code so you can run it and see for yourself:
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: system-ui, sans-serif;
background: #141d34;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* --- SCENARIO A: ID used for styling --- */
/* The ID rule (1,0,0) completely blocks .text-soft (0,1,0) */
#para-a {
color: #ffffff;
font-size: 1rem;
}
.text-soft {
color: #94a3b8; /* Won't apply to #para-a */
}
/* --- SCENARIO B: Class used instead --- */
/* Both rules score (0,1,0), source order decides: .text-soft wins */
.para-b {
color: #ffffff;
font-size: 1rem;
}
/* .text-soft already defined above, same score as .para-b */
/* Since .text-soft comes after .para-b, it applies correctly */
/* Labels for clarity */
.label {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 0.25rem;
}
.label-red { color: #ff5c7a; }
.label-green{ color: #2ecc71; }
</style>
</head>
<body>
<div>
<p class="label label-red">Scenario A: .text-soft tries to override #para-a</p>
<p id="para-a" class="text-soft">
This text stays white. The ID wins. .text-soft is ignored.
</p>
</div>
<div>
<p class="label label-green">Scenario B: .text-soft overrides .para-b cleanly</p>
<p class="para-b text-soft">
This text is the soft gray. Class vs class: source order decides.
</p>
</div>
</body>
</html>
So When Should You Actually Use an ID Selector?
Good question. There are legitimate reasons to use IDs, just not primarily for styling:
Jump links and fragment navigation: When you create anchor links like <a href="#contact">Contact</a> that jump to a section with id="contact", that’s IDs doing exactly what they’re designed for. We covered this in the HTML fragment identifiers lesson.
Form accessibility: Connecting a <label for="email"> to an <input id="email"> is a proper and important use of IDs. That’s an HTML pairing, and it’s right to use IDs for it.
JavaScript targeting: document.getElementById() is one of the fastest DOM queries in JavaScript. Using IDs for JS hooks is completely fine (JavaScript will be covered in its own series later, so no need to worry about it yet).
Genuinely unique, one-off elements: If you have a very specific element with a unique look that you’re 100% certain will never be reused or overridden, an ID can be used for styling. But the emphasis is on “certain”, and in practice, that certainty is rare.
The rule of thumb most pros follow: Use IDs for JavaScript hooks, accessibility, and fragment links. Use classes for styling. This single habit will save you from a surprising number of frustrating CSS bugs.
09Habits Worth Building From Day One
Here’s a practical summary of what good selector usage looks like in real projects, based on how the selector types are actually used in production CSS today:
Reach for the universal selector for resets only. The * { box-sizing: border-box; margin: 0; padding: 0; } reset at the top of your stylesheet is the main job of * in your day-to-day work. Don’t use it for anything beyond that without a clear reason.
Use type selectors to set base defaults, not component styles. p { line-height: 1.7 } is type-selector territory. .card-text { line-height: 1.7 } is class territory. Anything that should apply universally to a tag across the entire page belongs on the type selector. Anything that belongs to a specific component belongs on a class.
Build your UI in classes. Classes are reusable, they compose well with multiple classes on one element, and they keep your specificity at a predictable, manageable level. The inheritance we covered earlier works beautifully alongside class-based styles, because you’re not fighting the cascade at every turn.
Avoid ID selectors for styling, full stop, until you have a specific reason. Not because IDs are bad, but because the habit of using them for styling creates friction that compounds over time. Projects grow. UI components get reused. Styles need overriding. Classes handle all of that gracefully.
Keep your selector specificity as low as possible. This is called “keeping a flat specificity graph”. When all your rules have similar specificity, the cascade’s source-order tiebreaker works exactly as expected, and your code is predictable to anyone reading it.
10Quick Reference: All Four CSS Selector Types Side by Side
| Selector | Syntax | Targets | Specificity | Best Used For | Usage Note |
|---|---|---|---|---|---|
| Universal | * | Every element on the page | (0, 0, 0) | CSS resets, box-sizing | Use Carefully |
| Type | p, h1, a | All elements of that tag | (0, 0, 1) | Base/global typography styles | Use for Defaults |
| Class | .btn, .card | Any element with that class | (0, 1, 0) | Components, reusable styles | Use the Most |
| ID | #header | One unique element (per page) | (1, 0, 0) | JS hooks, jump links, forms | Avoid for Styling |
11What Comes Next in This Series
You now have the four CSS selector fundamentals locked in. That’s a real foundation. From here, the next logical step is learning how to combine selectors, which is where CSS starts feeling genuinely expressive.
In the next lesson, we’ll cover grouping and combining selectors: comma-separated groups for shared styles, descendant vs child combinators, adjacent and sibling selectors, and how to write compact, readable CSS that targets exactly the right elements in the right context.
Everything in that lesson builds on what you’ve learned here. Knowing what a class selector is matters a lot when you start writing things like .nav .link or .card + .card. You’ll see how it all fits together.
For now, take the universal, type, class, and ID selectors and practice using them intentionally on something real. The best way to make this click is to write the code, see what it does, and notice what happens when specificity isn’t what you expected.
You’ll get it quickly. These four selectors really are the whole foundation.
Universal, Type, Class, and ID
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.