Every CSS rule starts with a selector. The selector answers one question: which elements should this style apply to? Choosing good CSS selectors keeps your stylesheet short, predictable and easy to change. This lesson covers the selectors you will use daily and explains specificity, the score that decides which rule wins.
The basic CSS selectors
| Selector | Example | Selects |
|---|---|---|
| Type (element) | p | Every <p> element |
| Class | .card | Every element with class="card" |
| ID | #signup | The one element with id="signup" |
| Universal | * | Every element |
| Attribute | [type="email"] | Elements whose type attribute equals email |
| Grouping | h1, h2 | Both <h1> and <h2> elements |
Classes are your main tool. An element can have several classes separated by spaces, such as class="card featured", and the same class can be reused on many elements. IDs must be unique on a page and are very specific, so most teams reserve them for links and JavaScript hooks rather than styling.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Type, class, ID and attribute selectors</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; }
p { color: #444; } /* type */
.badge { /* class */
background: #ffe8a3;
padding: 2px 8px;
border-radius: 4px;
}
#offer { border: 2px dashed #e07a00; padding: 12px; } /* ID */
a[target="_blank"] { color: purple; } /* attribute */
h1, h2 { color: #0b5394; } /* grouping */
</style>
</head>
<body>
<h1>Chennai Book Fair</h1>
<p>Entry is free for students <span class="badge">New</span></p>
<div id="offer">Buy three Tamil novels, get one bookmark free.</div>
<h2>Useful links</h2>
<p><a href="#">Same-tab link</a> · <a href="#" target="_blank">Opens in a new tab</a></p>
</body>
</html>Combinators: selecting by relationship
Combinators let you select elements based on where they sit in the HTML tree. They help you style things in context, for example links inside the footer only.
nav a(space, descendant): any<a>anywhere inside<nav>.ul > li(child): only<li>elements that are direct children of a<ul>.h2 + p(next sibling): the first<p>that comes straight after an<h2>.h2 ~ p(subsequent sibling): every<p>after an<h2>with the same parent.
Pseudo-classes and pseudo-elements
A pseudo-class starts with one colon and selects an element in a particular state or position. Common ones are :hover, :focus-visible (keyboard focus), :first-child, :nth-child(odd) and :not().
The :has() pseudo-class, supported in all major browsers since late 2023, finally lets you style a parent based on what it contains. For example, .card:has(img) selects only cards that contain an image.
A pseudo-element starts with two colons and styles a part of an element, such as ::first-line, ::placeholder or ::before and ::after, which insert decorative content.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combinators and pseudo-classes</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; }
nav a {
color: #0b5394;
text-decoration: none;
margin-inline-end: 12px;
}
nav a:hover { text-decoration: underline; }
nav a:focus-visible { outline: 3px solid orange; }
li:nth-child(odd) { background: #f1f5f9; }
h2 + p { font-weight: bold; }
.card { border: 1px solid #ccc; padding: 10px; margin-block: 8px; }
.card:has(.sold-out) { opacity: 0.5; }
p::first-letter { font-size: 1.5em; color: #c0392b; }
</style>
</head>
<body>
<nav><a href="#">Home</a><a href="#">Menu</a><a href="#">Contact</a></nav>
<h2>Today's specials</h2>
<p>The first paragraph after the heading is bold.</p>
<p>This one is normal weight.</p>
<ul>
<li>Masala dosa</li><li>Idli vada</li><li>Pongal</li><li>Kesari</li>
</ul>
<div class="card">Ghee roast <span>available</span></div>
<div class="card">Rava dosa <span class="sold-out">sold out</span></div>
</body>
</html>Specificity: which rule wins?
When two rules conflict, the browser compares their specificity. Think of it as a three-part score written (IDs, classes, types):
- Each ID adds to the first column.
- Each class, attribute selector or pseudo-class adds to the second column.
- Each type selector or pseudo-element adds to the third column.
So p scores (0,0,1), .intro scores (0,1,0) and #hero .intro scores (1,1,0). Columns are compared from left to right, so a single ID beats any number of classes. Only when scores are equal does the later rule win. Inline style attributes beat all selectors, and !important overrides normal declarations.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Specificity comparison</title>
<style>
p { color: gray; } /* (0,0,1) */
.intro { color: green; } /* (0,1,0) wins over p */
#hero .intro { color: blue; } /* (1,1,0) wins over .intro */
:where(.box) { background: pink; } /* :where() adds zero specificity */
div { background: lightyellow; } /* (0,0,1) beats :where() */
body { font-family: system-ui, sans-serif; padding: 20px; }
.box { padding: 10px; }
</style>
</head>
<body>
<section id="hero">
<p class="intro">I am blue: the ID makes this selector strongest.</p>
</section>
<p class="intro">I am green: a class beats an element selector.</p>
<p>I am gray.</p>
<div class="box">My background is light yellow, because :where() has zero specificity.</div>
</body>
</html>Watch out: Avoid fixing conflicts by adding !important or long ID chains. It starts a specificity war where every new rule needs to be even stronger. Prefer simple class selectors with similar specificity and let source order do the rest.
Tip: The :is() pseudo-class groups selectors and takes the specificity of its most specific argument, while :where() always has zero specificity. :where() is useful for default styles that are easy to override.
Habits for maintainable selectors
Selectors that work today can become a headache when the HTML changes. A few habits keep a stylesheet easy to live with as a site grows:
- Name classes by purpose, not looks.
.price-tagstill makes sense after the design changes from red to green;.red-textdoes not. - Keep selectors short.
.menu-linkis easier to override and faster to read thanheader nav ul li a, which breaks as soon as the markup is reorganised. - Do not style by ID. Its high specificity makes later overrides painful. Use the ID for in-page links and JavaScript, and a class for styling.
- Style states with pseudo-classes and attributes.
button:disabledor[aria-expanded="true"]reflects the real state of the element, so the styling and the accessibility information never drift apart.
Next, put your selectors to work with CSS Colors and Backgrounds. CSS selectors are also used in JavaScript with document.querySelector(), which you will meet in JS DOM Manipulation.
