HardArchitecture
Choose a styling approach for a new multi-team app and justify it against the alternatives
Judge by four criteria rather than preference: scoping (can a style leak?), runtime cost (is CSS generated in the browser?), ergonomics for the team you actually have, and how well it server-renders. CSS Modules give real scoping with zero runtime and trivial SSR, at the cost of a separate file per component. Utility CSS gives tiny output and no naming debate, at the cost of markup legibility and a learning curve. Runtime CSS-in-JS gives the best dynamic-styling ergonomics but adds runtime work and complicates streaming SSR. Zero-runtime CSS-in-JS splits the difference with a build-time cost. For a multi-team app I would weight scoping and SSR behaviour highest, which usually lands on CSS Modules or a zero-runtime solution, with design tokens as the shared layer either way — because the tokens matter more than the syntax.
The criteria are the content; naming a favourite tool without them is the answer to avoid.
MediumBest practice
A user zooms to 200% or has set a large default font size. What has to be true of your CSS?
Size text, spacing and container widths in `rem` so they scale with the user's root font size, and let containers grow with `max-width` rather than fixing a `height` on anything that holds text. Use media queries in `em`/`rem` so breakpoints move with the font too. Keep `px` for things that genuinely should not scale — hairline borders, and occasionally a fixed icon.
The failure mode is a fixed-height box with `rem` text inside it: the text grows, the box does not, and content is clipped. Setting text in `px` is worse — it overrides the user's stated preference outright, and WCAG 1.4.4 requires text to survive 200% zoom without loss of content or functionality. Testing this is cheap: set the browser's default font to 24px and walk the app.
MediumWrite code
A sidebar sits left on desktop but must appear ABOVE the content on mobile. Implement it and name the trap
.layout { display: grid; gap: 1rem; }
@media (min-width: 48rem) {
.layout { grid-template-columns: 16rem 1fr; }
.sidebar { grid-column: 1; grid-row: 1; }
.content { grid-column: 2; grid-row: 1; }
}
Put the main content first in the DOM and place the sidebar with Grid — `grid-template-areas` or an explicit `grid-column`/`grid-row` — so the visual order changes while the source order stays fixed. The trap is doing it with `order` or reversed flex direction: that changes only the painted order, so keyboard tab order and screen reader reading order still follow the DOM and no longer match what is on screen.
Whichever technique you pick, the rule is the same: decide the correct reading order first and make the DOM match it, then move things visually. A mismatch is invisible to the person building it and immediately disorienting for anyone tabbing through — focus jumps from the top of the screen to the bottom and back.
MediumCode review
Review this responsive CSS
.card { display: flex; flex-direction: row; padding: 24px; font-size: 16px; }
@media (max-width: 768px) {
.card { display: block; padding: 12px; font-size: 14px; }
}
@media (max-width: 480px) {
.card { padding: 8px; }
}
It works, but it is written desktop-first with max-width overrides, so every small screen pays to download and then undo the desktop rules, and each new breakpoint adds another layer of override to reason about. Prefer mobile-first `min-width` queries so styles accumulate rather than cancel. Beyond structure: the sizes are magic numbers that should be tokens; `clamp()` would remove the font-size breakpoint entirely; and if the card is reused in a sidebar, viewport queries are the wrong signal and a container query is what you actually want.
The container-query point is the one that matters most for a component library — a viewport breakpoint is simply the wrong input for a reusable card.
HardConcept
What is a block formatting context, how does it differ from a stacking context, and what does each one actually fix?
They are unrelated mechanisms that people conflate because both are "contexts" created by similar-looking properties. A BLOCK FORMATTING CONTEXT is about LAYOUT: an independent region where floats are contained and margins do not collapse through the boundary. It is created by `overflow` other than `visible`, `display: flow-root`, floats, absolute positioning, and flex/grid items. It fixes a parent collapsing to zero height around floated children, and unwanted margin collapse. A STACKING CONTEXT is about PAINT ORDER: an atomic layer whose children are painted together, so a child’s `z-index` can never escape it. It is created by a positioned element with a `z-index`, `opacity` below 1, `transform`, `filter`, `will-change` and `isolation: isolate`. It explains why `z-index: 9999` can still render behind something.
The clean test of understanding is that `overflow: hidden` creates a BFC but NOT a stacking context, while `opacity: 0.99` creates a stacking context but NOT a BFC. `display: flow-root` exists precisely to create a BFC without the side effects of the old `overflow: hidden` hack.
MediumDebugging
A transition does not animate when the element first appears. Why?
el.style.display = 'block';
el.style.opacity = '1'; // expected to fade in from 0
There is no starting value to transition FROM in the browser’s eyes. Both changes happen in the same frame, so the element goes from `display: none` — where it has no rendered style at all — straight to opacity 1, and the engine has nothing to interpolate. Fix by making the element rendered with its starting value first, forcing a style recalculation (or waiting a frame with `requestAnimationFrame`), and only then setting the end value. The modern alternatives avoid the dance entirely: `@starting-style` declares the initial value for an entering element, and `transition-behavior: allow-discrete` lets `display` itself participate.
The underlying rule — you cannot transition from a state that was never rendered — explains a whole family of "why does my animation not run" bugs.
HardFollow-up
You replaced media queries with container queries. What would you measure before rolling that out widely?
Containers require size containment on the query container, so first check that nothing depends on that element being sized by its content — that is the most likely visual regression. Then measure: container queries introduce a layout dependency the engine must evaluate per container, so on a page with hundreds of containers watch style and layout time in a performance profile rather than assuming it is free. Also verify the fallback story for any browser you still support, and check that the containers you declared are the ones actually constraining the component, since a misplaced `container-type` silently makes the query match the wrong box.
Probes whether an architectural improvement was adopted with any measurement, and whether the candidate knows containment is a prerequisite rather than an implementation detail.
MediumMultiple choice
What beats `!important` in an author stylesheet?
`!important` does not change specificity — it moves the declaration into a different CASCADE ORIGIN band. The cascade compares origin-and-importance first: author-normal (including plain inline styles) loses to author-important, which in turn loses to user-important and then user-agent-important. Only once two declarations sit in the SAME band do specificity and then source order decide, which is why one `!important` is beaten by another with higher specificity. Note the interaction with `@layer`: for normal declarations a later layer wins, but for important declarations layer precedence is REVERSED, so an important rule in an earlier layer beats an important rule in a later one.
The remaining 42 HTML & CSS questions — plus mock interviews, spaced revision and progress tracking — are in the free interview prep workspace.