Frontend · most asked
CSS interview questions
25 questions · 9 easy · 11 medium · 5 hard. Open one to see the answer, a code example and the likely follow-up, then mark it as known or to review.
Showing 25 of 25 · 0 known · 0 to review
Every element is a rectangular box made of four layers, from the inside out:
- content: the text or image itself
- padding: space inside the border, takes the background colour
- border
- margin: transparent space outside the border, separating it from other boxes
By default (
box-sizing: content-box),widthandheightapply to the content only, so padding and border are added on top. Awidth: 200pxbox with20pxpadding and a1pxborder is actually 242px wide.DevTools shows this as the coloured box diagram in the Computed panel.
Likely follow-up: What is margin collapsing?
With
border-box, thewidthandheightyou set include padding and border. Adding padding shrinks the content area instead of making the box bigger.That makes layouts predictable: two
width: 50%columns with padding still fit side by side, which they would not with the defaultcontent-box.That is why almost every reset (and Tailwind's preflight) applies it to everything, including pseudo-elements. Margin is never included in either model.
Example *, *::before, *::after { box-sizing: border-box; } .card { width: 300px; padding: 20px; border: 1px solid #ddd; /* still exactly 300px wide */ }When several rules target the same element, the one with the highest specificity wins. It is compared as three numbers (IDs, classes, elements):
- IDs:
#navis (1,0,0) - classes, attributes, pseudo-classes:
.btn,[type="text"],:hoverare (0,1,0) - elements and pseudo-elements:
p,::beforeare (0,0,1)
The columns are compared left to right, so one ID beats any number of classes. Inline
stylebeats all of them, and!importantbeats inline. With equal specificity, the rule that comes last wins.*and combinators add nothing;:where()counts as zero, and:is()/:not()take their most specific argument.Example p { color: black; } /* 0,0,1 */ .intro { color: blue; } /* 0,1,0 */ article p.intro { color: green; } /* 0,1,2 wins over .intro */ #hero p { color: red; } /* 1,0,1 wins over all above */ :where(#hero) p { color: gray; } /* 0,0,1 :where adds zero */Likely follow-up: Why is overusing
!importanta problem, and what are cascade layers (@layer)?- IDs:
The cascade is the algorithm that decides which declaration wins when several apply. In order of priority:
- origin and importance (
!important, author vs browser styles) - cascade layers (
@layer) - specificity
- source order: later wins
Inheritance is separate: some properties pass from parent to child when the child has no value of its own. Mostly text-related properties inherit (
color,font-*,line-height,visibility), while box properties (margin,padding,border,background,width) do not.You can force it with
inherit, reset withinitial, or useunset(inherit if inheritable, else initial).- origin and importance (
static(default): normal flow;top/leftandz-indexhave no effectrelative: stays in the flow, but can be nudged with offsets. Its main job is being the containing block for absolute childrenabsolute: removed from the flow, positioned relative to the nearest positioned ancestor (any non-static position), or the page if there is nonefixed: removed from the flow, positioned relative to the viewport, so it stays put on scrollsticky: behaves like relative until a scroll threshold (top: 0), then sticks within its parent
A common gotcha: a parent with
transformorfilterbecomes the containing block even forfixedchildren.Example .card { position: relative; } .card .badge { position: absolute; top: 8px; right: 8px; /* corner of .card, not the page */ } .table-header { position: sticky; top: 0; }Likely follow-up: Why might
position: stickynot work? (overflow on an ancestor, notopset)block: new line, full available width, respects width/heightinline: flows in text, ignores width/height and vertical marginsinline-block: in the text flow but respects width, height and marginsflex/inline-flex: one-dimensional layout of its childrengrid/inline-grid: two-dimensional layout of its childrennone: removed completely, takes no space and is hidden from screen readerscontents: the box disappears and its children act as children of the parent
Note that
displayhas an outer part (how the box sits among siblings) and an inner part (how its children are laid out):inline-flexis inline outside, flex inside.display: none: removed from layout, takes no space, not clickable, hidden from screen readers. Cannot be transitioned (until recenttransition-behaviorsupport).visibility: hidden: invisible but keeps its space, not clickable, hidden from screen readers. Children can override it withvisibility: visible.opacity: 0: invisible but keeps its space, still clickable and focusable, still read by screen readers. Can be animated smoothly.
To hide something visually but keep it for screen readers, use a
.sr-only(visually-hidden) class instead.Flexbox is one-dimensional: it lays items out in a row or a column, and sizes them based on their content. Great for nav bars, toolbars, button groups, centering, and lists that wrap.
Grid is two-dimensional: you define rows and columns, and place items into that structure. Great for page layouts, card galleries, dashboards and anything that must line up in both directions.
A rule of thumb: flexbox is content-out (items decide their size), grid is layout-in (the container decides). They combine well: a grid page layout with flex inside each card.
Example /* flex: a row of items, spacing between them */ .toolbar { display: flex; align-items: center; gap: 8px; } .toolbar .spacer { flex: 1; } /* grid: responsive cards, no media query */ .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }Likely follow-up: What is the difference between
auto-fillandauto-fit?flexis shorthand for three properties:flex-grow: how much of the leftover space the item takes, relative to siblingsflex-shrink: how much it shrinks when there is not enough spaceflex-basis: its starting size before growing or shrinking (automeans use its width or content)
flex: 1means1 1 0%: start from zero and share the space equally, so items get equal widths regardless of content.flex: auto(1 1 auto) starts from content size, so bigger content gets a bigger box.flex: nonemakes an item rigid.Gotcha: flex items have
min-width: auto, so long text can overflow. Fix withmin-width: 0.The modern answers:
- Flexbox on the parent:
display: flex; justify-content: center; align-items: center - Grid on the parent:
display: grid; place-items: center(the shortest option) - Absolute positioning:
position: absolute; inset: 0; margin: autowith a set size, ortop: 50%; left: 50%; transform: translate(-50%, -50%)for unknown sizes
For horizontal-only centering of a block with a width,
margin: 0 autostill works. For inline content and text,text-align: center.Remember the parent needs a height for vertical centering to be visible.
Example .parent { display: grid; place-items: center; min-height: 100vh; } /* or */ .parent { display: flex; justify-content: center; /* main axis */ align-items: center; /* cross axis */ }- Flexbox on the parent:
px: fixed (a CSS pixel, not a device pixel)em: relative to the current element's font size (forfont-sizeitself, the parent's), so it compounds when nestedrem: relative to the root font size (onhtml), usually 16px, so it is predictable and respects the user's browser font setting%: relative to the parent (width for widths and padding, font size for font size)vh/vw: 1% of the viewport height / width
Common practice:
remfor font sizes and spacing,emfor things that should scale with their own text (button padding),pxfor borders. On mobile,100vhcan be taller than the visible area; use100dvh.Likely follow-up: Why is
rembetter for accessibility thanpxfor font sizes?Media queries apply styles only when a condition is true, usually the viewport width:
@media (min-width: 768px) { … }.Mobile-first means writing the base styles for small screens and adding
min-widthqueries to enhance for larger ones. Benefits:- the simplest layout is the default, so phones load the least CSS overrides
- it forces you to prioritise content
- styles build up instead of being undone
Desktop-first uses
max-widthqueries and tends to pile up overrides. Beyond width, you can queryprefers-color-scheme,prefers-reduced-motion,hoverandorientation. Pick breakpoints where your content breaks, not for specific devices.Example .grid { display: grid; gap: 16px; } /* phone: 1 column */ @media (min-width: 640px) { .grid { grid-template-columns: 1fr 1fr; } /* tablet */ } @media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); } } @media (prefers-reduced-motion: reduce) { * { animation: none !important; } }z-indexonly compares elements within the same stacking context. A stacking context is a group that is layered as a single unit: children can never escape their parent's context, however high theirz-index.New stacking contexts are created by, among others:
positionrelative/absolute with az-indexother thanauto, and allfixed/stickyelementsopacitybelow 1,transform,filter- flex or grid children with a
z-index isolation: isolate
So a modal with
z-index: 9999inside a parent withtransformandz-index: 1stays below a sibling withz-index: 2. Fixes: move the modal to the end of<body>(a React portal) or use<dialog>, which renders in the top layer.A pseudo-class (single colon) selects an element in a certain state or position:
:hover,:focus-visible,:checked,:disabled,:first-child,:nth-child(2n),:not(.active).A pseudo-element (double colon) targets a part of an element, or creates a virtual one:
::before,::after,::first-line,::placeholder,::selection,::marker.::beforeand::afterneed acontentproperty (evencontent: '') to render. They are great for decorations and icons, but their text is not reliably selectable or accessible, so do not put real content there.Example a:hover { text-decoration: underline; } /* state */ li:nth-child(odd) { background: #f6f6f6; } /* position */ .required::after { /* generated part */ content: ' *'; color: crimson; }A transition animates a property between two states when it changes, for example on
:hoveror when a class is added. It needs a trigger and only has a start and an end.An animation uses
@keyframeswith as many steps as you want. It can run on its own, loop (infinite), alternate direction, pause and be delayed.Use transitions for simple interactive feedback, animations for loaders, attention effects and multi-step sequences.
For smooth 60fps, animate only
transformandopacity, which the compositor can handle without layout or paint. Respectprefers-reduced-motion.Example .btn { transition: transform 150ms ease-out; } .btn:hover { transform: translateY(-2px); } @keyframes spin { to { transform: rotate(360deg); } } .loader { animation: spin 1s linear infinite; }- Reflow / layout: the browser recalculates the size and position of elements. Triggered by changing geometry (
width,margin,top, font size, adding DOM nodes, resizing the window). It is the most expensive step and can cascade to many elements. - Repaint: redrawing pixels without changing layout, for example a new
colororbackground. - Composite only:
transformandopacitycan often skip both.
Layout thrashing happens when JavaScript alternates writes and reads (
el.style.width = …thenel.offsetHeight), forcing a synchronous layout each time. Batch reads, then writes, or userequestAnimationFrame.Likely follow-up: What does
will-changedo, and when should you avoid it?- Reflow / layout: the browser recalculates the size and position of elements. Triggered by changing geometry (
Custom properties are declared with
--nameand read withvar(--name, fallback).Unlike Sass variables, which are replaced at build time, CSS variables live in the browser at runtime:
- they cascade and inherit, so you can override them in a subtree (
.dark { --bg: #111 }) - they can change in media queries and be updated from JavaScript with
style.setProperty - they are visible and editable in DevTools
This makes them the standard way to do theming (dark mode, brand colours) and design tokens. Sass variables are still handy for build-time things like loops and breakpoints in media queries, where
var()cannot be used.Example :root { --bg: #fff; --text: #111; --space: 1rem; } @media (prefers-color-scheme: dark) { :root { --bg: #111; --text: #eee; } } body { background: var(--bg); color: var(--text); padding: var(--space); }- they cascade and inherit, so you can override them in a subtree (
When vertical margins of block elements touch, they combine into one margin equal to the larger of the two, instead of adding up. A
margin-bottom: 20pxfollowed bymargin-top: 30pxgives a 30px gap, not 50px.It happens between adjacent siblings, between a parent and its first or last child (the child's margin "leaks" out of the parent), and in empty blocks.
It does not happen with horizontal margins, in flex or grid containers, or when the parent has padding, a border or creates a new block formatting context (for example
display: flow-root).Many teams avoid the issue by using margin in one direction only, or
gapin flex and grid layouts.BEM stands for Block, Element, Modifier:
- block: a standalone component,
.card - element: a part of it, joined with two underscores,
.card__title - modifier: a variant, joined with two dashes,
.card--featured
The goal is flat, low-specificity selectors (one class each) that say exactly where a style belongs. That avoids specificity wars and makes CSS safe to change in a big codebase.
Today, scoping often comes from tooling instead: CSS Modules generate unique class names, Tailwind uses utility classes, and CSS-in-JS ties styles to components. BEM is still common in plain CSS and Sass projects.
Example <article class="card card--featured"> <h2 class="card__title">Title</h2> <button class="card__button card__button--primary">Read</button> </article>Likely follow-up: How do CSS Modules or Tailwind solve the same problem?
- block: a standalone component,
- CSS Modules: plain CSS files whose class names are scoped automatically at build time. Zero runtime, familiar syntax; styles live in a separate file.
- Tailwind: utility classes in the markup (
flex gap-4 p-2). Very fast to build with, consistent design tokens, tiny final CSS. Markup gets verbose, and there is a learning curve. - CSS-in-JS (styled-components, Emotion): styles written in JavaScript, easy dynamic styling from props. Runtime libraries add bundle size and cost, and they do not fit well with React Server Components. Zero-runtime options (vanilla-extract, Panda) avoid that.
There is no single right answer: interviewers want to hear that you know the trade-offs and what your team chose and why.
The baseline is
img { max-width: 100%; height: auto; }, so images never overflow their container and keep their ratio.When an image must fill a box of a fixed shape (a card thumbnail, an avatar), use:
object-fit: cover: fills the box and crops the overflow, keeping the ratioobject-fit: contain: fits entirely inside, leaving empty spaceobject-positionto choose which part stays visible
aspect-ratio: 16 / 9reserves the right height before the image loads, preventing layout shift. For background images,background-size: coverdoes the same asobject-fit: cover.Serving the right file size is HTML's job (
srcset), not CSS's.Example img { max-width: 100%; height: auto; } .thumb { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; object-position: center top; }A block formatting context (BFC) is an independent layout region: floats inside it are contained, and margins do not collapse across its edge.
Floats were the old way to build columns. A floated child is taken out of normal flow, so its parent collapses to zero height. Fixes, oldest to newest:
- the clearfix hack: a
::afterwithcontent: ''; display: table; clear: both overflow: hiddenon the parent (creates a BFC, but clips content)display: flow-root, which creates a BFC with no side effects
Today, use flex or grid for layout. Floats are only for wrapping text around an image.
- the clearfix hack: a
:has()is the long-awaited parent selector: it matches an element based on what it contains, or what comes after it..card:has(img): cards that contain an imageform:has(:invalid): a form with any invalid fieldlabel:has(+ input:checked): a label followed by a checked inputbody:has(dialog[open]): lock scrolling while a modal is open
It removes a lot of JavaScript that only existed to toggle a class on a parent. It is supported in all modern browsers. Its specificity is that of its most specific argument, and very broad uses (like on
body) can cost performance.Example .field:has(input:invalid) label { color: crimson; } .card:has(img) { grid-template-rows: 200px auto; } body:has(dialog[open]) { overflow: hidden; }Media queries respond to the viewport. Container queries respond to the size of a parent container.
That matters for reusable components: the same card might sit in a wide main column or a narrow sidebar on the same screen. With media queries it cannot know; with container queries it adapts to the space it actually has.
How it works:
- mark the parent with
container-type: inline-size(optionallycontainer-name) - write
@container (min-width: 400px) { … }for the children - units like
cqiare relative to the container size
Use media queries for page-level layout, container queries for component-level layout.
Example .card-wrapper { container-type: inline-size; } .card { display: grid; gap: 8px; } @container (min-width: 400px) { .card { grid-template-columns: 120px 1fr; } /* side-by-side when roomy */ }- mark the parent with
Performance:
- ship less CSS: remove unused styles (Tailwind and CSS Modules help), split per route
- inline the critical CSS for above-the-fold content, load the rest without blocking
- animate
transformandopacity, avoid layout thrashing content-visibility: autoto skip rendering off-screen sections- reserve space for images and fonts to avoid layout shift (CLS)
Maintainability:
- low, flat specificity and a naming convention or scoped styles
- design tokens as CSS variables for colours, spacing and typography
@layerto control the order of resets, libraries and components- a linter such as Stylelint
Selector matching speed rarely matters in practice; the size of the CSS and layout work do.