CSS Interview Questions: Selectors, Specificity and the Cascade
Reviewed by Mark Dickie · Last updated
6 questions on this page you can answer and see marked on the spot. Go to the first one
The CSS cascade is the set of rules that decides which declaration applies when several rules target the same element. Interviewers mostly test how specificity, source order and cascade layers combine to settle that conflict. Expect to work out a selector's specificity by hand and to predict which of two competing rules wins. A common mistake is treating @layer as a tiebreaker below specificity, when it actually ranks above it.
| Rule | What it decides | Example |
|---|---|---|
| Specificity | Which selector wins when origin and importance are equal | #nav .link (1,1,0) beats .nav .link (0,2,0) |
| Source order | The tiebreaker when specificity is equal | A later .btn rule overrides an earlier .btn rule |
| :is() and :where() | :is() takes the specificity of its most specific argument; :where() adds none | :where(article) p has specificity (0,0,1) |
| @layer | Layered rules lose to unlayered rules, whatever their specificity | An unlayered p { color: red } beats @layer base { #intro p { color: blue } } |
| !important | Author !important beats normal author rules; user !important beats author !important | A user stylesheet can override a site's color: black !important |
The order a browser checks when two declarations conflict:
- Origin and importance (user agent, user, author, and any !important)
- Cascade layers, where unlayered styles beat layered ones
- Specificity, counted as IDs, then classes, attributes and pseudo-classes, then type selectors and pseudo-elements
- Source order, where the later declaration wins
Key facts
- Tarmac's CSS interview questions cover 10 questions at difficulty 1–5 of 5.
- Tarmac tracked 828 job postings asking for CSS in September 2026.
- Roles asking for CSS advertise a median base salary of US$179,150, across 269 job postings as of September 2026.
- Tarmac last reviewed these CSS interview questions on 26 October 2026.
At a glance
| Questions | 10 |
|---|---|
| Difficulty | 1–5 of 5 |
| Formats | Multiple choice, Fill in the blank, Ordering, True / false, Short answer |
What you'll review
- has selector
- selectors
- cascade specificity
- cascade layers
- nesting
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
Which elements does .card:has(> img) { border: 2px solid } style?#
Options
Show answer
.card:has(> img) styles each .card element that has a direct img child — the .card itself receives the border, not the image. :has() tests a condition without moving the subject, so the element being styled stays on the left of :has(). The > img is only the test, never the target, and the > means a more deeply nested img will not make the card match.
:has() is a pseudo-class that tests a condition without moving the subject: the element being styled stays on the left of :has(). So .card:has(> img) matches the .card itself whenever the relative selector > img finds a direct img child — this is the long-requested "parent selector". The img is only the test, never the target, and because the relative selector starts with >, an img nested deeper (say inside a <span>) does not make the card match. To style the image instead you would write a normal descendant selector like .card > img.
The child combinator div _____ p matches a <p> only when it is a direct child of a <div>, while the attribute selector a[_____] matches any anchor that has an href.#
Show answer
The child combinator div **>** p matches a <p> only when it is a direct child of a <div>, while the attribute selector a[**href**] matches any anchor that has an href.
> is the child combinator: div > p targets only <p> elements that are immediate children of a <div> (a plain space would match any descendant). a[href] is an attribute selector matching every <a> that has an href attribute, regardless of its value.
Order these selectors from lowest to highest specificity (the last one wins a conflict).#
Put these in order
Show answer
From lowest to highest specificity:
*— the universal selector contributes nothing, (0,0,0)li— one type selector, (0,0,1).nav li— one class plus one type, (0,1,1)#main— one ID, (1,0,0)
Compared column by column as the (id, class, type) tuple, the ID wins because its first column outranks every class and type count.
Specificity is the tuple (id, class, type). * contributes nothing → (0,0,0); li is one type → (0,0,1); .nav li is one class plus one type → (0,1,1); #main is one id → (1,0,0). Compared column by column, that orders them lowest to highest, and the ID wins because its first column outranks the others.
Two rules set a button's color, and @layer base, theme; declares the layer order:#
Options
Show answer
The button is green. Cascade layers are resolved before specificity, and because theme is declared after base, any rule in theme beats any rule in base regardless of selector weight. So the low-specificity .btn rule in theme wins over the heavy #app .btn.primary rule in base — the (1,2,0) specificity is never even compared.
Cascade layers are compared before specificity in the cascade. Within the same origin, a declaration in a later-declared layer beats one in an earlier layer no matter how specific the earlier selector is — so the low-specificity .btn rule in theme (declared second) wins over the heavy #app .btn.primary rule in base. Specificity is only consulted between declarations that share the same layer, so the (1,2,0) weight is never even compared here. This inversion of the usual "specificity wins" intuition is the entire point of @layer: it lets you control precedence by intent rather than by escalating selector weight.
Given native CSS nesting:#
Options
Show answer
Every <span> descendant of .card gets color: gray. A nested bare type selector has the parent plus a descendant combinator implicitly prepended, so span inside .card means .card span. There is no string concatenation here, unlike Sass where an &-prefixed selector like &span concatenates onto the parent; a bare nested selector descends in both.
In native CSS nesting, a nested selector that doesn't reference & has & (the parent plus a descendant combinator) implicitly prepended, so span inside .card means .card span and matches every <span> descendant. There is no string concatenation like Sass's &__modifier. To match an element that is both the parent and a span you must write the type selector first — span& — because a type selector has to lead its compound; &span is invalid. To target a direct child, write & > span explicitly.
A single ID selector (#nav) has higher specificity than a selector made of ten classes (.a.b.c.d.e.f.g.h.i.j).#
Options
Show answer
True. A single ID outweighs any number of classes because specificity compares the (id, class, type) columns left to right, and a higher column always wins regardless of the lower ones. One ID gives (1, 0, 0) while ten classes give (0, 10, 0) — the ID column is compared first, and class count never carries up into it.
Specificity compares the (id, class, type) columns left to right; a higher column always wins regardless of the lower ones. One ID gives (1, 0, 0) while ten classes give (0, 10, 0). Because the ID column is compared first and 1 > 0, the ID wins — the class count can never carry into the ID column.
For normal (non-!important) declarations within the author origin, an unlayered style takes priority over any style placed inside a @layer, regardless of specificity.#
Options
Show answer
True. For normal (non-!important) author declarations, an unlayered style beats every layered rule in the same origin no matter how specific the layered selector is, because cascade layers rank below unlayered styles. This is by design, so a framework shipped in a layer can never out-muscle the consuming app's plain CSS. Under !important the ranking flips.
Cascade layers are ranked below unlayered author styles for normal declarations: any rule not inside a @layer block beats every layered rule in the same origin, no matter how specific the layered selector is. This is by design — a framework shipped in a layer can never out-muscle the consuming app's plain CSS. (Under !important the ranking flips: an important layered declaration beats an important unlayered one.)
What problem do CSS cascade layers (@layer) solve, and how does the position of a layer in the declared order interact with selector specificity when two rules conflict?#
Show answer
Cascade layers let you group rules into named buckets and control their precedence by intent rather than by escalating selector weight — solving specificity wars between, say, a third-party framework and your own overrides. The browser compares layer order before it ever looks at specificity: a declaration in a later-declared layer beats one in an earlier layer no matter how specific the earlier selector is. Specificity is only consulted between declarations that sit in the same layer (and unlayered styles outrank all layered ones for normal declarations).
@layer introduces an explicit precedence tier that is evaluated earlier in the cascade than specificity. You declare an order (e.g. @layer reset, framework, app;) and later layers win over earlier ones; only when two conflicting declarations share a layer does selector specificity decide between them. This kills the specificity arms race — you no longer need ever-heavier selectors or !important to override a framework, because you can simply place your styles in a later layer (or leave them unlayered, since unlayered normal styles beat all layered ones).
Explain how the browser uses specificity to decide which conflicting rule wins, and where !important and source order fit in.#
Show answer
Specificity is scored in three columns — IDs, then classes/attributes/pseudo-classes, then element types/pseudo-elements — and the columns are compared left to right, so a higher column beats any number of lower ones (inline styles rank above all selectors). The most specific rule wins; only when specificity ties does source order decide, with the last declaration winning. !important is separate: it lifts a declaration into a higher-priority layer that overrides normal declarations regardless of specificity.
The cascade resolves conflicts by: importance (!important vs normal), then specificity, then source order. Specificity is an (inline, id, class, type) tuple compared column by column; a higher column wins outright. Ties fall through to whichever matching rule appears last in the source.
A button's color is declared in two places: a heavy #app .btn.primary rule inside @layer components, and a plain button rule in the page's stylesheet outside any layer — and the browser's built-in (user-agent) stylesheet sets its own value too. The plain button rule wins, and selector weight had nothing to do with it. Walk through, in order, the full sequence of checks the cascade runs to pick a winning declaration, and explain why comparing specificity is one of the LAST things the browser does — what gets consulted before specificity ever comes into play, and where do !important and source order fit?#
Show answer
The cascade is a pipeline, and specificity sits in the middle of it, not at the top. Step 1 is origin and importance: declarations are bucketed by where they come from — the browser's user-agent stylesheet, user styles, and author (page) styles. For normal declarations, author beats user beats user-agent; for !important declarations that ranking REVERSES, which is why a user-agent !important rule can never be overridden by a page. Step 2, within the author origin, is cascade layers: @layer blocks are ranked by declaration order, with later layers beating earlier ones for normal declarations (reversed again under !important), and crucially, for normal declarations unlayered author styles beat every layered one (under !important this also reverses: an important layered declaration beats an important unlayered one). That's exactly why the plain button rule wins here — it's unlayered, the #app .btn.primary rule lives inside a layer, and the comparison ends before any selector is examined. Step 3, only between declarations that survive with the same origin and layer standing, is specificity (with inline style="" ranking above all selectors). Step 4, the final tiebreaker when specificity is equal, is source order — the later declaration wins.
The mental model most people carry — "highest specificity wins, plus !important as a trump card" — describes only the middle of the algorithm. The cascade actually filters in this order: (1) origin & importance — user-agent < user < author for normal declarations, with the order reversed for !important ones, so UA !important outranks everything a page can write; (2) cascade layers within the author origin — later-declared layers beat earlier ones for normal declarations (reversed under !important), and unlayered author styles beat all layered styles for normal declarations — under !important that flips too, with layered important declarations winning. This is the whole design point of @layer: a framework shipped in a layer can never out-muscle the consuming app's plain CSS no matter how specific its selectors are; (3) specificity — only consulted between declarations that tied on origin and layer, with inline style attributes ranking above any selector; (4) source order as the last tiebreaker. In the scenario, the layered #app .btn.primary rule loses to the unlayered button rule at step 2, so its three-part selector weight is never even counted. Interviewers ask this because @layer made the pre-specificity stages practically relevant: debugging "why does my high-specificity rule lose?" now requires knowing the pipeline, not just the ID/class/type columns.
Sources
The official documentation these questions are checked against:
Related interview questions
Job market
See css salaries and hiring demand from live job postings.
Practise these until they stick
That's every question we hold on this topic, and the page marks what you pick. What it can't do is remember. A free account keeps every answer, and what you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan