Why CSS feels confusing β and how to fix it
CSS isn't hard because there's too much to memorize. It's confusing because three invisible systems run at once: selecting (what gets styled), the cascade (which rule wins), and layout (how boxes flow). Learn them as separate ideas and the fog lifts.
1. Select
Every rule starts by pointing at elements. Master selectors and you control exactly what changes.
2. The cascade
When two rules fight, specificity decides the winner. This is the #1 source of "why isn't my CSS working?!"
3. The box model
Every element is a box with content, padding, border, margin. Layout is just arranging boxes.
4. Layout flow
Normal flow β then Flexbox & Grid. Don't jump to layout before the three ideas above click.
The best way to actually learn it
- One concept at a time. Don't open 20 properties at once. Learn selectors fully, then the box model, then color/typography, then layout.
- Type it, don't copy it. Re-typing code builds muscle memory that copy-paste never will.
- Use DevTools constantly. Right-click any element β Inspect. Edit CSS live and watch it change β this is the fastest feedback loop in web dev.
- Steal from real sites. Inspect a site you like and read how its rules are built. CSS is more readable than you think.
- Build β break β fix. Change a value, see what happens, undo it. Breaking things on purpose teaches cause and effect.
- Don't memorize β reference. Even pros look things up. Keep a cheat sheet (like the one below) and repetition will do the memorizing for you.
selector { property: value; }. If your style isn't applying, 9 times out of 10 your
selector is wrong or another rule is more specific β not the property.
What good selectors unlock
Selectors aren't trivia β they're leverage. The right one lets you style hundreds of elements at once, react to the user, and decorate a page without touching the HTML. Here's where they pay off on real projects every day:
Style 100 things at once
Give buttons one class and style them in a single place. Change the rule β every button updates.
.btn { ... }
Readable tables & lists
Zebra-stripe rows automatically β no classes on each row, and it stays right when rows are added.
tr:nth-child(even) { ... }
Interactivity, no JavaScript
Hover effects, focus rings, and open/closed states β all from a pseudo-class.
.card:hover { ... }
Style by purpose
Target the email field, the required inputs, or the disabled buttons β by what they are.
input[type="email"] { ... }
Decorate without clutter
Add icons, quote marks, or counters straight from CSS instead of pasting them into every element.
.badge::before { content: "β
"; }
Theme everything at once
Flip one class on a parent (like .dark) and restyle everything inside it.
.dark .title { color: #fff; }
The anatomy of a rule
Before the selectors, lock in the shape of every CSS rule. Everything else is a variation on this.
.button { /* selector β WHAT to style */ color: white; /* declaration: property + value */ background: #8b5cf6; padding: 10px 18px; } /* the { } is the "declaration block" */
Basic selectors
The four you'll use every single day β plus how to group them. These point at elements by their tag, a class you add, or a unique id.
A plain paragraph β styled by its type (p).
A paragraph with class="d-highlight" β styled by class.
A paragraph with id="dSpecial" β styled by id.
/* Type β every <p> on the page */ p { color: #cbd5e1; } /* Class β any element with class="d-highlight" (reusable) */ .d-highlight { border-left: 3px solid #8b5cf6; } /* ID β the ONE element with id="dSpecial" (unique per page) */ #dSpecial { border-left: 3px solid #ec4899; } /* Universal β literally everything */ * { box-sizing: border-box; } /* Grouping β apply the same rule to several selectors */ h1, h2, h3 { font-family: sans-serif; }
Combinators β selecting by relationship
These target elements based on where they sit relative to others. The highlighted chips below show exactly what each one grabs.
.box > span/* Descendant (space) β ANY .box span, nested at any depth */ .box span { color: tomato; } /* Child (>) β only DIRECT children */ .box > span { font-weight: bold; } /* Adjacent sibling (+) β the element IMMEDIATELY after */ h2 + p { margin-top: 0; } /* General sibling (~) β ALL following siblings */ h2 ~ p { color: gray; }
Attribute selectors
Target elements by their HTML attributes and values β great for styling form fields and links without adding extra classes.
type, links decorated by href/* Exact match */ input[type="email"] { border-color: #8b5cf6; } input[type="password"] { border-color: #ec4899; } /* Starts with ^= / Ends with $= / Contains *= */ a[href^="https"]::before { content: "π "; } a[href$=".pdf"]::after { content: " π"; } a[href*="example"] { color: teal; } /* Just "has the attribute at all" */ [disabled] { opacity: 0.5; }
Pseudo-classes β state & position
A pseudo-class (one colon :) styles an element in a particular state
(hovered, focused) or position (first, even, nth). Hover the button and tab into it
to see the states fire.
- First item β
:first-child - Even row β
:nth-child(even) - Odd row
- Even row β
:nth-child(even) - Last item β
:last-child
/* State β react to the user */ button:hover { background: #8b5cf6; } button:focus { outline: 3px solid rgba(139,92,246,.4); } a:visited { color: purple; } input:checked { accent-color: green; } /* Structural β react to position */ li:first-child { font-weight: bold; } li:last-child { color: pink; } li:nth-child(even) { background: #22222b; } /* zebra stripes */ /* :not() β everything EXCEPT the match */ li:not(.active) { opacity: 0.7; }
Pseudo-elements β style part of an element
A pseudo-element (two colons ::) styles a piece of an element, or injects
decorative content that isn't in your HTML. ::before and ::after are the
workhorses.
::before β no extra HTML.The first letter of this paragraph is enlarged into a drop cap using ::first-letter, a classic magazine effect that takes one line of CSS and zero changes to the markup itself.
/* Inject content before/after β needs the content property */ .badge::before { content: "β "; color: #fbbf24; } .card::after { content: ""; display: block; clear: both; } /* Style a PART of the text */ p::first-letter { font-size: 2.6rem; font-weight: 800; color: #ec4899; float: left; } p::first-line { font-weight: bold; } ::selection { background: #8b5cf6; } /* highlighted text */ ::placeholder { color: #71717a; }
Specificity β why your CSS "isn't working"
When two rules target the same element, the more specific one wins β not necessarily the last one. Read specificity as three columns: IDs, classes, types. Higher column wins, left to right.
p
.note
nav .note p
#header .note
#id beats any number of .classes,
which beat any number of types. So #header .note (1,1,0) beats
nav .note p (0,1,2). When stuck, stop adding !important β lower your
specificity instead. Keep selectors flat and class-based and these fights mostly disappear.
Every common selector at a glance
Bookmark this. You don't memorize selectors β you look them up until they stick.
| Selector | Matches |
|---|---|
| Basic | |
* | Every element |
p | All <p> elements (by tag/type) |
.class | Elements with that class |
#id | The one element with that id |
a, b, c | Grouping β each of a, b, and c |
| Combinators | |
A B | B inside A (descendant, any depth) |
A > B | B that is a direct child of A |
A + B | B immediately after A (adjacent sibling) |
A ~ B | All B siblings after A |
| Attribute | |
[attr] | Has the attribute |
[attr="x"] | Attribute equals x |
[attr^="x"] | Starts with x |
[attr$="x"] | Ends with x |
[attr*="x"] | Contains x |
| Pseudo-classes (state) | |
:hover | Pointer is over it |
:focus | Element has keyboard focus |
:active | Being clicked |
:checked | Checked checkbox / radio |
:disabled | Disabled form control |
| Pseudo-classes (structural) | |
:first-child | First child of its parent |
:last-child | Last child of its parent |
:nth-child(n) | The nth child (e.g. even, 3, 2n+1) |
:not(x) | Everything except x |
| Pseudo-elements | |
::before | Injected content before the element |
::after | Injected content after the element |
::first-letter | First letter (drop caps) |
::first-line | First line of text |
::selection | Text the user highlighted |
::placeholder | Placeholder text in an input |
Use CSS to its full capacity
Knowing the selectors is half of it. These habits separate people who fight CSS from people who steer it.
Do
- Style with classes β reusable and low-specificity
- Keep selectors flat (1β2 levels), not
div ul li a span - Use DevTools to inspect and edit live
- Let
:hover/:focushandle states instead of JS - Group shared rules; reach for
:nth-child&::beforebefore adding markup
Don't
- Reach for
!importantβ fix specificity instead - Over-use
#idselectors for styling - Write deep, brittle selector chains
- Add a class for something a pseudo-class already does
- Memorize blindly β reference, repeat, and it sticks