Skip to content
Frontend

12 Frontend HTML and CSS Interview Questions and Answers

This focused guide turns RecallDeck’s curated Frontend HTML and CSS material into 12 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

13 min read12 detailed answersReviewed Aug 24, 2026
What to remember

Connect every framework answer to browser behavior, accessibility, performance, or user-visible state; that is where senior frontend judgment becomes visible.

Question set

12 detailed answers

01

What is semantic HTML and why does it matter (accessibility, SEO, maintainability)?

Short answer: Semantic HTML means using tags for their meaning (<nav>, <article>, <button>) instead of <div> for everything. Browsers and assistive tech understand the document structure, which buys you accessibility, SEO, and readable code.

In depth:

  1. Accessibility (a11y) — screen readers build the accessibility tree from tags: <nav>, <main>, <h1> enable landmark and heading navigation. Div soup is announced as unstructured text.
  2. SEO — search engines treat <article>, <h1><h6>, <time> as structure and content-importance signals.
  3. Maintainability<header>/<footer>/<aside> self-document the markup; you don't read class names to learn a block's role.
  4. Free behavior<button> is focusable and reacts to Enter/Space, <a> navigates, <details> toggles — no JS required.
<!-- ❌ div soup -->
<div class="nav"><div class="link" onclick="go()">Home</div></div>

<!-- ✅ semantic -->
<header>
  <nav aria-label="Primary">
    <a href="/">Home</a>
  </nav>
</header>
<main>
  <article>
    <h1>Article title</h1>
    <time datetime="2026-06-30">June 30, 2026</time>
  </article>
</main>

⚠️ Common mistake: making a clickable <div onclick> instead of a <button> — you lose focus, keyboard handling, and the screen-reader role.

02

Explain the CSS box model and how `box-sizing: border-box` changes sizing.

Short answer: Every element is nested layers: content → padding → border → margin. By default (content-box) width sizes only the content, and padding plus border are added on top. border-box makes padding and border fit inside the declared width.

In depth:

  1. content — the content area, which width/height refer to under content-box.
  2. padding — inner spacing, painted with the element's background.
  3. border — the frame between padding and margin.
  4. margin — outer spacing, transparent; adjacent vertical margins collapse (margin collapse).
┌──────────── margin ────────────┐
│  ┌───────── border ─────────┐  │
│  │  ┌────── padding ─────┐  │  │
│  │  │     content        │  │  │
│  │  └────────────────────┘  │  │
│  └──────────────────────────┘  │
└────────────────────────────────┘
/* content-box: actual width = 200 + 2*16 + 2*2 = 236px */
.a { box-sizing: content-box; width: 200px; padding: 16px; border: 2px solid; }

/* border-box: actual width is exactly 200px, padding/border inside */
.b { box-sizing: border-box; width: 200px; padding: 16px; border: 2px solid; }

/* common reset */
*, *::before, *::after { box-sizing: border-box; }

⚠️ Common mistake: setting width: 100% plus padding under content-box — the box overflows its container; border-box fixes it.

03

Flexbox vs CSS Grid: how do they differ and when do you choose each?

Short answer: Flexbox is one-dimensional layout (a row OR a column), Grid is two-dimensional (rows AND columns at once). Flex excels at distributing items along a single axis; Grid excels at controlling a layout on a grid.

In depth:

  1. Flexbox — lays content along a main axis; sizes often come from content (content-out). Ideal for toolbars, navbars, rows of buttons, centering.
  2. Grid — you define a grid with a template (layout-in) and place items into cells/areas. Ideal for overall page layout, card grids, aligning across rows AND columns.
  3. They combine — typically Grid for the page frame, Flex inside components.
Criterion Flexbox Grid
Axes one (1D) two (2D)
Approach content-driven grid-template-driven
Strength distribute in a row/column rows + columns together
Example navbar, toolbar page layout, gallery
/* Flex: items in a row, push to edges, vertically centered */
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; }

/* Grid: responsive card grid with no media queries */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }

⚠️ Common mistake: building a two-dimensional layout out of nested flex containers — columns won't line up across rows; that's a job for Grid.

04

How is selector specificity calculated and how does the cascade resolve conflicts? Where do `!important` and `@layer` fit?

Short answer: Specificity is a triple (id, class, type): an # outweighs any number of classes, a class outweighs any number of tags. On ties, the later-declared rule wins. !important and @layer sit above the normal cascade.

In depth:

  1. Counting (a,b,c) — a = id selectors, b = classes/attributes/pseudo-classes, c = types/pseudo-elements. Compared left to right.
  2. Inline styles — outweigh any selector (effectively "1,0,0,0").
  3. Ties break on order — the last matching rule in source order wins.
  4. !important — lifts a declaration above the normal cascade; only beaten by another !important with higher specificity.
  5. @layer — cascade layers: layer order beats specificity within them (a later layer beats an earlier one), letting you control priority without a specificity arms race.
/* specificity: */
a { }                    /* 0,0,1 */
.btn { }                 /* 0,1,0 — beats a */
nav a.btn { }            /* 0,1,2 */
#cta { }                 /* 1,0,0 — beats everything above */

@layer base, theme;      /* theme declared later → wins */
@layer base  { .btn { color: black; } }
@layer theme { .btn { color: rebeccapurple; } }  /* wins */

⚠️ Common mistake: silencing conflicts with !important — it escalates the specificity war; cascade layers or a less specific selector are usually the right fix.

05

How do `position` values differ: static, relative, absolute, fixed, sticky? Give a real use for sticky.

Short answer: static — normal flow; relative — offset from its own spot without leaving flow; absolute — positioned relative to the nearest positioned ancestor and removed from flow; fixed — relative to the viewport; sticky — a hybrid that acts like relative until it hits a scroll threshold, then "sticks".

In depth:

  1. static — the default; top/left/z-index are ignored.
  2. relative — shifts by offsets but keeps its place in flow; establishes a containing block for absolute descendants.
  3. absolute — removed from flow, measured from the nearest ancestor with position != static.
  4. fixed — removed from flow, pinned to the viewport (banners, modals).
  5. sticky — needs a threshold (top, bottom…) and a scrollable container; sticks within its parent.
position In flow Reference
static yes
relative yes its own spot
absolute no positioned ancestor
fixed no viewport
sticky yes→stuck ancestor + scroll threshold
/* a table header that sticks to the top while scrolling */
thead th { position: sticky; top: 0; background: white; z-index: 1; }

⚠️ Common mistake: sticky silently fails if an ancestor has overflow: hidden/auto or you forgot the threshold (top).

06

What is a stacking context and why does a higher `z-index` sometimes do nothing?

Short answer: z-index is only compared within a single stacking context. If two elements live in different contexts, their ancestor contexts decide order — not their own z-index — so a "999999" can be powerless against a neighboring subtree.

In depth:

  1. Root<html> creates the root stacking context.
  2. What creates a new contextposition != static + z-index ≠ auto; any opacity < 1; transform, filter, perspective, will-change; isolation: isolate; a flex/grid child with z-index.
  3. Compared locally — within a context, elements sort by z-index, then DOM order.
  4. Across contexts — the z-index of their parent contexts decides, not the nested children.
root (html)
├─ A  z-index:1   ← creates context A
│   └─ A1 z-index:9999   (trapped inside A)
└─ B  z-index:2   ← context B
    └─ B1 z-index:1

B1 paints ON TOP of A1: B(2) > A(1), even though 9999 > 1
/* the "fix": raise the parent context, not the child */
.panel-a { position: relative; z-index: 3; }
/* or isolate a subtree from its neighbors */
.widget { isolation: isolate; }

⚠️ Common mistake: bumping a child's z-index while forgetting that an opacity/transform on an ancestor already trapped it in its own context.

07

How do you build responsive layouts: mobile-first, relative units, media queries, and modern container queries?

Short answer: Mobile-first means base styles target the narrow screen and you scale up with min-width media queries. Use relative units (rem, em, %, vw) over pixels. Container queries respond to a parent's size, not the viewport — that's how you get adaptive components.

In depth:

  1. Mobile-first — base styles with no media queries, then @media (min-width: …) grows the layout; fewer overrides.
  2. Relative unitsrem off the root (scales with the user's font size), em off the parent, %/vw off context; pixels for borders/shadows.
  3. Media queries — respond to the viewport; choose breakpoints by content, not by device models.
  4. Container queries — a component adapts to its container: the same block looks different in a sidebar vs the main column.
/* mobile-first media query */
.grid { display: grid; gap: 16px; }
@media (min-width: 48rem) { .grid { grid-template-columns: 1fr 1fr; } }

/* container queries */
.card-wrap { container-type: inline-size; }
@container (min-width: 30rem) {
  .card { grid-template-columns: 120px 1fr; }
}

⚠️ Common mistake: building desktop-first with max-width and a pile of overrides — mobile CSS bloats; and setting font sizes in px, which breaks user zoom.

08

How do you scale CSS in a large app: BEM, CSS Modules, CSS-in-JS, utility-first (Tailwind) — the tradeoffs?

Short answer: All four address the same problem — CSS being global and prone to name clashes. BEM is a naming convention, CSS Modules localizes class names at build time, CSS-in-JS keeps styles in JS with runtime dynamics, utility-first composes prebuilt atomic classes in the markup.

In depth:

  1. BEMblock__element--modifier; works with any stack, zero tooling, but names are long and discipline is on humans.
  2. CSS Modulesstyles.button is hashed to a unique class; isolation by default, plain CSS, but no runtime dynamics.
  3. CSS-in-JS — styles colocated with the component, easy to bind to props/theme; cost is a runtime or heavier build; the trend has shifted toward zero-runtime solutions.
  4. Utility-first (Tailwind) — compose atomic classes; fast to build, small shipped CSS, but "noisy" markup and a learning curve.
Approach Isolation Dynamics Cost
BEM by convention none discipline
CSS Modules build-time weak needs bundler
CSS-in-JS runtime/build strong weight/runtime
Utility (Tailwind) global-atomic via classes noisy HTML

⚠️ Common mistake: mixing several methodologies with no rules — both shipped CSS and cognitive load grow; pick one primary approach.

09

How do pseudo-classes differ from pseudo-elements? How do `:is()`, `:where()`, `:has()` work and what's their specificity?

Short answer: A pseudo-class (:hover, :focus) targets a state of an existing element — one colon; a pseudo-element (::before, ::first-line) creates/styles a part not in the DOM — two colons. :is()/:where() group selectors, and :has() is the "parent" selector.

In depth:

  1. Pseudo-class — state/position: :hover, :nth-child(2), :checked. Syntax :name.
  2. Pseudo-element — a virtual part: ::before, ::after, ::placeholder, ::marker. Syntax ::name.
  3. :is(...) — shortens groups; takes the specificity of its heaviest argument.
  4. :where(...) — same, but specificity is always 0,0,0 — handy for base/resettable styles.
  5. :has(...) — matches an element by its descendants/children's state (parent selector); specificity is the heaviest argument.
/* without :is — verbose */
.post h1, .post h2, .post h3 { margin-top: 0; }
/* with :is — compact (specificity 0,1,1) */
.post :is(h1, h2, h3) { margin-top: 0; }

/* :where — zero specificity, easy to override */
:where(ul, ol) { padding-left: 1rem; }

/* :has — style a form when it contains an invalid field */
form:has(input:invalid) .submit { opacity: .5; }

⚠️ Common mistake: assuming :is() has zero specificity — only :where() does.

10

How do CSS custom properties work: cascade/scope, runtime theming, and how do they differ from Sass variables?

Short answer: Custom properties (--name) inherit and cascade like ordinary properties, live at runtime, and are read via var(). Unlike Sass variables, which the compiler inlines at build time, CSS variables can change dynamically — via classes, media queries, or JS.

In depth:

  1. Declare and read--gap: 16px; then gap: var(--gap, 8px); (the second arg is a fallback).
  2. Cascade and scope — the value inherits down the tree; override it on any selector/:root/inline.
  3. Runtime — they react to :hover, media queries, theme; read/written from JS via getPropertyValue/setProperty.
  4. vs Sass — Sass variables are static: after compilation they're gone from the CSS, so you can't switch themes on the fly.
:root { --bg: white; --fg: #111; }
[data-theme="dark"] { --bg: #111; --fg: #eee; }  /* theme switch via override */

.card { background: var(--bg); color: var(--fg); }

@media (prefers-color-scheme: dark) {
  :root { --bg: #111; --fg: #eee; }
}
// read/write from JS
const root = document.documentElement;
root.style.setProperty('--bg', '#0b0b0b');

⚠️ Common mistake: expecting var() to do Sass-style math — use calc() for computation, and you can't write media queries against the variables themselves.

11

What's the difference between block, inline, and inline-block, and what is normal document flow?

Short answer: Block elements take the full line width and accept any size/spacing; inline elements sit within a line, sized by content, and ignore width/vertical margins; inline-block sits inline but with controllable sizing. Normal flow is the order in which elements lay out top-to-bottom (block) and left-to-right (inline) without positioning.

In depth:

  1. block (<div>, <p>, <section>) — starts a new line, defaults to full width, honors width/height and all margins/padding.
  2. inline (<span>, <a>, <strong>) — flows within text; width/height and vertical margins are ignored, horizontal ones apply.
  3. inline-block — sits inline like inline, but with full sizing and spacing — the classic for "buttons" in a row.
  4. Normal flow — the default layout; position, float, flex/grid take elements out of it.
display Line break width/height Vert. margin
block yes yes yes
inline no no no
inline-block no yes yes
.tag { display: inline-block; width: 80px; padding: 4px 8px; }

⚠️ Common mistake: setting width/height on a pure inline element and expecting an effect — they don't exist there; use inline-block or block.

12

What causes layout thrashing / reflow across CSS and JS, and how do you avoid it?

Short answer: Layout thrashing is repeatedly interleaving geometry reads (offsetWidth, getBoundingClientRect) with DOM writes in the same frame. Each read after a write forces the browser to recompute layout synchronously (forced reflow). The fix is batching: all reads first, then all writes.

In depth:

  1. Reflow (layout) — recomputing geometry; triggered by changing sizes/positions and by reading layout properties.
  2. read→write→read in a loop — each read invalidates the cache and forces a synchronous reflow → jank.
  3. Batching — group all reads, then all writes; read outside loops.
  4. transform/opacity — animate these instead of top/left/width: they run on the compositor, skipping layout and paint.
  5. will-change / contain — hint a layer ahead of time and bound the recalc area (contain: layout).
// ❌ thrashing: reads and writes interleave
for (const el of items) {
  el.style.width = el.offsetWidth + 10 + 'px'; // read→write each iteration
}

// ✅ batched: read first, then write
const widths = items.map(el => el.offsetWidth);  // all reads
items.forEach((el, i) => { el.style.width = widths[i] + 10 + 'px'; }); // all writes

⚠️ Common mistake: animating width/top in a loop and reading getBoundingClientRect right after — the browser forces a reflow every frame; use transform and batching.

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS