Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with Node.js 24.21.0
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

The menu under the banner

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:

Which box is painted last where they overlap JavaScript · stacking/paint-order.mjs
// 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

Nested stacking contexts: the root compares the header at z-index 1 with the banner at 2; the menu at 1000 stays inside the header.html: the root contextheader 1 against banner 2header: sticky, z-index 1a stacking contextmain: not a contextmenu: z-index 1000counts only in the headerbanner: z-index 2counts at the root

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 gets z-index: 11.
  • Isolate the content: main { isolation: isolate; } gives main a 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.

  1. Minutes 0 to 3, clarify. Done when the limits are agreed and written down: widths, how long text behaves, states, scripts and browsers.
  2. 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.
  3. Minutes 8 to 15, layout. Done when each row is a flex line: the avatar, a text column that may shrink, the dot.
  4. Minutes 15 to 20, stress the content. Done when the longest name, a web address, 320 px and right to left have been tried.
  5. Minutes 20 to 25, states. Done when unread, hover and keyboard focus are visible.
  6. 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.

The task, solved in 30 minutes

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.

Contrast of the task's colours in both schemes JavaScript · task/contrast.mjs
// 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

HTML, CSS & JS Playground (Online Editor) Paste notifications.html and notifications.css into its panes and try the task at 320 px yourself.

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:

Baseline status, as sentences JavaScript · evidence.mjs
// 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.

  1. Question 1 of 10 Two paragraphs follow each other in normal flow. The first has margin-bottom: 24px and the second margin-top: 16px. How far apart are they?

    Type a number.

    Show the answer to question 1

    Answer: 24 px

    Adjoining vertical margins of block boxes in normal flow collapse into one margin as large as the larger of them, so the gap is 24 px, not 40 (CSS 2.1, section 8.3.1).

  2. Question 2 of 10 The same two paragraphs are put in a flex container with flex-direction: column and no gap. How far apart are they now?

    Choose one answer.

    Show the answer to question 2

    Answer: 40 px: margins do not collapse between flex items

    Margin collapsing is a rule of normal flow. Inside a flex or grid container, every margin is kept, so 24 and 16 add up to 40 px.

  3. Question 3 of 10 A menu with z-index: 1000 sits inside a header with position: sticky and z-index: 1. A banner after the header has position: relative and z-index: 2. Which is on top where the menu and the banner overlap?

    Choose one answer.

    Show the answer to question 3

    Answer: The banner: the menu's 1000 counts only inside the header's stacking context

    The sticky header with a z-index is a stacking context, painted as one layer at its own z-index of 1. At the root level the header's 1 meets the banner's 2, so the banner is painted later, over everything inside the header.

  4. Question 4 of 10 Which change puts the menu above the banner without changing any z-index value?

    Choose one answer.

    Show the answer to question 4

    Answer: main { isolation: isolate; } on the element that holds the banner

    isolation: isolate gives main its own stacking context, painted at the root's z-index 0, so nothing inside it can rise above the header's 1. The other two changes leave the header and the banner where they are.

  5. Question 5 of 10 Both rules set the colour of the same link. Which selector wins: #nav a, or .menu .item a.link?

    Choose one answer.

    Show the answer to question 5

    Answer: #nav a: (1, 0, 1) beats (0, 3, 1), because IDs are compared first

    Specificity is compared count by count from the left, and the first difference decides. One ID outweighs any number of classes. Order in the source decides only when the specificities are equal.

  6. Question 6 of 10 Which selector wins for a link with the class link: :where(#nav) a, or a.link?

    Choose one answer.

    Show the answer to question 6

    Answer: a.link, at (0, 1, 1), because :where() adds nothing

    :where() contributes zero specificity whatever it contains, so :where(#nav) a is (0, 0, 1). :is() would have counted its most specific argument, the ID.

  7. Question 7 of 10 A rule in @layer components uses an ID selector. An unlayered rule uses one class. Both set the same property with normal declarations. Which wins?

    Choose one answer.

    Show the answer to question 7

    Answer: The unlayered rule: layers are compared before specificity, and unlayered styles come last

    The cascade compares cascade layers before specificity. For normal declarations, styles outside every layer win over layered ones, so specificity is never reached.

  8. Question 8 of 10 A flex item holds a title with white-space: nowrap and text-overflow: ellipsis, but no ellipsis appears and the row is wider than the screen. What fixes it?

    Choose one answer.

    Show the answer to question 8

    Answer: min-inline-size: 0 on the flex item, whose automatic minimum width is its content's

    flex-shrink is already 1 by default. The item cannot shrink below its automatic minimum, the width of its unwrapped title. With min-inline-size: 0 it can, and the title then overflows its own box, where the ellipsis appears. In the lesson's task, leaving it out made the page 774 px wide on a 320 px screen.

  9. Question 9 of 10 The same product card appears in a 300 px sidebar and in a 900 px main column of one page. Which query lets it change its layout in each?

    Choose one answer.

    Show the answer to question 9

    Answer: A container query, with container-type: inline-size on each place that holds cards

    The window is the same for both cards, so a media query cannot tell them apart. A container query asks about the box the card sits in, which is 300 px in one place and 900 px in the other.

  10. Question 10 of 10 A box has width: 300px, padding: 20px, a 2px border and box-sizing: content-box. How wide is its border box, in px?

    Type a number.

    Show the answer to question 10

    Answer: 344 px

    With content-box, width sets only the content. The border box adds the padding and the border on both sides: 300 + 2 × 20 + 2 × 2 = 344 px. With box-sizing: border-box it would be 300 px.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

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: isolate keeps 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.