It Is Decided in Three Steps
In one line
When several rules apply to the same property, the browser picks one in three stages: importance → specificity → order. If the decision is made at an earlier stage, it doesn't look at the later ones.
Why you should memorize this
The moment CSS eats the most time is not from not knowing the syntax, but the moment of "I clearly wrote it, but it doesn't apply." The rule is clearly visible in the developer tools, but it is struck through.
If, without knowing why, you write the selector one step longer or stack one more rule on top and move on, the next person has to write an even longer selector, and the person after that uses !important. A few months later you are left with a file in which nobody knows which lines are safe to delete. This is almost always the path by which CSS falls apart.
Once you know the three stages, you can answer "if it lost, why did it lose?" on the spot. Just telling apart whether it was specificity or order changes the way to fix it completely.
Stage 1 — Importance
사용자 !important > 작성자 !important > 작성자 일반 > 사용자 일반 > 브라우저 기본
Here !important wins. That's why you reach for it in a hurry. But the only way to beat !important is another !important, so once you use it, that property can only be handled that way forever. That is the typical path by which CSS falls apart.
When you feel like using !important, the real problem is usually selector design.
Stage 2 — Specificity
You count with three numbers.
| Place | What is counted | Example |
|---|---|---|
| a | IDs | #nav |
| b | Classes · attributes · pseudo-classes | .btn, [open], :hover |
| c | Tags · pseudo-elements | div, ::before |
a → 0,0,1
.btn.primary → 0,2,0
#nav a:hover → 1,1,1
ul > li::marker → 0,0,3
Places don't carry over into each other. Even 11 classes can't beat a single ID. 0,11,0 < 1,0,0.
Two special cases.
:where(...)is always 0.:where(.card) .title=0,1,0. A library uses it to provide "defaults that are easy to override":is(...)and:not(...)follow the highest one inside the parentheses
Stage 3 — Order
This is where people get caught most often.
If importance and specificity are equal, the one written later wins. It is about position in the file, not meaning on screen.
@media (max-width: 900px) {
.side { position: fixed; } /* 모바일 덮개 */
}
.side { position: sticky; } /* ← 나중에 썼다 */
This code is sticky on mobile too. Being inside a media query doesn't make it stronger. Since the specificity is the same (both 0,1,0), the one written later wins.
Always put the base rule above the media query.
This is a bug that actually happened in this service. The mobile table-of-contents overlay wouldn't open, and the cause was that the base .lessonSide { position: sticky } was below the @media block. Moving one block up fixed it.
Inheritance is not the cascade
Things like color, font-family, and line-height are inherited by children. margin, padding, display, and border are not.
A value received through inheritance is weaker than any rule applied directly to that element. However low the specificity, the directly applied one wins. An inherited value is used only "when there is no rule at all."
Custom properties are inherited
:root { --accent: #3b82f6; }
.btn { background: var(--accent); }
--accent is inherited, so if you put it on :root you can use it anywhere, and you can override it only in a particular subtree.
.dark-panel { --accent: #93c5fd; } /* 이 안의 .btn 들만 색이 바뀐다 */
This is the decisive difference from preprocessor variables (SCSS $). SCSS variables disappear at compile time, but custom properties are alive at runtime and take part in the cascade. That is why switching a theme takes one line of JavaScript.
What really bites you in grid
Grid is easy to learn, but not knowing one thing can hurt badly.
A child with display: none drops out of the grid entirely. It does not keep its place empty.
.layout { grid-template-columns: 0 1fr; } /* 접었을 때 목차를 0px 로 */
.layout.hidden .side { display: none; }
The intent was "collapse the table of contents to 0px." But because it is display: none, the table of contents drops out of the flow, and the body gets pushed into the first column (0px). The line breaks after every character, and the page became 33,000px tall.
This too is a bug that actually happened in this service. The fix is one line.
.layout.hidden { grid-template-columns: 1fr; } /* 칸을 아예 하나로 */
You don't make the column 0; you reduce the number of columns. If the child is gone, the column must be gone too.
When to use grid and when flex
- Grid — you decide both directions at once. Page skeletons, card grids
- Flex — you divide within a single row (or a single column). Toolbars, button groups, list items
If you're unsure, ask this. "Do I want to decide both rows and columns?" If so, it's grid.
The three things you use most in flex are justify-content: space-between + align-items: center + gap. Pushing just one item to the right with margin-left: auto is also common.
The order in which to diagnose in the field
When CSS "doesn't apply," check in order before adding rules.
- Does the selector match the element at all — whether the rule shows up in the developer tools
- Is it losing to another rule — if it is struck through, it lost
- If it lost, why — specificity or order
- Are you expecting an inherited value — if a directly applied rule exists, inheritance is not used
Use !important only when there is still no answer after checking all four. It usually ends at number 3.
The reason this order matters in practice is that the way to fix it differs at each stage. If you lost on specificity, you have to work on the selector, and if you lost on order, you have to move the file load order or the rule position. If you touch both sides at once without separating the cause, you can't tell whether it was fixed or just happened to pass.