CSS selectors are the patterns you write to target HTML elements. Knowing all of them — not just class and id — lets you write less HTML and cleaner stylesheets.
Quick reference
The selectors you'll reach for every day.
| Selector | Example | Matches |
|---|---|---|
* |
* {} |
Every element |
tag |
p {} |
All <p> elements |
.class |
.btn {} |
Elements with class btn |
#id |
#header {} |
Element with id header |
A, B |
h1, h2 {} |
Both h1 and h2 |
A B |
nav a {} |
<a> inside <nav> |
A > B |
ul > li {} |
<li> direct child of <ul> |
A + B |
h2 + p {} |
<p> immediately after <h2> |
A ~ B |
h2 ~ p {} |
All <p> after <h2> (same parent) |
:hover |
a:hover {} |
Element being hovered |
:focus |
input:focus {} |
Focused element |
:nth-child(n) |
li:nth-child(2) {} |
Second <li> among siblings |
::before |
p::before {} |
Generated content before <p> |
[attr] |
[disabled] {} |
Has a disabled attribute |
[attr="val"] |
[type="text"] {} |
Attribute equals value |
Basic selectors
Universal selector
* {
box-sizing: border-box;
}
Matches everything. Useful for resets and debugging (* { outline: 1px red }).
Type selector
p { margin-bottom: 1rem; }
a { color: #0070f3; }
button { cursor: pointer; }
Matches all elements of that tag. Lowest specificity besides *.
Class selector
.card { border-radius: 8px; }
.btn.primary { background: #0070f3; } /* both classes */
Most common selector. Multiple classes on one element are matched with chained dots.
ID selector
#main-nav { position: sticky; top: 0; }
Matches one element per page. High specificity — avoid for styling, prefer for JS anchors.
Selector list (grouping)
h1,
h2,
h3 {
font-family: 'Inter', sans-serif;
}
Applies the same rules to multiple selectors. One invalid selector invalidates the whole list in older browsers.
Combinators
Descendant combinator (space)
nav a { text-decoration: none; } /* any <a> inside <nav> */
.card p { color: #666; } /* any <p> inside .card */
Matches nested elements at any depth.
Child combinator (>)
ul > li { list-style: disc; } /* direct children only */
.dropdown > .menu { display: none; }
Matches only direct children, not grandchildren.
Adjacent sibling (+)
h2 + p { margin-top: 0; } /* first <p> after any <h2> */
label + input { border-color: blue; }
Matches the element immediately after another (same parent).
General sibling (~)
h2 ~ p { color: #444; } /* all <p> after a <h2> */
.toggle:checked ~ .panel { display: block; }
Matches all later siblings, not just the first one. Useful for CSS-only toggles.
Pseudo-classes
Pseudo-classes select elements based on state or position.
User action states
a:hover { color: #0057b8; }
a:focus { outline: 2px solid #0070f3; }
a:active { color: #003d80; }
a:visited { color: purple; }
/* keyboard focus only (not mouse click) */
button:focus-visible { outline: 2px solid #0070f3; }
Order matters for <a>: LoVe HAte — :link, :visited, :hover, :active.
Form states
input:disabled { opacity: 0.5; }
input:enabled { border: 1px solid #ccc; }
input:checked { accent-color: #0070f3; }
input:required { border-color: #e44; }
input:optional { border-color: #ccc; }
input:valid { border-color: #2a2; }
input:invalid { border-color: #e44; }
input:placeholder-shown { color: #999; }
input:focus-within { } /* parent when child is focused */
Structural pseudo-classes
/* position among siblings */
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
li:nth-child(2) { background: #f5f5f5; } /* 2nd */
li:nth-child(odd) { background: #fafafa; } /* 1, 3, 5… */
li:nth-child(even){ background: #f0f0f0; } /* 2, 4, 6… */
li:nth-child(3n) { color: red; } /* every 3rd */
li:nth-child(3n+1){ color: blue; } /* 1, 4, 7… */
/* position among same-type siblings */
p:first-of-type { font-size: 1.1em; }
p:last-of-type { margin-bottom: 0; }
p:nth-of-type(2) { }
/* only child */
p:only-child { } /* no siblings at all */
p:only-of-type { } /* no <p> siblings */
/* empty element */
div:empty { display: none; }
Negation and matching
/* not */
:not(.disabled) { cursor: pointer; }
:not(p):not(div) { color: #333; } /* chain multiple */
:not(.a, .b) { } /* list in :not() — modern browsers */
/* matches any in list (like grouping, but with specificity of highest) */
:is(h1, h2, h3) a { color: inherit; }
/* same as :is() but zero specificity */
:where(h1, h2, h3) a { color: inherit; }
/* has (parent selector) */
.card:has(img) { padding: 0; } /* .card that contains an <img> */
form:has(:invalid){ border: 1px solid red; }
:has() is the long-awaited parent selector — supported in all modern browsers as of 2024.
Language and direction
p:lang(fr) { quotes: "«" "»"; }
:dir(rtl) { text-align: right; }
Pseudo-elements
Pseudo-elements create virtual elements or target specific parts.
p::before {
content: "→ ";
color: #0070f3;
}
p::after {
content: " ←";
}
/* first letter / first line */
p::first-letter { font-size: 2em; float: left; }
p::first-line { font-weight: bold; }
/* text selection */
::selection {
background: #0070f3;
color: #fff;
}
/* placeholder text */
input::placeholder { color: #999; font-style: italic; }
/* scroll marker (modern) */
::scroll-marker { content: "•"; }
Always use :: (double colon) for pseudo-elements. :before (single colon) still works but is the old CSS2 syntax.
content is required on ::before / ::after. Use content: "" if you only need an empty box.
Attribute selectors
/* has attribute */
[disabled] { opacity: 0.4; }
[data-active] { background: yellow; }
/* equals */
[type="email"] { }
[rel="noopener"] { }
/* contains word (space-separated) */
[class~="btn"] { } /* matches class="btn primary" */
/* starts with */
[href^="https"] { } /* secure links */
[href^="mailto"] { }
/* ends with */
[href$=".pdf"] { } /* PDF links */
[src$=".svg"] { }
/* contains substring */
[href*="example"] { } /* URL contains "example" */
/* hyphen-separated starts with */
[lang|="en"] { } /* en, en-US, en-GB */
/* case-insensitive flag */
[href$=".PDF" i] { } /* matches .pdf and .PDF */
Specificity
When two selectors match the same element, the more specific one wins.
Specificity scoring
| Selector | Score (a, b, c) |
|---|---|
| Inline style | (1, 0, 0, 0) |
#id |
(0, 1, 0, 0) |
.class, :pseudo-class, [attr] |
(0, 0, 1, 0) |
tag, ::pseudo-element |
(0, 0, 0, 1) |
*, combinators |
(0, 0, 0, 0) |
Scores are compared left to right. One ID beats any number of classes.
/* specificity (0,0,1,0) */
.btn { color: red; }
/* specificity (0,0,1,1) — wins */
button.btn { color: blue; }
/* specificity (0,1,0,0) — wins regardless of class count */
#submit { color: green; }
:is(), :not(), :has() specificity
:is() and :has() take the specificity of their most specific argument:
/* same specificity as #id (0,1,0,0) */
:is(#id, .class) { }
/* always zero specificity — use for base styles */
:where(.card, .panel) p { }
!important
.text { color: blue !important; } /* beats everything except another !important */
Avoid !important in component CSS. Reserve it for utility overrides (!important on .sr-only, .hidden, etc.) where it's intentional.
Practical patterns
Zebra striping
tr:nth-child(odd) { background: #f9f9f9; }
tr:nth-child(even) { background: #fff; }
Style last item differently
li:not(:last-child) { border-bottom: 1px solid #eee; }
Cards that contain images
.card:has(img) {
padding: 0;
}
.card:not(:has(img)) {
padding: 1.5rem;
}
CSS-only accordion
.toggle:checked + .content { display: block; }
.toggle:not(:checked) + .content { display: none; }
Focus ring only for keyboard users
button:focus { outline: none; }
button:focus-visible { outline: 2px solid #0070f3; }
External links
a[href^="http"]:not([href*="yourdomain.com"])::after {
content: " ↗";
font-size: 0.75em;
}
Common mistakes
1. IDs for everything
Using #id forces you into specificity wars. Prefer classes.
2. Forgetting LoVe HAte order
a:visited after a:hover means hover styles never apply to visited links. Use :link, :visited, :hover, :active in that order.
3. Descendant when you need child
nav a matches <a> nested anywhere — including dropdown sub-menus. Use nav > a if you only want top-level links.
4. ::before without content
::before and ::after require content. Without it, the pseudo-element doesn't render. Use content: "" for an empty box.
5. :nth-child counts all siblings, not just same-type
p:nth-child(2) matches a <p> only if it's the second child of its parent, regardless of type. Use p:nth-of-type(2) to count only <p> siblings.
6. Not accounting for :has() fallback
:has() is baseline-supported since 2024 but needs a fallback for older browsers. Add a no-JS/no-:has() default style first, then enhance.
FAQ
What is the difference between :nth-child and :nth-of-type?
:nth-child(2) selects the second child element regardless of its tag. :nth-of-type(2) selects the second element of a specific tag type. p:nth-child(2) only matches if the second child happens to be a <p>.
What is CSS specificity and how do I calculate it?
Specificity is a score (a, b, c): a counts ID selectors, b counts class/attribute/pseudo-class selectors, c counts tag/pseudo-element selectors. Compare left to right. The higher score wins. Inline styles beat everything except !important.
How does :is() differ from a selector list?
A regular selector list (,) fails entirely if one selector is invalid. :is() is forgiving — invalid selectors are ignored. Also, :is() inherits the specificity of its most specific argument.
What does :where() do that :is() doesn't?
:where() has zero specificity, making it easy to override. Use :where() for default/reset styles you expect developers to override, and :is() when you want the selector's natural specificity.
Is :has() (parent selector) safe to use?
Yes, as of 2024 it has baseline-wide support (Chrome 105+, Firefox 121+, Safari 15.4+). Add graceful degradation for older browser support if needed.
Why does ::before need content: ""?
The CSS spec requires content for generated content. Even an empty string content: "" tells the browser to render the pseudo-element box. Without it, nothing renders — not even width/height.