CSS & browser fundamentals
They use Tailwind, but the underlying CSS knowledge still matters. Expect questions about specificity, layout, the rendering pipeline, and how the browser actually paints things.
01Selectors, cascade, specificity
How does CSS specificity work?
Each selector has a specificity score, written as four numbers: (inline, IDs, classes/attrs/pseudo-classes, elements/pseudo-elements). Higher wins. When equal, last declaration wins.
* { ... } /* 0,0,0,0 */
div { ... } /* 0,0,0,1 */
.btn { ... } /* 0,0,1,0 */
.btn:hover { ... } /* 0,0,2,0 */
#header .btn { ... } /* 0,1,1,0 */
<div style="..."> /* 1,0,0,0 */
!important /* trumps the calculation */
The practical rule: avoid IDs in styles, avoid !important, keep selectors flat. With Tailwind you mostly sidestep this — but you still need to know it for legacy code, third-party CSS, and edge cases.
What is the cascade?
The algorithm browsers use to decide which CSS rule wins for a given property on a given element. In order:
- Origin & importance — user agent, user, author stylesheets, with !important inverting some priorities
- Specificity — higher wins
- Source order — last one wins
Modern additions: @layer (cascade layers — explicit ordering of rule groups) and :where() / :is() for managing specificity intentionally.
02Box model & layout
Explain the box model. box-sizing: border-box?
Every element is a box: content, padding, border, margin (outside).
Default box-sizing: content-box means width applies to content only — padding and border add to the visible width. border-box makes width include padding and border, which is almost always what you want.
* { box-sizing: border-box; }
Universal border-box at the top of every CSS file is standard practice now.
Flexbox vs Grid — when each?
Flexbox: one dimension. Lay out a row or a column. Distribute space along a single axis. Use for navbars, button groups, simple lists.
Grid: two dimensions. Define rows and columns simultaneously. Use for page layouts, dashboards, anything with a real grid structure.
Grid is more powerful but more ceremony. Flex is simpler and often enough. They compose — a Grid cell can have a Flex child.
position values?
- static: default. In normal flow.
- relative: in flow, but you can offset with top/left/etc. Creates a positioning context for absolute children.
- absolute: out of flow. Positioned relative to the nearest positioned ancestor.
- fixed: out of flow. Positioned relative to the viewport.
- sticky: hybrid. Acts relative until it crosses a threshold, then acts fixed within its parent.
How does z-index actually work?
z-index only applies to positioned elements (anything except static), and it's relative to the element's stacking context, not the page.
Stacking contexts are created by: positioned + z-index, opacity < 1, transform, filter, isolation: isolate, and others. A child with z-index: 9999 still can't escape its parent's stacking context.
The classic bug: "I set z-index: 99999 and it still doesn't go on top." Almost always a stacking context problem with a parent.
03Tailwind specifics
What's your take on Tailwind?
I like it. The honest case for Tailwind:
- You stop naming things you don't need to name.
.user-card-header__title--largedies a peaceful death. - Constraints come from the design system tokens, not from individual stylesheets.
- Dead CSS can't accumulate — if a class isn't in the markup, it's not in the build.
- It plays well with components — the styles travel with the JSX.
The trade-offs:
- Markup gets verbose. Mitigation: extract repeated patterns into components, use
@applysparingly. - Designers and people new to the codebase have a learning curve.
- Customization for non-trivial design systems requires committing to the config — it's not a drop-in.
For an enterprise app at Omnesoft's scale, it's a defensible choice — but the design system needs to be a first-class citizen of the Tailwind config.
How do you handle component variants in Tailwind?
Three patterns I use:
- Conditional class strings with
clsxortailwind-merge - cva (class-variance-authority) for true variant systems — primary/secondary/destructive buttons, size/state matrices
- @apply for one-off utility groupings (sparingly — it works against Tailwind's grain)
const button = cva('rounded font-semibold', {
variants: {
intent: { primary: 'bg-blue-500 text-white', secondary: 'border' },
size: { sm: 'px-2 py-1', md: 'px-4 py-2' },
},
});
04The browser rendering pipeline
What happens between "user clicks a link" and "page is interactive"?
- Network: DNS, TCP, TLS, HTTP request
- HTML parsing: builds the DOM. Encounters CSS and JS, fetches them.
- CSS parsing: builds the CSSOM. Render-blocking until done.
- JS execution: blocks the parser unless
asyncordefer. - Render tree: DOM + CSSOM merged, only visible nodes.
- Layout ("reflow"): compute geometry of every node.
- Paint: fill in pixels.
- Composite: combine layers, send to GPU, display.
Then JS continues running, hydration happens (for SSR/SSG), event handlers attach, and the page becomes interactive (TTI / INP).
What's reflow vs repaint vs composite?
From most to least expensive:
- Reflow / layout: changing geometry — width, height, margin, font-size — forces re-computing layout for the affected subtree (sometimes the whole document). Expensive.
- Repaint: changing visual properties that don't affect layout — color, background. Cheaper.
- Composite-only:
transformandopacitycan be handled entirely on the GPU compositor without re-laying out or repainting. Cheapest. Use these for animations.
Practical takeaway: animate transform and opacity, not top/left/width. Add will-change: transform to hint the compositor when needed (sparingly — it has a memory cost).
What is the critical rendering path?
The minimum sequence the browser must complete to render the first pixel: HTML → DOM, CSS → CSSOM, JS that blocks parsing executes, render tree assembled, layout, paint.
Optimizing it means: minimize render-blocking resources, inline critical CSS for above-the-fold content, defer non-critical JS, use preload/preconnect hints, ship less.
05Modern CSS worth knowing
CSS variables — when to use?
--name custom properties cascade like normal CSS but can be read and updated at runtime, including from JS. Use for:
- Theming (light/dark, brand variants)
- Design tokens (spacing scale, color palette)
- Per-instance config without rewriting CSS
:root { --brand: #FF6B1A; }
.btn { background: var(--brand); }
[data-theme="alt"] { --brand: #C77DFF; }
What's :has()?
The "parent selector" we waited a decade for. Style a parent based on what's inside it.
/* Style a card that contains an image */
.card:has(img) { padding: 0; }
/* A form field with an invalid input */
.field:has(input:invalid) { border-color: red; }
Modern browsers support it. Lets you delete a lot of JavaScript that used to add a parent class based on child state.
What's container queries?
Media queries are based on the viewport. Container queries are based on the size of an ancestor container. This makes truly modular components possible — the same component can adapt to wherever it's placed.
.card-wrapper { container-type: inline-size; }
@container (min-width: 400px) {
.card { display: grid; grid-template-columns: 1fr 2fr; }
}
Fully supported now. Big deal for design systems and component libraries.
06Accessibility (CSS angle)
How do you handle focus styles?
Never strip them without replacement. Use :focus-visible to show focus rings only for keyboard users, not mouse clicks:
button:focus { outline: none; }
button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
This gives mouse users a clean look while preserving keyboard usability.
What about reduced motion?
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Respect users with vestibular disorders. Cheap to add, real impact for affected users.