CSS Module 24 – Design to code and interview preparation
CSS interview preparation
Prepare for CSS interviews: explain margins, stacking, specificity and flexbox in a minute each, solve a timed layout task, and back answers with evidence.
What you will learn
- Explain core CSS concepts clearly, among them the cascade, specificity, the box model, formatting contexts, stacking and flexbox versus grid
- Solve layout tasks under a time limit with a repeatable approach
- Justify trade-offs in performance, accessibility and browser support with evidence you can cite
Before you start
What you’ll learn · Before you start
What you will learn
- Explain core CSS concepts clearly, among them the cascade, specificity, the box model, formatting contexts, stacking and flexbox versus grid
- Solve layout tasks under a time limit with a repeatable approach
- Justify trade-offs in performance, accessibility and browser support with evidence you can cite
Before you start
On this page
CSS interviews come in two formats, and each tests a different skill: explaining how CSS decides what you see, and building a piece of interface against the clock. Prepare for each on its own. For explaining, keep an explain-it-back list of core mechanisms, such as margin collapsing, stacking contexts, specificity, flex items that overflow and container queries, and practise a one-minute answer and a five-minute demo for each. For building, follow one routine every time: clarify the requirements, write semantic HTML, sketch the layout, stress the content, add the states, and finish with an accessibility pass. In both kinds of round, back your claims with evidence: a Baseline status, a WCAG success criterion or a number you measured. This lesson gives model explanations, a stacking demo and a 30-minute task solved step by step, with results recorded in Chromium 153 and Firefox 155.
Two kinds of round
In a concept round you are asked to explain something, to predict what a snippet shows, or to say why a page is
broken. A strong answer names the mechanism in the terms the specification uses, gives a small example with numbers,
mentions the edge case, and says how you would check it in DevTools. A weak answer names a fix without its reason:
“add z-index: 9999” is an answer that a follow-up question takes apart.
In a build round you get a fixed time, such as 30 to 60 minutes, an editor and a component to build: a card list, a navigation bar, a form or a dialog. What gets noticed is the order of your work as much as the result: HTML that works before any styling, a layout choice you can defend, content that may be long or missing, keyboard focus, and what you say while you type. Pixel-perfect polish matters least, and it is what to drop when time runs short.
Prepare for the first kind with the explain-it-back list below, said aloud against a timer, and for the second with the 30-minute routine, practised on several small tasks.
The explain-it-back list
Each of these six topics explains a whole family of layout bugs, which is what makes them good interview questions: margins, stacking contexts, specificity, overflowing flex items, container or media queries, and flexbox or grid. For each, learn the one-minute answer below, a demo you can type in five minutes, and the follow-up question it invites.
Margins. In normal flow, the vertical margins of block boxes that touch, with no padding, border or line box
between them, collapse into one margin as large as the largest of them (CSS 2.1, section 8.3.1). Two paragraphs with
margin-bottom: 24px and margin-top: 16px end up 24 px apart, not 40. It also happens between a parent and its
first or last child, which is why a heading’s margin can appear above its card. Horizontal margins never collapse, and
nothing collapses inside a flex or grid container. The follow-up is “how do you stop it?”: padding or a border on the
parent, or display: flow-root, which gives the parent its own block formatting context.
Stacking contexts. A stacking context is a group of boxes painted together, as one layer, in the order of their
z-index. The root element starts one, and so do a positioned box with a z-index, sticky and fixed boxes, opacity below
1, a transform or filter, isolation: isolate, container-type: inline-size and several others. A z-index is only
compared with boxes in the same context, so a menu with z-index: 1000 inside a header with z-index: 1 stays below
a banner with z-index: 2 in the page. The follow-up is “how do you fix it without a bigger number?”, which the demo
below answers.
Specificity. A selector’s specificity is three counts: IDs; classes, attributes and pseudo-classes; and types and
pseudo-elements. They are compared from the left, and the first difference decides, so #nav a at (1, 0, 1) beats
.menu .item a.link at (0, 3, 1). :is(), :not() and :has() count their most specific argument, and :where()
counts nothing. The follow-up is the cascade around it: origin and importance, the style attribute and cascade layers
are all compared before specificity, and the order in the source only after it.
Overflowing flex items. The initial min-width of a flex item is auto, which for a flex item means a minimum
based on its content: flex items do not shrink below the length of their longest word or unwrapped line. Text with
white-space: nowrap, or a long web address, therefore keeps the item wide, and the row overflows instead of
truncating. The fix is min-inline-size: 0 on the item, which the task below measures. The follow-up: the automatic
minimum is zero for an item that is itself a scroll container, which is why overflow: hidden on the item also
“fixes” it, sometimes by cutting off what it should not.
Container or media queries. A media query asks about the window, the device or the reader’s settings; a
container query asks about the box a component sits in. The same card placed in a 300 px sidebar and in a 900 px
column can only adapt to both with a container query, and Baseline lists container size queries as widely available.
Media queries remain the tool for the page’s own layout and for preferences such as prefers-reduced-motion. The
follow-up: the container needs container-type: inline-size, which stops its width from depending on its content,
so it must get its width from its parent, and which also makes it a stacking context.
Flexbox or grid. Flexbox lays out items along one line, which may wrap, and lets each item’s content decide its size; grid places items in rows and columns that line up whatever the content. A row of tags or buttons is flexbox; a gallery of cards whose edges must align is grid. The follow-up: the two combine, a grid for the page or the cards, flexbox inside each card.
A stacking demo you can build in five minutes
The demo is a header with an open account menu and a banner below it. The menu has z-index: 1000 and the banner
z-index: 2, and the banner still covers the menu:
stacking/menu.css
/* The bug: the open menu has z-index: 1000, yet the banner covers it. */
body { margin: 0; font: 1rem/1.5 system-ui, sans-serif; }
.bar {
position: sticky; /* sticky positioning makes .bar a stacking context */
top: 0;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: #ffffff;
}
.account { position: relative; }
.account__menu {
position: absolute;
inset-block-start: 100%;
inset-inline-end: 0;
z-index: 1000; /* compared only with boxes inside .bar's stacking context */
min-inline-size: 10rem;
margin: 0;
padding: 0.5rem 0;
list-style: none;
background: #ffffff;
border: 1px solid #8a8f9c;
}
.account__menu a { display: block; padding: 0.5rem 1rem; }
.banner {
position: relative;
z-index: 2; /* in the root stacking context: above .bar's 1, menu and all */
margin: 0;
padding: 1.5rem 1rem;
background: #ffe7a3;
}
.products { margin: 0; padding: 1rem; } stacking/menu-fixed.css
/* The fix: main gets a stacking context of its own, so nothing inside it can rise above the header. */
main { isolation: isolate; } stacking/menu.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stacking demo</title>
<link rel="stylesheet" href="menu.css">
<!-- The fix: add <link rel="stylesheet" href="menu-fixed.css"> here. -->
</head>
<body>
<header class="bar">
<a class="logo" href="#">Corner Shop</a>
<nav class="account" aria-label="Account">
<button class="account__button" type="button" aria-expanded="true" aria-controls="account-menu">Account</button>
<ul class="account__menu" id="account-menu">
<li><a href="#orders">Orders</a></li>
<li><a href="#settings">Settings</a></li>
<li><a href="#sign-out">Sign out</a></li>
</ul>
</nav>
</header>
<main>
<p class="banner">Delivery is on us this week.</p>
<p class="products">Products go here.</p>
</main>
</body>
</html> In Chromium and Firefox alike, document.elementFromPoint() at the centre of the menu’s first two items,
“Orders” and “Settings”, returned the banner; the third item, below the banner, was visible. The model below follows
the painting order of CSS 2.1 for positioned boxes and shows why:
// A small model of stacking contexts, for the demo page: which box is painted later, and so on top, where the open
// menu and the banner overlap. It follows the order CSS 2.1 gives for positioned boxes (appendix E, simplified): inside
// each stacking context, children with a negative z-index first, then those with z-index auto or 0 in document order,
// then positive z-indexes from low to high; a stacking context is painted whole, with everything inside it.
const page = (mainIsolated) => ({
id: "html", context: "the root element",
children: [
{ id: "header.bar", z: 1, context: "sticky positioning", children: [
{ id: "ul.account__menu", z: 1000, context: "absolute positioning with a z-index" },
] },
{ id: "main", z: "auto", context: mainIsolated ? "isolation: isolate" : null, children: [
{ id: "p.banner", z: 2, context: "relative positioning with a z-index" },
] },
],
});
// The boxes of one stacking context in paint order, bottom to top; a child context brings its own boxes with it.
function paint(context) {
const members = [];
const collect = (node) => {
for (const child of node.children ?? []) {
members.push(child);
if (!child.context) collect(child); // a box that is no context of its own is stacked in this one
}
};
collect(context);
const level = (box) => (box.z === "auto" ? 0 : box.z);
const order = members
.map((box, i) => ({ box, i }))
.sort((a, b) => level(a.box) - level(b.box) || a.i - b.i) // document order on ties
.map(({ box }) => box);
return [context, ...order.flatMap((box) => (box.context ? paint(box) : [box]))];
}
for (const isolated of [false, true]) {
const order = paint(page(isolated));
console.log(isolated ? "With main { isolation: isolate }:" : "As written:");
console.log(` paint order, bottom to top: ${order.map((b) => b.id).join(", ")}`);
for (const box of order.filter((b) => b.context && b.id !== "html")) console.log(` ${box.id} is a stacking context (${box.context}), z-index ${box.z}`);
const top = order.findLastIndex((b) => b.id === "ul.account__menu") > order.findLastIndex((b) => b.id === "p.banner") ? "the menu" : "the banner";
console.log(` where the menu and the banner overlap, ${top} is on top\n`);
} Output
As written:
paint order, bottom to top: html, main, header.bar, ul.account__menu, p.banner
header.bar is a stacking context (sticky positioning), z-index 1
ul.account__menu is a stacking context (absolute positioning with a z-index), z-index 1000
p.banner is a stacking context (relative positioning with a z-index), z-index 2
where the menu and the banner overlap, the banner is on top
With main { isolation: isolate }:
paint order, bottom to top: html, main, p.banner, header.bar, ul.account__menu
main is a stacking context (isolation: isolate), z-index auto
p.banner is a stacking context (relative positioning with a z-index), z-index 2
header.bar is a stacking context (sticky positioning), z-index 1
ul.account__menu is a stacking context (absolute positioning with a z-index), z-index 1000
where the menu and the banner overlap, the menu is on top
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node paint-order.mjs
Where each z-index is compared in the menu demo
Text description of the diagram
The diagram shows boxes inside boxes. The outer box is html, the root stacking context, which compares the header's z-index of 1 with the banner's z-index of 2.
Inside it, at the top, is header.bar: sticky, z-index 1, and a stacking context of its own. Inside the header is ul.account__menu, at z-index 1000, which is compared only with other boxes inside header.bar.
Below the header, still inside html, is main, which has no stacking context of its own. Inside main is p.banner, at z-index 2. Because main does not isolate it, the banner is compared at the root, where its 2 beats the header's 1, so it is painted over the whole header, the menu included.
The sticky header is a stacking context with z-index 1, so everything inside it, the menu included, is painted as part of that one layer. At the root, the header’s 1 meets the banner’s 2, and the banner wins; the menu’s 1000 is compared only with other boxes inside the header. There are three honest fixes, and saying which you would choose and why is the answer interviewers listen for:
- Raise the header, for example to
z-index: 10. It works until a card in the page getsz-index: 11. - Isolate the content:
main { isolation: isolate; }givesmaina stacking context of its own, painted at the root’s z-index 0, so nothing inside it, whatever its z-index, can rise above the header. It changes nothing else about the layout. With the second style sheet linked, the menu was on top in both engines. - Remove the banner’s z-index, if nothing needs it. Most stray z-indexes are left over from an earlier fix.
A 30-minute task, step by step
A typical build round, written for this lesson: build the notification list of a web app. The panel is at most 30rem wide, with a heading and a “Mark all as read” button. Each notification is a link with the sender’s initials, a title on one line, a message on at most two lines, the time, and a dot when it is unread. It must work from 320 px wide, with the keyboard, in the dark scheme and in languages written right to left.
- Minutes 0 to 3, clarify. Done when the limits are agreed and written down: widths, how long text behaves, states, scripts and browsers.
- Minutes 3 to 8, semantic HTML. Done when a section with a heading, a button and a list of links works with no CSS at all.
- Minutes 8 to 15, layout. Done when each row is a flex line: the avatar, a text column that may shrink, the dot.
- Minutes 15 to 20, stress the content. Done when the longest name, a web address, 320 px and right to left have been tried.
- Minutes 20 to 25, states. Done when unread, hover and keyboard focus are visible.
- Minutes 25 to 30, accessibility pass. Done when the contrast is checked, the targets are large enough, the unread dot has a text alternative and the dark scheme works.
Say what you are doing as you go, briefly: “a list of links, because each notification opens something”, “the text
column gets min-inline-size: 0 so that the title can shrink and truncate”. A silent half hour leaves the interviewer
guessing at your reasons, and a stated reason earns credit even when time runs out before the code is finished.
task/notifications.css
/* The 30-minute task. Each group of rules says which step of the plan wrote it. */
/* Minutes 8 to 15, layout: a panel, a header that wraps, and a row per notification */
:root {
color-scheme: light dark;
--ink: #1c2333;
--muted: #535b6e;
--surface: #ffffff;
--canvas: #f3f4f8;
--line: #d5d9e3;
--hover: #eef1f7;
--accent: #b4400e;
--focus: #2f5bd3;
}
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; padding: 1rem; font: 1rem/1.5 system-ui, sans-serif; color: var(--ink); background: var(--canvas); }
.panel {
max-inline-size: 30rem;
margin-inline: auto;
border: 1px solid var(--line);
border-radius: 0.75rem;
background: var(--surface);
}
.panel__head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
padding: 0.75rem 1rem;
border-block-end: 1px solid var(--line);
}
.panel__head h2 { margin: 0; font-size: 1.125rem; }
.notes { margin: 0; padding: 0; list-style: none; }
.note {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: inherit;
text-decoration: none;
}
.note__avatar {
flex: none;
display: grid;
place-items: center;
inline-size: 2.5rem;
block-size: 2.5rem;
border-radius: 50%;
background: var(--line);
font-size: 0.875rem;
font-weight: 600;
}
.note__text {
flex: 1;
min-inline-size: 0; /* without it, the title's unwrapped line sets the smallest width of the row */
}
.note__title, .note__message, .note__time { display: block; }
.note__title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.note__message {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
font-size: 0.875rem;
line-height: 1.25rem;
}
.note__time { color: var(--muted); font-size: 0.875rem; line-height: 1.25rem; }
.note__dot {
flex: none;
inline-size: 0.625rem;
block-size: 0.625rem;
margin-block-start: 0.5rem;
border-radius: 50%;
background: var(--accent);
}
/* Minutes 15 to 20, stress: a web address or a long word wraps instead of widening the message */
.note__message { overflow-wrap: anywhere; }
/* Minutes 20 to 25, states: unread rows have a bold title and a dot; hover and keyboard focus are visible */
.note--unread .note__title { font-weight: 700; }
.note:hover { background: var(--hover); }
.panel__action {
min-block-size: 2.75rem;
padding-inline: 1rem;
border: 1px solid var(--line);
border-radius: 0.5rem;
background: transparent;
color: inherit;
font: inherit;
}
.note:focus-visible, .panel__action:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }
/* Minutes 25 to 30, accessibility pass: colours for the dark scheme, checked with contrast.mjs */
@media (prefers-color-scheme: dark) {
:root {
--ink: #e9ecf3;
--muted: #a9b0c2;
--surface: #171c2a;
--canvas: #0e121c;
--line: #2c3345;
--hover: #20273a;
--accent: #ff9a5c;
--focus: #8cb8ff;
}
} task/notifications.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notifications</title>
<link rel="stylesheet" href="notifications.css">
</head>
<body>
<!-- Agreed in the first minutes: 320 px and up, at most 30rem wide; titles one line, messages two;
keyboard, dark scheme and right-to-left must work; current browsers only. -->
<section class="panel" aria-labelledby="panel-title">
<header class="panel__head">
<h2 id="panel-title">Notifications</h2>
<button class="panel__action" type="button">Mark all as read</button>
</header>
<ul class="notes" role="list">
<li>
<a class="note note--unread" href="#note-1">
<span class="note__avatar" aria-hidden="true">RK</span>
<span class="note__text">
<span class="note__title">Riya Kapoor commented on “Fix the sticky header on small screens”</span>
<span class="note__message">Looks good on my phone. One thing: the menu still opens under the banner when you scroll halfway down the page.</span>
<span class="note__time"><time datetime="PT5M">5 min ago</time></span>
</span>
<span class="note__dot" role="img" aria-label="Unread"></span>
</a>
</li>
<li>
<a class="note note--unread" href="#note-2">
<span class="note__avatar" aria-hidden="true">TS</span>
<span class="note__text">
<span class="note__title">Tanvir Siddiqui approved your change</span>
<span class="note__message">Approved.</span>
<span class="note__time"><time datetime="PT1H">1 h ago</time></span>
</span>
<span class="note__dot" role="img" aria-label="Unread"></span>
</a>
</li>
<li>
<a class="note" href="#note-3">
<span class="note__avatar" aria-hidden="true">LV</span>
<span class="note__text">
<span class="note__title">Lakshminarayanan Venkatasubramanian mentioned you in “Design tokens for the dark scheme”</span>
<span class="note__message">Can you check the muted text against the new surface colour? The numbers are at https://example.org/tokens/dark-scheme-contrast-report-final</span>
<span class="note__time"><time datetime="P1D">Yesterday</time></span>
</span>
</a>
</li>
<li>
<a class="note" href="#note-4">
<span class="note__avatar" aria-hidden="true">W</span>
<span class="note__text">
<span class="note__title">Your weekly summary is ready</span>
<span class="note__message">You closed 12 issues this week.</span>
<span class="note__time"><time datetime="P2D">2 days ago</time></span>
</span>
</a>
</li>
</ul>
</section>
</body>
</html> The comments in the style sheet mark the step that wrote each group of rules. Measured at 320 and 480 px in both
engines, the page never scrolled sideways, every title that did not fit ended in an ellipsis, long messages stopped at
two lines, and the rows were 88 or 108 px tall, far above the 24 by 24 px minimum target of WCAG 2.2 (success
criterion 2.5.8); the button was 44 px tall. Tab reached the button first and then each notification, each drawing a
3 px focus ring inside its edge. With dir="rtl" on the page, every avatar moved to the right edge, because the layout
uses flexbox and logical properties and nothing in it says left or right.
Two measurements make good interview material. Without min-inline-size: 0 on the text column, the page scrolled
sideways at both widths in both engines: on a 320 px screen it grew to 774 px wide in Chromium and 777 px in Firefox,
because the titles’ unwrapped lines set the rows’ minimum width, exactly as the explain-it-back list says. And at 320 px, the message “You closed 12 issues this week.” fitted on one line in
Chromium but wrapped onto a second line in Firefox, making that row 20 px taller. Engines draw the same text at
slightly different widths, so no layout should depend on a line fitting with a pixel to spare.
The accessibility pass, as numbers: the program reads the colour tokens of the style sheet, light and dark, and checks each pair the panel draws.
// The accessibility pass of the task, as numbers: reads the colour custom properties of notifications.css, light and
// dark, and checks each pair the panel draws against WCAG 2.2: 4.5:1 for text (1.4.3), 3:1 for the unread dot and the
// focus ring, which are not text (1.4.11).
import { readFileSync } from "node:fs";
const css = readFileSync("notifications.css", "utf8");
const block = (from) => css.slice(css.indexOf("{", css.indexOf(from)) + 1, css.indexOf("}", css.indexOf(from)));
const tokens = (text) => Object.fromEntries([...text.matchAll(/--([\w-]+):\s*(#[0-9a-f]{6})/gi)].map((m) => [m[1], m[2]]));
const light = tokens(block(":root"));
const dark = { ...light, ...tokens(block("prefers-color-scheme: dark")) };
const luminance = (hex) => {
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const ratio = (a, b) => {
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
};
const pairs = [
["title and message on the panel", "ink", "surface", 4.5],
["time on the panel", "muted", "surface", 4.5],
["time on a row under the pointer", "muted", "hover", 4.5],
["initials on the avatar", "ink", "line", 4.5],
["unread dot on the panel", "accent", "surface", 3],
["focus ring on the panel", "focus", "surface", 3],
];
console.log(`${"pair".padEnd(34)}needs light dark`);
let failures = 0;
for (const [label, fg, bg, need] of pairs) {
const cells = [light, dark].map((t) => {
const r = ratio(t[fg], t[bg]);
if (r < need) failures++;
return `${r.toFixed(2)}:1${r < need ? "!" : " "}`.padStart(9);
});
console.log(`${label.padEnd(34)}${String(need).padEnd(5)}${cells.join("")}`);
}
console.log(failures ? `\n${failures} pairs fail (marked !)` : "\nEvery pair passes in both schemes."); Output
pair needs light dark title and message on the panel 4.5 15.70:1 14.37:1 time on the panel 4.5 6.80:1 7.83:1 time on a row under the pointer 4.5 6.01:1 6.85:1 initials on the avatar 4.5 11.11:1 10.65:1 unread dot on the panel 3 5.71:1 8.11:1 focus ring on the panel 3 5.90:1 8.43:1 Every pair passes in both schemes.
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node contrast.mjs
Back your answers with evidence
“It works in all browsers” invites the question “which ones?”. An answer with evidence names its source. For browser support, the source is Baseline: a feature is newly available once it works in the latest versions of the core browsers, Chrome on desktop and Android, Edge on desktop, Firefox on desktop and Android and Safari on macOS and iOS, and widely available 30 months later. This program turns the Baseline data of the features used above into sentences you can say:
// Turns Baseline data into sentences you can say in an interview: for each feature the timed task, the stacking demo
// and the model answers rely on, its status and its years, from features.json (a few fields of web-features).
import { readFileSync } from "node:fs";
const data = JSON.parse(readFileSync("features.json", "utf8"));
const groups = { task: "the notification task", demo: "the stacking demo", answer: "the explain-it-back answers" };
function sentence(f) {
if (f.baseline === "high") return `${f.name}: Baseline widely available, newly available in ${f.newly} and widely in ${f.widely}.`;
if (f.baseline === "low") return `${f.name}: Baseline newly available in ${f.newly}, so keep a fallback for older browsers.`;
return `${f.name}: limited availability, missing from at least one core browser.${f.note ? ` web-features notes: "${f.note}"` : ""}`;
}
for (const [use, label] of Object.entries(groups)) {
console.log(`Features of ${label}`);
for (const f of Object.values(data.features).filter((x) => x.usedIn === use)) console.log(` ${sentence(f)}`);
console.log("");
}
const all = Object.values(data.features);
const count = (status) => all.filter((f) => f.baseline === status).length;
console.log(`${all.length} features: ${count("high")} widely available, ${count("low")} newly available, ${count(false)} limited.`);
console.log(`Source: ${data.source.split(", data.json")[0]}.`); Output
Features of the notification task Flexbox gap: Baseline widely available, newly available in 2021 and widely in 2023. Logical properties: Baseline widely available, newly available in 2021 and widely in 2024. :focus-visible: Baseline widely available, newly available in 2022 and widely in 2024. Text overflow: Baseline widely available, newly available in 2015 and widely in 2018. line-clamp: limited availability, missing from at least one core browser. web-features notes: "The prefixed -webkit-line-clamp is widely supported but only works with -webkit-box-orient: vertical in combination with display: -webkit-box or display: -webkit-inline-box." prefers-color-scheme media query: Baseline widely available, newly available in 2020 and widely in 2022. Features of the stacking demo Sticky positioning: Baseline widely available, newly available in 2019 and widely in 2022. isolation: Baseline widely available, newly available in 2020 and widely in 2022. Features of the explain-it-back answers Container queries (size): Baseline widely available, newly available in 2023 and widely in 2025. :has(): Baseline widely available, newly available in 2023 and widely in 2026. Anchor positioning: limited availability, missing from at least one core browser. 11 features: 9 widely available, 0 newly available, 2 limited. Source: web-features 3.42.0 (npm).
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node evidence.mjs
The line about line-clamp is the kind of detail that earns trust: the standard property is not available everywhere
yet, but the prefixed -webkit-line-clamp, which the task uses with display: -webkit-box, is widely supported.
For accessibility, quote the success criterion and its number. These are the ones CSS decides most often:
| Requirement in WCAG 2.2 | The number |
|---|---|
| Text contrast, 1.4.3 | 4.5:1, or 3:1 for text of at least 24 px, or 18.66 px and bold |
| Contrast of controls and graphics, 1.4.11 | 3:1 against what is next to them |
| Reflow, 1.4.10 | no scrolling in two directions at 320 px wide |
| Resizing text, 1.4.4 | text up to 200 % without loss of content |
| Text spacing, 1.4.12 | line height 1.5, paragraphs 2, letters 0.12 and words 0.16 times the font size |
| Target size, 2.5.8 | 24 by 24 px, or enough space around a smaller target |
| Focus, 2.4.7 and 2.4.11 | a visible focus indicator, never entirely hidden by content the page adds |
For performance, quote what you measured and against which line: the size of the style sheet, how long it blocks the first render, or the layout shift, where a Cumulative Layout Shift of 0.1 or less at the 75th percentile of page loads is good. A number you measured yourself, with the browser and the width, is the strongest evidence of all, as the 20 px row above shows.
Practise like the real round
- Say each explain-it-back answer aloud against a one-minute timer, then check it against the specification section cited above, and write down what you left out.
- Build a small component against a 30-minute timer, then test it at 320 px, with Tab, in the dark scheme and right to left. Note which step ran over.
- Practise the follow-ups as well as the first question. Interviewers vary their questions, so a memorised answer to a question seen before often fails at the second question.
- When you do not know, say what you do know, reason from the mechanism, and say how you would find out: DevTools, the specification, or a five-minute experiment.
Self-check
Check yourself
10 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
An interviewer asks about a CSS feature you have never used. How do you answer? Say so, then reason in the open. Describe what you know of the problem the feature solves and the mechanism it probably builds on, for example “a container query must know the container’s size before the children are laid out, so I would expect the container’s size to be independent of its content”. Say how you would find out the rest: the specification or MDN for the syntax, Baseline for support, and a small experiment in DevTools. Interviewers learn more from a clear guess with its reasons, and a plan to check it, than from a confident answer that falls apart at the first follow-up.
In a 30-minute live CSS task, what do you do in the first five minutes? Clarify, then write HTML. Ask about the widths to support, how long the content can be, which states matter, whether right-to-left or the dark scheme are in scope, and which browsers count, and write the answers as a comment at the top of the file. Then write the semantic HTML, headings, lists, buttons and links, so the page works before any CSS. Say your plan aloud in one sentence, so the interviewer can correct it early, instead of discovering a misunderstanding at minute 25.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
In a 45-minute live round you must build a pricing section: three plan cards side by side on wide screens and stacked on phones, the middle plan marked as recommended, feature lists of different lengths, and every card's button aligned at the bottom. Plan your time, and say what you would cut if you ran short.
-
Mid to senior
You propose container queries and :has() for a redesign. A senior engineer says they are too new, because many of your users have older Android phones. How do you answer with evidence, and what would you put in place before shipping?
Key takeaways
- Prepare two skills separately: explaining mechanisms, and building against the clock.
- Keep an explain-it-back list: margin collapsing, stacking contexts, specificity, overflowing flex items, container queries, flexbox or grid; one minute, one demo and one follow-up each.
- A z-index is compared only inside its stacking context;
isolation: isolatekeeps a region’s z-indexes inside it. - A flex item cannot shrink below its content until it has
min-inline-size: 0. - Use one 30-minute routine: clarify, HTML, layout, stress, states, accessibility; and talk while you work.
- Back claims with evidence: Baseline status, the WCAG criterion and its number, and what you measured.
References
- CSS Cascading and Inheritance Level 5: cascade sorting order (W3C CSS Working Group)
- Selectors Level 4: calculating a selector's specificity (W3C CSS Working Group)
- CSS 2.1: collapsing margins (8.3.1) and the painting order (appendix E) (W3C CSS Working Group)
- CSS Flexible Box Layout Module Level 1: automatic minimum size of flex items (W3C CSS Working Group)
- CSS Conditional Rules Module Level 5: container queries (W3C CSS Working Group)
- Stacking context (MDN Web Docs) (Mozilla)
- Web Content Accessibility Guidelines (WCAG) 2.2 (W3C)
- Baseline (W3C WebDX Community Group)
- web-features (npm package and data) (W3C WebDX Community Group)
- Cumulative Layout Shift (CLS) (Google (web.dev))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress