CSS Interview Questions and Answers
Short answer
CSS rounds test layout reasoning, not memorised properties. The recurring questions are the box model and border-box, specificity and the cascade, the position values, why z-index fails, margin collapsing, and how you build a responsive layout without a stack of media queries.
| The question | What it reveals |
|---|---|
| Explain the box model | Whether border-box is a habit or a fact you recall |
| How does specificity work? | Whether your CSS scales or ends in !important |
| Explain position: absolute | Whether "positioned ancestor" means something to you |
| Why is z-index not working? | Stacking contexts - the deepest CSS question commonly asked |
| Centre a div | Speed. There is one right answer and you should not hesitate |
| Flexbox or Grid? | Whether you pick by dimensionality or by habit |
| Make this responsive | Whether you reach for media queries first or last |
| What is margin collapsing? | Real debugging experience - it cannot be reasoned out |
What a CSS round actually looks like
For React-heavy candidates this is the round that goes worst, because it is the one they did not revise. It is usually short - ten to fifteen minutes inside a larger frontend interview, and it is almost never about obscure properties. The interviewer describes a layout and asks how you would build it, then asks why something in their existing code is not behaving, and both questions are really about whether you can reason about the cascade and the box model rather than trying declarations until one works.
That means the highest-return preparation is not learning more properties. It is being able to explain, out loud and without hedging, what creates a containing block, what creates a stacking context, and what makes a layout adapt without a breakpoint. Those three ideas cover most of what gets asked and almost all of what gets debugged.
What is the box model, and why border-box?
Every element is a box of content, wrapped in padding, then a border, then margin. Under the default `content-box`, the `width` you set applies to the content only, so padding and border are added on top and the element ends up wider than the number you wrote. `border-box` makes width include padding and border, which is what almost everyone means when they set a width.
1.card {
2 width: 200px;
3 padding: 20px;
4 border: 2px solid;
5}
6/* → content-box (default): 200 + 40 + 4 = 244px on screen */
7
8.card-fixed {
9 box-sizing: border-box;
10 width: 200px;
11 padding: 20px;
12 border: 2px solid;
13}
14/* → border-box: exactly 200px; the content shrinks to 156px */
15
16/* The reset in every real codebase */
17*, *::before, *::after { box-sizing: border-box; }
18/* → set it once, then width means what you expect everywhere */
19
20/* Margin is always outside the box, in both modes */
21.spaced { box-sizing: border-box; width: 200px; margin: 10px; }
22/* → occupies 220px of horizontal space, but the box is 200px */The detail worth adding is that margin is never included in either mode, it is space around the box, not part of it, which is why a full-width element with a margin still overflows its container. That is the actual cause of most "why is there a horizontal scrollbar" bugs, and connecting the two shows the model is something you use rather than recite.
How does specificity and the cascade work?
When two rules set the same property, the browser resolves it in order: origin and importance first, then specificity, then source order. Specificity is a three-part score, ids, then classes and attributes and pseudo-classes, then elements and pseudo-elements, compared left to right, with the first differing column deciding. It is not a single number, which is why a hundred classes never beat one id.
1a { } /* → 0,0,1 */
2.link { } /* → 0,1,0 beats any number of elements */
3nav a.link { } /* → 0,1,2 */
4#main .link { } /* → 1,1,0 one id outranks all of the above */
5a:hover { } /* → 0,1,1 pseudo-CLASS counts as a class */
6a::before { } /* → 0,0,2 pseudo-ELEMENT counts as an element */
7[data-active] { } /* → 0,1,0 attribute selectors count as classes */
8:where(#main) .link { } /* → 0,1,0 :where contributes ZERO specificity */
9:is(#main, .x) .link { } /* → 1,1,0 :is takes its most specific argument */
10
11/* Equal specificity: the later rule wins */
12.btn { color: red; }
13.btn { color: blue; } /* → blue */
14
15/* !important jumps the whole cascade, and can only be beaten by another
16 !important with higher specificity, which is how specificity wars start */
17.btn { color: green !important; } /* → green */Two modern additions are worth naming because they show you have kept up. `:where()` always scores zero, which makes it the correct way to write library or reset styles that consumers can override with a single class. And cascade layers - `@layer reset, base, components`, let you decide precedence by layer order rather than by selector weight, which is the structural fix for the problem `!important` papers over.
What do the position values do?
`static` is the default and ignores offsets. `relative` shifts the element visually while it keeps its space in the flow, and, more importantly, makes it a positioning context for its descendants. `absolute` removes the element from the flow and positions it against the nearest positioned ancestor. `fixed` positions against the viewport. `sticky` behaves as relative until a scroll threshold, then as fixed within its parent.
1.parent { position: relative; } /* → becomes the reference point */
2.badge { position: absolute; top: 0; right: 0; }
3/* → without position:relative on .parent, this anchors to the nearest
4 positioned ancestor further up - often the viewport itself */
5
6.header { position: sticky; top: 0; }
7/* → needs a threshold: sticky with no top/left/right/bottom never sticks */
8
9/* Gotcha 1: sticky is bounded by its PARENT, not the viewport.
10 It stops at the parent's bottom edge. */
11
12/* Gotcha 2: an ancestor with overflow hidden/auto/scroll silently kills it */
13.wrapper { overflow: hidden; }
14.wrapper .header { position: sticky; top: 0; }
15/* → never sticks, and there is no warning anywhere */
16
17/* fixed is relative to the viewport, UNLESS an ancestor has a transform,
18 which makes that ancestor the containing block instead */
19.animated { transform: translateZ(0); }
20.animated .modal { position: fixed; inset: 0; }
21/* → fills .animated, not the screen */The last case is genuinely useful to know and almost never covered: a `transform` on an ancestor turns that ancestor into the containing block for `fixed` descendants, so a modal inside an animated wrapper stops covering the screen. It is the same underlying rule as the z-index problem below, which makes them a good pair to answer together.
Why doesn't z-index work?
Because `z-index` only orders siblings within the same stacking context. If an ancestor creates its own context, everything inside it is painted as a single unit at that ancestor's level, so a child with `z-index: 9999` cannot escape a parent whose context sits below the header. Raising the number does nothing, which is why this bug is so frustrating to debug by trial and error.
1/* A new stacking context is created by, among others: */
2.a { position: relative; z-index: 0; } /* positioned + any z-index but auto */
3.b { opacity: 0.99; } /* → any opacity below 1 */
4.c { transform: translateY(0); } /* → any transform */
5.d { filter: blur(0); } /* → any filter */
6.e { will-change: transform; }
7.f { isolation: isolate; } /* → the explicit, intentional way */
8.g { position: fixed; } /* → always, regardless of z-index */
9
10/* The classic failure */
11.page { transform: translateZ(0); } /* someone added this for "GPU perf" */
12.modal { position: fixed; z-index: 9999; }
13/* → .modal is trapped inside .page's context and still sits under .header */
14
15/* The fixes, in order of preference */
16/* 1. Render the modal in a portal at document.body, outside the context */
17/* 2. Remove the accidental context-creating property from the ancestor */
18/* 3. Give the header and modal a shared parent with isolation: isolate */The answer that scores names the cause and the fix in the right order. The cause is an ancestor creating a context, usually an `opacity`, a `transform` or a `filter` added for an animation. The fix is to render the modal in a portal so it is not inside that ancestor at all, which is exactly why every React UI library portals its overlays to the body. Escalating z-index values is the anti-pattern being tested for.
This is 1 of 200+ questions in the Complete Frontend Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the Complete Frontend Kit → ₹499How do you centre an element?
Both axes, one line: `display: grid; place-items: center`. Interviewers ask this to check for hesitation more than knowledge, so answer instantly and then offer the alternatives. The old absolute-positioning trick is still worth knowing because you will meet it in legacy code.
1.parent { display: grid; place-items: center; }
2/* → both axes, one declaration, no assumptions about the child */
3
4.flex { display: flex; justify-content: center; align-items: center; }
5/* → same result; justify is the MAIN axis, align the cross axis,
6 so they swap meaning when flex-direction is column */
7
8.legacy { position: relative; }
9.legacy > .child {
10 position: absolute;
11 top: 50%; left: 50%;
12 transform: translate(-50%, -50%);
13}
14/* → works without knowing the child's size, because translate percentages
15 resolve against the ELEMENT's own dimensions, not the parent's */
16
17.horizontal-only { margin-inline: auto; max-width: 60ch; }
18/* → the correct way to centre a block horizontally; auto margins do not
19 centre vertically in normal flow, which is the classic follow-up */The reason `translate(-50%, -50%)` works is worth a sentence, because it is the one part of that snippet people cannot explain: percentage values in `transform` resolve against the element's own size, whereas percentages in `top` and `left` resolve against the containing block. That is what makes the trick size-agnostic.
What is margin collapsing?
Adjacent vertical margins in normal flow combine into one margin equal to the larger of them, rather than adding up. It applies between siblings, between a parent and its first or last child, and on an empty element's own top and bottom margins. Horizontal margins never collapse, and neither do margins inside flex or grid containers.
1/* 1. Siblings: 30px between them, not 50px */
2.a { margin-bottom: 30px; }
3.b { margin-top: 20px; }
4/* → gap is 30px */
5
6/* 2. Parent and first child: the child's margin ESCAPES the parent */
7.parent { background: grey; }
8.parent > .first { margin-top: 40px; }
9/* → the 40px pushes .parent down instead of the child inside it */
10
11/* Anything that establishes a block formatting context stops it */
12.parent { padding-top: 1px; } /* → or a border, overflow, or: */
13.parent { display: flow-root; } /* → the modern, side-effect-free fix */
14.parent { display: flex; } /* → flex/grid children never collapse */
15
16/* 3. Which is why gap has largely replaced margins for spacing */
17.stack { display: grid; gap: 24px; }
18/* → 24px between every child, no collapsing, no last-child override */The strongest close here is to say you rarely deal with margin collapsing any more because you space children with `gap` in a flex or grid container. That removes the collapsing entirely, removes the `:last-child { margin-bottom: 0 }` rule everyone used to write, and is what a modern codebase looks like, which is a better answer than knowing three ways to suppress it.
How do you build a responsive layout without media queries?
By letting the layout respond to the space it is given rather than to the viewport size. `clamp()` handles fluid type and spacing between a floor and a ceiling. `minmax()` with `auto-fit` builds a grid that reflows on its own. Container queries let a component respond to its own width, which is what you actually want for a card that appears in both a sidebar and a main column.
1/* Fluid type: never below 1rem, never above 2rem, scales between */
2h1 { font-size: clamp(1rem, 2.5vw + 0.5rem, 2rem); }
3/* → the +0.5rem term keeps it zoomable; pure vw units break zoom */
4
5/* A responsive card grid with no breakpoints at all */
6.grid {
7 display: grid;
8 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
9 gap: 1.5rem;
10}
11/* → 4 columns on a desktop, 1 on a phone, and every step between */
12
13/* Container queries: respond to the PARENT, not the viewport */
14.card-wrapper { container-type: inline-size; }
15@container (min-width: 400px) {
16 .card { display: grid; grid-template-columns: 120px 1fr; }
17}
18/* → the same card is stacked in a 300px sidebar and side-by-side in a
19 700px main column, with no knowledge of the page layout */
20
21/* Logical properties adapt to writing direction for free */
22.note { margin-inline-start: 1rem; padding-block: 0.5rem; }
23/* → margin-left in English, margin-right in Arabic or Urdu */The `auto-fit` and `minmax` grid is the single best answer to "make this responsive" in an interview, because it produces every breakpoint at once from one declaration. And the reason container queries matter is worth stating: a component's correct layout depends on the space it has, not on the size of the window, so a viewport media query is the wrong signal for anything reusable.
What are CSS custom properties good for?
Custom properties are not just variables, they cascade and inherit, they are live at runtime, and JavaScript can read and write them. That combination is what makes them the mechanism behind theming: define the tokens on `:root`, override them under a selector or a media query, and every rule that references them updates without touching any other CSS.
1:root {
2 --surface: #111827;
3 --accent: #a855f7;
4 --space: 1rem;
5}
6
7.card { background: var(--surface); padding: var(--space); }
8
9/* Overriding a token in a subtree re-themes everything inside it */
10.card--highlight { --surface: #1a2333; }
11/* → no new background rule needed; the existing one picks it up */
12
13@media (prefers-color-scheme: light) {
14 :root { --surface: #ffffff; }
15}
16/* → one override, whole theme switches */
17
18/* Fallbacks, and reading from JS */
19.badge { color: var(--brand, var(--accent, purple)); }
20/* → tries --brand, then --accent, then purple */
21/* getComputedStyle(el).getPropertyValue("--accent") → " #a855f7" */
22/* el.style.setProperty("--accent", "#06b6d4") → live, no rebuild */Which modern CSS is worth mentioning?
Naming two or three features that shipped recently is a cheap way to show you still read about CSS rather than reusing what you learnt four years ago. `:has()` is the one to lead with, it is the long-requested parent selector, and it lets you style a container based on what it contains, which previously required JavaScript. `aspect-ratio` replaces the padding-top percentage hack for keeping media in proportion. `inset` is shorthand for top, right, bottom and left in one declaration.
Beyond those, subgrid lets a nested grid align to its parent's tracks, which finally solves the problem of making card headers line up across a row when the text lengths differ. Logical properties such as `margin-inline` and `padding-block` adapt automatically to writing direction, which matters for any product shipping in more than one script. And `@layer` gives the cascade explicit ordering, which is the structural answer to specificity conflicts rather than the `!important` one. Mention what you have actually used and say so honestly, an interviewer will follow up, and a feature you have only read about is easy to spot.
What loses marks in a CSS round
CSS answers fail in predictable ways, and none of them are about not knowing enough properties.
- →Raising a z-index instead of asking what created the stacking context.
- →Reaching for !important, then having no explanation beyond "it was being overridden".
- →Not knowing that position: absolute needs a positioned ancestor to anchor to.
- →Hesitating on how to centre a div, it reads as inexperience, fairly or not.
- →Answering every responsive question with a list of pixel breakpoints.
- →Picking Flexbox or Grid by habit rather than by whether the layout is one or two dimensional.
- →Not recognising margin collapsing when shown a gap that is smaller than the sum of two margins.
Frequently asked questions
- What is the CSS box model?
- Every element is a content box wrapped in padding, then a border, then margin. With the default content-box, a set width applies to the content only and padding and border are added on top; with border-box, width includes padding and border. Margin is always outside the box in both modes.
- Why should I set box-sizing: border-box globally?
- Because it makes width mean what you intend. Without it, adding padding to a 200px element makes it render wider than 200px, so every width has to be recalculated whenever spacing changes. One universal rule removes an entire category of layout arithmetic.
- How does CSS specificity work?
- Selectors score three components - ids, then classes, attributes and pseudo-classes, then elements and pseudo-elements, compared left to right, with the first differing column winning. The components are never added together, which is why one id beats any number of classes. Equal specificity is resolved by source order.
- What is the difference between position relative, absolute and fixed?
- relative offsets the element visually while keeping its space in the flow and makes it a positioning context. absolute removes it from the flow and positions it against the nearest positioned ancestor. fixed positions against the viewport, unless an ancestor has a transform, which becomes the containing block instead.
- Why is my z-index not working?
- Because z-index only orders siblings within one stacking context. If an ancestor created its own, through opacity below 1, a transform, a filter, will-change or position fixed, everything inside is painted as a unit at that ancestor's level and no child value can escape it. Portal the element out instead of raising the number.
- What creates a new stacking context in CSS?
- A positioned element with any z-index other than auto, any opacity below 1, any transform, filter, backdrop-filter or perspective, will-change on those properties, position: fixed, isolation: isolate, and flex or grid children with a z-index. The first three cause almost every real-world bug.
- What is the best way to centre a div?
- display: grid with place-items: center centres on both axes in one declaration and makes no assumptions about the child's size. Flexbox with justify-content and align-items does the same in three. For horizontal-only centring of a block, margin-inline: auto with a max-width is the idiomatic answer.
- What is margin collapsing?
- Adjacent vertical margins in normal flow merge into one margin equal to the larger, rather than adding. It happens between siblings, between a parent and its first or last child, and within empty elements. Horizontal margins never collapse, and neither do margins inside flex or grid containers.
- How do I stop margins from collapsing?
- Establish a block formatting context on the parent with display: flow-root, or add padding, a border or overflow, though those have visual side effects. The better answer is to stop using margins for spacing between children and use gap in a flex or grid container, where collapsing does not occur.
- When should I use Flexbox versus Grid?
- Flexbox for one dimension - a row or a column where content sizes itself, like a navbar or a toolbar. Grid for two dimensions at once, or whenever you want to define the tracks up front, as in a page layout or a card gallery. They combine freely; a grid item is often a flex container.
- How do you make a layout responsive without media queries?
- Use clamp() for fluid type and spacing between a floor and a ceiling, repeat(auto-fit, minmax(280px, 1fr)) for grids that reflow on their own, and container queries so a component responds to its own width rather than the viewport. Media queries then handle only genuine layout changes rather than every size adjustment.
- Is CSS actually important in frontend interviews?
- Yes, and it is where React-focused candidates most often drop easy marks. Layout reasoning, specificity and a debugging question such as a broken z-index come up routinely, because they are quick to ask and hard to bluff. Neglecting CSS costs points that require no extra preparation to win.
This is 1 of 200+ questions in the Complete Frontend Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the Complete Frontend Kit → ₹499