What are CSS selectors?
A CSS selector is the “who” part of your CSS rule. The declaration block is the “what”.
In other words: selectors choose elements, then properties style them.
p {
outline: 3px dashed #111;
}
strong {
background: #ffe08a;
}
code {
border: 2px solid #111;
padding: 2px 6px;
border-radius: 10px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo-wrap {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
border: 3px solid #111;
border-radius: 16px;
background: #fff;
display: grid;
gap: 10px;
}
p {
margin: 0;
padding: 10px 12px;
background: #f3f3f3;
border-radius: 12px;
}
Select the who, then style the
what.This is a second paragraph with bold text and
inline code.
The universal selector: *
The * selector matches everything.
You’ll often see it used for “reset” styles, especially when paired with pseudo-elements.
The key idea: you can set defaults once, so your real selector examples stay focused.
* {
outline: 2px dashed #111;
}
.demo * {
outline: 2px dashed #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
border: 3px solid #111;
border-radius: 16px;
background: #fff;
}
.demo .card {
background: #f3f3f3;
padding: 12px;
border-radius: 14px;
display: grid;
gap: 8px;
}
.demo h3 {
margin: 0;
}
.demo p {
margin: 0;
}
Scoped reset demo
Some text with a link.
Notice the difference between * (global) and .demo * (scoped).
Learn more about the universal selector in the CSS Universal Selector (*) Interactive Tutorial.
Type selectors: selecting by tag name
A type selector matches HTML elements by their tag name:
p, h2, button, etc.
They’re great for base typography, but can be too broad if you don’t scope them.
h3 {
letter-spacing: 0.04em;
}
p {
font-size: 16px;
}
button {
border-radius: 999px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
border: 3px solid #111;
border-radius: 16px;
background: #fff;
display: grid;
gap: 10px;
}
h3 {
margin: 0;
}
p {
margin: 0;
background: #f3f3f3;
padding: 10px 12px;
border-radius: 12px;
}
button {
justify-self: start;
padding: 10px 14px;
border: 2px solid #111;
background: #fff;
font: inherit;
}
Type selectors
This paragraph is targeted by
p.
Learn more about tag (also called type) selectors in the CSS Type Selector Interactive Tutorial.
Class selectors: the workhorse
A class selector starts with a dot: .card, .button,
.warning.
Classes are reusable, and they play nicely with component-style CSS.
.card {
border: 3px solid #111;
}
.card.featured {
background: #ffe08a;
}
.tag {
display: inline-block;
padding: 4px 10px;
border: 2px solid #111;
border-radius: 999px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.card {
padding: 12px;
border-radius: 16px;
background: #f3f3f3;
display: grid;
gap: 8px;
}
.card h3 {
margin: 0;
}
.card p {
margin: 0;
}
.tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
Normal card
Just a regular card.
Featured card
This card has two classes:
cardandfeatured.
That .card.featured selector means “an element that has both classes”.
That’s an AND relationship (more on that soon).
ID selectors: unique, powerful, and often a trap
An ID selector starts with #, like #main.
IDs are meant to be unique on a page.
The big gotcha: IDs have high specificity, which can make your CSS harder to override later. For styling, prefer classes most of the time.
#promo {
border: 3px solid #111;
background: #ffe08a;
}
.promo {
border: 3px solid #111;
background: #ffe08a;
}
#promo .button {
border-width: 4px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.panel {
padding: 12px;
border-radius: 16px;
background: #f3f3f3;
display: grid;
gap: 10px;
}
.panel h3,
.panel p {
margin: 0;
}
.button {
justify-self: start;
padding: 10px 14px;
border: 2px solid #111;
background: #fff;
border-radius: 999px;
font: inherit;
}
Promo panel
This element has
id="promo"andclass="panel".Same look via class
This element uses a class instead of an ID.
Combining selectors: “AND” logic
When you write selectors with no space between them, you’re stacking requirements: the element must match all parts.
.button.primarymeans: has both classesbutton.buttonmeans: a<button>that also has classbuttona.button.primarymeans: a link that has both classes
.button.primary {
background: #111;
color: #fff;
}
button.button {
transform: rotate(-1deg);
}
a.button {
text-decoration: none;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.button {
display: inline-block;
padding: 10px 14px;
border: 2px solid #111;
background: #f3f3f3;
border-radius: 999px;
font: inherit;
color: inherit;
}
a.button {
background: #fff;
}
Grouping selectors: “OR” logic
A comma-separated list means “match any of these”. Think of it like OR.
This is perfect for shared styles without repeating yourself.
h3,
p {
margin: 0;
}
button,
a {
border: 2px solid #111;
border-radius: 999px;
}
.card,
.badge {
background: #ffe08a;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.card {
padding: 12px;
border-radius: 16px;
border: 2px solid #111;
background: #f3f3f3;
display: grid;
gap: 10px;
}
.badge {
justify-self: start;
padding: 6px 10px;
border: 2px solid #111;
border-radius: 999px;
background: #f3f3f3;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
button,
a {
padding: 10px 14px;
background: #fff;
font: inherit;
color: inherit;
text-decoration: none;
}
Learn more about OR and AND selectors in the CSS OR and AND Selectors Interactive Tutorial.
Combinators: descendant vs child
Combinators describe relationships between elements. Two of the most important:
- Descendant (space):
.card pmeans “anypinside.card, at any depth”. - Child (
>):.card > pmeans “onlypthat are direct children of.card”.
.card p {
outline: 3px dashed #111;
}
.card > p {
outline: 3px dashed #111;
}
.card > .inner > p {
outline: 3px dashed #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
}
.card {
padding: 12px;
border-radius: 16px;
background: #f3f3f3;
border: 2px solid #111;
display: grid;
gap: 10px;
}
.card h3 {
margin: 0;
}
.card p {
margin: 0;
padding: 10px 12px;
background: #fff;
border-radius: 12px;
border: 2px solid #111;
}
.inner {
padding: 10px;
border-radius: 14px;
background: #ffe08a;
border: 2px solid #111;
display: grid;
gap: 10px;
}
Descendant vs child
I am a direct child paragraph.
I am nested inside
.inner.
If you ever feel like your selector is turning into a family tree chart, it’s usually a sign you should add a class closer to what you actually want to style.
Learn more about direct child selectors in the CSS Direct Child Selector (>) Interactive Tutorial.
Combinators: adjacent vs general sibling
Sibling selectors work on elements that share the same parent.
- Adjacent sibling (
+):h3 + pselects the firstpimmediately after anh3. - General sibling (
~):h3 ~ pselects allpthat come after anh3(same parent).
h3 + p {
background: #ffe08a;
}
h3 ~ p {
background: #ffe08a;
}
.note + .note {
border-style: dashed;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.note {
padding: 12px;
border-radius: 16px;
border: 2px solid #111;
background: #f3f3f3;
display: grid;
gap: 8px;
}
h3,
p {
margin: 0;
}
p {
padding: 10px 12px;
border-radius: 12px;
background: #fff;
border: 2px solid #111;
}
Heading
Paragraph A (right after the heading).
Paragraph B (still after the heading).
Another heading
Paragraph C (right after the heading).
Paragraph D (still after the heading).
Learn more about sibling selectors in the CSS Sibling Selector Interactive Tutorial.
Attribute selectors: selecting by attributes
Attribute selectors let you match elements based on attributes like href, type,
data-*, and more.
Basic attribute selector
[disabled] means “elements that have a disabled attribute”.
button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
a[href] {
text-decoration: underline;
}
[data-kind="warning"] {
background: #ffe08a;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
button {
padding: 10px 14px;
border: 2px solid #111;
border-radius: 999px;
background: #fff;
font: inherit;
}
a {
color: inherit;
}
.badge {
padding: 10px 12px;
border-radius: 14px;
border: 2px solid #111;
background: #f3f3f3;
}
Attribute operators you’ll actually use
[attr="value"]equals[attr^="value"]starts with[attr$="value"]ends with[attr*="value"]contains[attr~="word"]contains a whole word in a space-separated list
a[href^="https"] {
border-bottom: 3px solid #111;
}
a[href$=".pdf"] {
background: #ffe08a;
}
a[href*="docs"] {
outline: 3px dashed #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 10px;
}
a {
display: inline-block;
padding: 10px 12px;
border-radius: 14px;
border: 2px solid #111;
background: #f3f3f3;
color: inherit;
text-decoration: none;
}
Learn more about attribute selectors in the CSS Attribute Selector Interactive Tutorial.
Pseudo-classes: state and structure
A pseudo-class starts with :.
It matches elements based on state (hover, focus) or position in the document (first child, nth
child).
State pseudo-classes: :hover, :focus-visible, :active
These selectors react to user interaction. They’re the foundation of UI styling.
button:hover {
transform: translateY(-2px);
}
button:active {
transform: translateY(1px);
}
button:focus-visible {
outline: 4px solid #111;
outline-offset: 3px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 10px;
}
button {
justify-self: start;
padding: 12px 16px;
border: 2px solid #111;
border-radius: 999px;
background: #ffe08a;
font: inherit;
transition: transform 120ms ease;
}
Link pseudo-classes: :link, :visited
Links have special states. The common order is:
:link, :visited, :hover, :active.
Many people remember it as “LoVe HAte”.
a:link {
background: #f3f3f3;
}
a:visited {
background: #ffe08a;
}
a:hover {
outline: 3px dashed #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 10px;
}
a {
display: inline-block;
padding: 10px 12px;
border-radius: 14px;
border: 2px solid #111;
color: inherit;
text-decoration: none;
}
The :visited might not be styled in some browsers for privacy reasons.
Structural pseudo-classes: :first-child, :last-child, :nth-child()
These are about where an element appears in its parent’s children list.
The most useful one is :nth-child().
li:nth-child(1)is the first itemli:nth-child(odd)is 1, 3, 5, …li:nth-child(3n)is 3, 6, 9, …li:nth-child(3n + 1)is 1, 4, 7, …
li:first-child {
background: #ffe08a;
}
li:last-child {
background: #ffe08a;
}
li:nth-child(odd) {
background: #ffe08a;
}
li:nth-child(3n) {
background: #ffe08a;
}
li:nth-child(3n + 1) {
background: #ffe08a;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
ol {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 8px;
}
li {
padding: 10px 12px;
border-radius: 14px;
border: 2px solid #111;
background: #f3f3f3;
}
- Item 1
- Item 2
- Item 3
- Item 4
- Item 5
- Item 6
- Item 7
- Item 8
- Item 9
Heads up: :nth-child() counts all element children, not just the type you wrote.
If you need “the third li specifically”, and there are other elements mixed in, look at
:nth-of-type().
Learn more about pseudo-classes in the CSS Pseudo Classes Interactive Tutorial.
Pseudo-elements: styling part of an element
A pseudo-element starts with ::.
It targets a piece of an element, like the first line, selection highlight, or a generated “before/after”
box.
p::first-letter {
font-size: 32px;
font-weight: 700;
}
p::first-line {
text-transform: uppercase;
}
.badge::before {
content: "★";
margin-right: 8px;
}
p::selection {
background: #ffe08a;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 10px;
}
p {
margin: 0;
padding: 12px;
border-radius: 16px;
border: 2px solid #111;
background: #f3f3f3;
max-width: 56ch;
}
.badge {
justify-self: start;
padding: 8px 12px;
border-radius: 999px;
border: 2px solid #111;
background: #fff;
}
FeaturedPseudo-elements can style a part of text or create decorative content without extra HTML. Try selecting some text, too.
Learn more about pseudo-elements in the CSS Pseudo Elements Interactive Tutorial.
Specificity: why your CSS “doesn’t work”
When multiple rules target the same element and property, the browser needs to pick a winner. The main factors are:
- Specificity (how “targeted” the selector is)
- Order (later rules can win if specificity ties)
- Importance (
!importantcan override, but use sparingly)
A beginner-friendly specificity rule
- IDs (
#something) are very strong - Classes, attributes, pseudo-classes are medium
- Type selectors and pseudo-elements are light
If your rule “isn’t applying”, it’s often because another rule has higher specificity.
p {
border: 3px solid #111;
}
.note p {
border: 6px solid #111;
}
#wrap p {
border: 10px solid #111;
}
p {
border: 3px solid #111 !important;
}
*,
::before,
::after {
box-sizing: border-box;
}
#wrap {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
}
.note {
padding: 12px;
border-radius: 16px;
background: #f3f3f3;
border: 2px solid #111;
}
p {
margin: 0;
padding: 12px;
border-radius: 14px;
background: #fff;
}
This paragraph is targeted by multiple selectors.
!important is like yelling in a meeting: it works, but it makes future conversations harder.
Prefer fixing specificity by writing better selectors or reorganizing your CSS.
Specificity helpers: :where() and :is()
Two modern helpers can make selector logic easier:
:is()groups selectors, but keeps the greatest specificity from its arguments.:where()groups selectors with zero specificity, which is great for making styles easy to override.
.card :is(h3, p) {
outline: 3px dashed #111;
}
.card :where(h3, p) {
outline: 3px dashed #111;
}
:is(.card, .panel) .tag {
background: #ffe08a;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.card,
.panel {
padding: 12px;
border-radius: 16px;
border: 2px solid #111;
background: #f3f3f3;
display: grid;
gap: 8px;
}
h3,
p {
margin: 0;
}
.tag {
justify-self: start;
padding: 6px 10px;
border-radius: 999px;
border: 2px solid #111;
background: #fff;
}
Card title
Card text
tagPanel title
Panel text
tag
Learn more about :is() and :where() in the CSS :is() and :where() Pseudo-Classes Interactive Tutorial.
Practical selector patterns you’ll use all the time
Pattern: component + descendants
A simple, scalable approach is: give your component a class, then style inside it. This keeps your CSS from accidentally leaking into other areas.
.card-title {
letter-spacing: 0.04em;
}
.card .card-desc {
opacity: 0.85;
}
.card .card-actions > a {
background: #ffe08a;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.card {
padding: 12px;
border-radius: 16px;
border: 2px solid #111;
background: #f3f3f3;
display: grid;
gap: 10px;
}
.card-title,
.card-desc {
margin: 0;
}
.card-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.card-actions a {
display: inline-block;
padding: 10px 12px;
border-radius: 999px;
border: 2px solid #111;
background: #fff;
color: inherit;
text-decoration: none;
}
Pattern: utility classes (small reusable helpers)
Utility classes are tiny, single-purpose classes like .text-center or .sr-only.
They help you avoid rewriting the same CSS in multiple places.
.text-center {
text-align: center;
}
.rounded {
border-radius: 16px;
}
.shadow {
box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.15);
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: system-ui, Arial, sans-serif;
padding: 16px;
background: #fff;
border: 3px solid #111;
border-radius: 16px;
display: grid;
gap: 12px;
}
.card {
padding: 12px;
border-radius: 10px;
border: 2px solid #111;
background: #f3f3f3;
display: grid;
gap: 8px;
}
h3,
p {
margin: 0;
}
Utility classes
Stack small classes to build a style.
Common selector mistakes (and quick fixes)
-
“My selector doesn’t work.”
Check that your HTML actually matches what your selector describes. Typos in class names are the classic villain.
-
“It works, but it styles too much.”
Scope it: instead of
p, use.article por add a component wrapper class. -
“I can’t override it.”
You’re likely fighting specificity. Prefer classes over IDs, keep selectors short, and consider
:where()for low-specificity grouping. -
“My :nth-child() is weird.”
Remember it counts all element children. If you need type-only counting, try
:nth-of-type().
Selectors cheat sheet
*universalptype selector.cardclass selector#promoID selector.card pdescendant.card > pdirect childh3 + padjacent siblingh3 ~ pgeneral sibling[disabled]attribute presence[href^="https"]attribute starts with:hover,:focus-visible,:activestate:first-child,:nth-child()structure::before,::after,::selectionpseudo-elements:is()grouping helper:where()grouping helper with zero specificity
CSS Selectors Conclusion
CSS selectors are the foundation of styling on the web. With a solid grasp of how they work, you can write CSS that’s powerful, efficient, and maintainable. Remember to keep your selectors focused, be mindful of specificity, and use modern helpers to simplify your code. Happy styling!
