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
01What is semantic HTML and why does it matter (accessibility, SEO, maintainability)?
junior
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:
- 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. - SEO — search engines treat
<article>,<h1>–<h6>,<time>as structure and content-importance signals. - Maintainability —
<header>/<footer>/<aside>self-document the markup; you don't read class names to learn a block's role. - 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.
02Explain the CSS box model and how `box-sizing: border-box` changes sizing.
junior
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:
- content — the content area, which
width/heightrefer to undercontent-box. - padding — inner spacing, painted with the element's background.
- border — the frame between padding and margin.
- 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.
03Flexbox vs CSS Grid: how do they differ and when do you choose each?
middle
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:
- Flexbox — lays content along a main axis; sizes often come from content (content-out). Ideal for toolbars, navbars, rows of buttons, centering.
- 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.
- 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.
04How is selector specificity calculated and how does the cascade resolve conflicts? Where do `!important` and `@layer` fit?
middle
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:
- Counting (a,b,c) — a = id selectors, b = classes/attributes/pseudo-classes, c = types/pseudo-elements. Compared left to right.
- Inline styles — outweigh any selector (effectively "1,0,0,0").
- Ties break on order — the last matching rule in source order wins.
!important— lifts a declaration above the normal cascade; only beaten by another!importantwith higher specificity.@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.
05How do `position` values differ: static, relative, absolute, fixed, sticky? Give a real use for sticky.
middle
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:
- static — the default;
top/left/z-indexare ignored. - relative — shifts by offsets but keeps its place in flow; establishes a containing block for
absolutedescendants. - absolute — removed from flow, measured from the nearest ancestor with
position != static. - fixed — removed from flow, pinned to the viewport (banners, modals).
- 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).
06What is a stacking context and why does a higher `z-index` sometimes do nothing?
senior
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:
- Root —
<html>creates the root stacking context. - What creates a new context —
position != static+z-index≠ auto; anyopacity < 1;transform,filter,perspective,will-change;isolation: isolate; a flex/grid child withz-index. - Compared locally — within a context, elements sort by
z-index, then DOM order. - Across contexts — the
z-indexof 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.
07How do you build responsive layouts: mobile-first, relative units, media queries, and modern container queries?
middle
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:
- Mobile-first — base styles with no media queries, then
@media (min-width: …)grows the layout; fewer overrides. - Relative units —
remoff the root (scales with the user's font size),emoff the parent,%/vwoff context; pixels for borders/shadows. - Media queries — respond to the viewport; choose breakpoints by content, not by device models.
- 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.
08How do you scale CSS in a large app: BEM, CSS Modules, CSS-in-JS, utility-first (Tailwind) — the tradeoffs?
senior
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:
- BEM —
block__element--modifier; works with any stack, zero tooling, but names are long and discipline is on humans. - CSS Modules —
styles.buttonis hashed to a unique class; isolation by default, plain CSS, but no runtime dynamics. - 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.
- 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.
09How do pseudo-classes differ from pseudo-elements? How do `:is()`, `:where()`, `:has()` work and what's their specificity?
middle
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:
- Pseudo-class — state/position:
:hover,:nth-child(2),:checked. Syntax:name. - Pseudo-element — a virtual part:
::before,::after,::placeholder,::marker. Syntax::name. :is(...)— shortens groups; takes the specificity of its heaviest argument.:where(...)— same, but specificity is always0,0,0— handy for base/resettable styles.: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.
10How do CSS custom properties work: cascade/scope, runtime theming, and how do they differ from Sass variables?
middle
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:
- Declare and read —
--gap: 16px;thengap: var(--gap, 8px);(the second arg is a fallback). - Cascade and scope — the value inherits down the tree; override it on any selector/
:root/inline. - Runtime — they react to
:hover, media queries, theme; read/written from JS viagetPropertyValue/setProperty. - 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.
11What's the difference between block, inline, and inline-block, and what is normal document flow?
junior
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:
- block (
<div>,<p>,<section>) — starts a new line, defaults to full width, honorswidth/heightand all margins/padding. - inline (
<span>,<a>,<strong>) — flows within text;width/heightand vertical margins are ignored, horizontal ones apply. - inline-block — sits inline like inline, but with full sizing and spacing — the classic for "buttons" in a row.
- 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.
12What causes layout thrashing / reflow across CSS and JS, and how do you avoid it?
senior
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:
- Reflow (layout) — recomputing geometry; triggered by changing sizes/positions and by reading layout properties.
- read→write→read in a loop — each read invalidates the cache and forces a synchronous reflow → jank.
- Batching — group all reads, then all writes; read outside loops.
transform/opacity— animate these instead oftop/left/width: they run on the compositor, skipping layout and paint.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.