The 62 HTML and CSS questions interviewers actually ask, with direct answers, working markup, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
62 questions with answersKey Takeaways
HTML (HyperText Markup Language) is the language that structures every web page: it marks up content as elements like headings, paragraphs, lists, links, images, and forms, giving each piece a meaning that browsers, assistive technology, and search engines can read. CSS (Cascading Style Sheets) is the companion language that styles that structure: it decides color, spacing, fonts, and, above all, layout, how elements sit next to and on top of each other across every screen size. Together they're the foundation of the front end: HTML for what content is, CSS for how it looks, with JavaScript layered on for behavior. In interviews, HTML and CSS questions probe the models beneath the syntax: the box model, the cascade, specificity, stacking contexts, and how flexbox and grid position elements, not trivia about property names. This page collects the 62 questions that come up most, each with a direct answer and working markup. If you're still building fundamentals, the Learn web development track on MDN Web Docs from Mozilla is the canonical path; increasingly the first front-end round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Learn CSS in 20 Minutes
Video: Learn CSS in 20 Minutes (Web Dev Simplified, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your HTML & CSS certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Semantic HTML means choosing elements for their meaning, not their appearance: nav for navigation, article for self-contained content, button for actions. A div styled as a button can look identical on screen, but the semantic version tells browsers, screen readers, and search engines what the content actually is.
It matters for three audiences. Assistive technology announces landmarks and roles so users can jump straight to the content they want. Search engines read structure when parsing pages. And other developers see intent in the markup itself, which keeps large codebases readable.
<!-- says nothing about its content -->
<div class="nav"><div onclick="go()">Home</div></div>
<!-- says everything -->
<nav><a href="/">Home</a></nav>Key point: Reaching for button instead of a clickable div is the fastest way to sound like you've shipped accessible front-ends.
Block elements start on a new line and stretch the full width of their container: div, p, section. Inline elements flow within text and take only the space their content needs: span, a, strong. Inline-block flows like inline but respects width, height, and vertical spacing like block.
The practical trap: width and height do nothing on inline elements, and vertical margins are ignored. That's why nav links get display: inline-block the moment they need real padding and dimensions.
| Block | Inline | |
|---|---|---|
| New line | Starts on its own line | Flows within the text |
| Width / height | Respected | Ignored |
| Vertical margin | Respected | Ignored |
| Examples | div, p, section | span, a, strong |
Key point: The follow-up is usually 'why isn't width working on my span?'. The answer: it's inline; change its display.
Use article for content that stands alone if you lifted it off the page: a blog post, a product card, a comment. Use section for a thematic grouping that belongs to the page, ideally with its own heading. Use div when you need a hook for styling and no semantic element fits.
The self-containment test settles most debates: would this block make sense on its own in an RSS feed? Article if yes, section if it's a chapter of something larger, div if it's pure presentation.
Key point: 'Would it make sense syndicated on its own?' is the one-line rule interviewers accept for article versus section.
The doctype tells the browser to render the page in standards mode. Without it, browsers drop into quirks mode, an emulation of 1990s behavior where the box model, percentage heights, and inline image gaps all work differently for the sake of ancient pages.
It isn't an HTML element and isn't case-sensitive; it's an instruction to the parser. The HTML5 form is deliberately short because the older doctypes with long DTD URLs were copied wrong so often.
Key point: If asked what actually breaks in quirks mode, The box model: widths include padding and border, like the old IE model.
Data attributes (data-*) store custom values on any element in valid HTML: data-product-id="42", data-state="open". JavaScript reads them through element.dataset, and CSS can select on them with attribute selectors, which makes them the standard bridge for passing small pieces of state between markup, style, and behavior.
Use them for element-scoped state and configuration. Don't use them as a database: anything sensitive or large belongs in JavaScript state, and anything with a real attribute (like disabled) should use the real attribute.
<button data-product-id="42" data-state="idle">Add to cart</button>
<!-- CSS can target it: [data-state="loading"] { opacity: 0.5; } -->
<!-- JS reads it: button.dataset.productId is "42" -->alt provides a text alternative for an image: screen readers announce it, browsers show it when the image fails to load, and search engines index it. Good alt text describes the image's function in context, briefly: 'Bar chart showing signups doubling in Q3', not 'chart' or 'image123'.
The senior detail: decorative images should carry an empty alt (alt=""), which tells screen readers to skip them entirely. Omitting the attribute is worse, because many readers then announce the file name.
Key point: Knowing that alt="" and a missing alt behave differently is the detail that separates practice from checkbox knowledge.
Two ways: give the input an id and point the label's for attribute at it, or wrap the input inside the label element. Either way, screen readers announce the label when the input receives focus, and clicking the label focuses the field or toggles the control.
The click behavior matters more than people expect: it turns a 16-pixel checkbox into a full-sentence tap target on mobile. Placeholder text is not a label; it disappears the moment the user types and usually fails contrast requirements.
<label for="email">Work email</label>
<input id="email" type="email" name="email" />
<label>
<input type="checkbox" name="terms" /> I agree to the terms
</label>Key point: 'Is a placeholder enough?' is the standard follow-up. the technical answer is no, and saying why (it disappears on input) closes the question.
required blocks empty submission, type (email, url, number) validates format, min, max, and step bound numeric input, minlength and maxlength bound text length, and pattern matches a regex. When any of them fail, the browser blocks submission and shows a native message, with zero JavaScript written.
The judgment half of the answer: built-in validation is a UX layer, not a security layer. Anyone can bypass it in dev tools, so the server must validate everything again.
<form>
<input type="email" required />
<input type="number" min="1" max="10" step="1" />
<input type="text" pattern="[A-Za-z]{3}" title="Three letters" />
<button>Submit</button>
</form>Key point: Always end this answer with 'and the server validates again'. Client-side-only validation is a red flag in any review.
GET appends form data to the URL as a query string; POST sends it in the request body. GET requests are bookmarkable, shareable, and cacheable, which suits reads like search. POST suits writes: creating, updating, anything with side effects or data that shouldn't appear in the URL.
URLs leak: they land in browser history, server logs, and analytics tools. That's why passwords and personal data must never travel by GET, and why the method choice is a security decision, not a style preference.
Key point: Anchor the answer in 'search form GET, login form POST' and the reasoning explains itself.
An id identifies exactly one element per page, and each element carries at most one; classes group any number of elements, and each element can carry several. In CSS, #id outranks .class on specificity; in HTML, ids also serve anchor targets, label wiring, and ARIA relationships.
Most teams style with classes only. High id specificity makes overrides painful later, so ids get reserved for the jobs that genuinely need a unique reference: fragment links, for attributes, aria-labelledby.
Key point: Saying 'I style with classes and keep ids for anchors and form wiring' answers the question and its follow-up together.
One h1 describing the page, then h2 for major sections, h3 nested inside those, without skipping levels on the way down. Headings build the document outline that screen reader users jump between to scan a page, the same way sighted users skim the visual hierarchy.
Never pick a heading level for its font size; that's what CSS is for. A jump from h2 to h4 reads like missing content to assistive technology and to anyone auditing the page.
Key point: The rule that sticks: heading levels are structure, font sizes are style, and the two are set independently.
Every element renders as a rectangle built from four layers: the content area, padding inside the border, the border itself, and margin outside pushing neighbors away. Widths, backgrounds, and click targets all depend on which layer you're measuring, which is why this model opens most CSS interviews.
By default, width sets only the content area, so padding and border grow the visible box beyond the number you declared. That surprise is the setup for the box-sizing question that almost always follows.
.card {
width: 300px; /* content area */
padding: 16px; /* inside the border */
border: 2px solid #333;
margin: 24px; /* outside, pushes neighbors */
}
/* rendered width: 300 + 32 + 4 = 336px (content-box) */Key point: Walk the layers from the inside out, then volunteer the rendered-width math. It leads the interviewer to their own next question.
Watch a deeper explanation
Video: Learn CSS Box Model In 8 Minutes (Web Dev Simplified, YouTube)
border-box makes width and height include padding and border, so a 300px card measures 300px on screen no matter how much internal padding it carries. The default, content-box, adds padding and border on top of the declared width, which makes every layout calculation harder than it should be.
Nearly every codebase applies it globally in a reset. Percentage grids are the clearest win: three 33.33% columns with padding overflow their row under content-box and fit exactly under border-box.
*, *::before, *::after {
box-sizing: border-box;
}Key point: Mention applying it to ::before and ::after too; the pseudo-elements are the part most candidates forget.
Padding creates space inside the element, between its content and its border: it extends the clickable area and shows the element's own background. Margin creates space outside, between the element and its neighbors: it's always transparent and it can be negative. The choice is about who owns the space.
Two behavioral differences decide the edge cases: padding grows the hit area of buttons and links, while vertical margins between blocks can collapse into each other. If a click should land there, pad it; if siblings need breathing room, margin it.
Key point: 'Padding is part of the element, margin is between elements' is the framing that keeps this answer short and right.
When multiple rules target the same element, specificity picks the winner. Selectors are compared in tiers: inline styles beat ids, ids beat any number of classes, and classes beat any number of element selectors. When two selectors tie exactly, source order decides and the later rule wins.
The comparison is positional, not additive: eleven classes never outrank one id. That's why a single #sidebar selector can haunt every override attempt in a stylesheet, and why teams keep selectors flat and class-based.
CSS specificity weights, by selector type
Each selector type carries a weight; a higher tier beats any number of lower ones (log scale). This is why one id outranks ten classes.
Scale: Hyring editorial score for interview preparation, not an external benchmark.
| Selector type | Specificity | Example |
|---|---|---|
| Inline style | Highest (beats all selectors) | style="color: red" |
| ID | 1-0-0 | #header |
| Class, attribute, pseudo-class | 0-1-0 | .card, [type="text"], :hover |
| Element, pseudo-element | 0-0-1 | p, ::before |
| Universal | 0-0-0 | * |
Key point: 'ten classes still lose to one id' matters.
The cascade is the algorithm that picks one winning declaration when several set the same property on the same element. It weighs, in order: origin and importance (user agent, user, author, !important), then specificity, then source order, with later rules beating earlier ones when everything else ties.
That ordering explains everyday behavior: why your rule loses to a more specific one no matter where you place it, and why swapping two equally specific rules changes the page. Modern @layer inserts an explicit layering step before specificity is even consulted.
How the cascade picks a winning declaration
The engine walks these steps in order and stops at the first one that breaks the tie.
Key point: Most candidates say 'specificity' and stop. Naming origin and source order as the steps around it puts you ahead immediately.
px is a fixed length that ignores context. em scales with the current element's font size, so it compounds through nesting. rem scales with the root font size, one predictable reference for the whole page. vh and vw are percentages of the viewport's height and width respectively.
The convention most teams land on: rem for font sizes and spacing so user font settings scale the whole design, em for things tied to local text like icon sizing, px for hairline borders, and viewport units for full-screen sections.
| Unit | Relative to | Typical use |
|---|---|---|
| px | Nothing (fixed) | Borders, shadows, fine detail |
| em | Current font size | Icons, padding tied to text |
| rem | Root font size | Type scale, spacing system |
| vh / vw | Viewport height / width | Hero sections, overlays |
| % | Parent's corresponding size | Fluid widths |
Key point: Explain why rem respects user font-size settings while px overrides them; that accessibility angle is what's being screened.
The working set: block and inline for normal document flow, inline-block for inline placement with box control, none to remove an element from layout entirely, and flex and grid to turn a container into a flexbox or grid formatting context for its children.
Two details worth volunteering: display: none also removes the element from the accessibility tree, and flow-root creates a new block formatting context, the modern fix for containing floats and stopping margin collapse.
static is the default flow. relative nudges an element from its normal spot and, more usefully, anchors absolutely positioned children. absolute removes the element from flow and positions it against the nearest positioned ancestor. fixed pins to the viewport. sticky scrolls normally until it hits an offset, then pins.
Use cases map cleanly: relative for anchoring, absolute for badges and dropdowns, fixed for the persistent chat bubble, sticky for table headers. The perennial bug is an absolute element positioning against the page because nobody set position: relative on the intended parent.
.card { position: relative; } /* anchor for children */
.card .badge {
position: absolute;
top: 8px;
right: 8px; /* against .card, not the page */
}
.table-header { position: sticky; top: 0; }Key point: Explaining that absolute hunts for the nearest positioned ancestor, and that relative is how you provide one, answers the favorite follow-up in advance.
A pseudo-class selects an element in a particular state or position: :hover, :focus, :nth-child(2n), :not(.active). A pseudo-element styles a part of an element or generates content that doesn't exist in the markup: ::before, ::after, ::first-line, ::placeholder. State versus part, in four words.
The syntax rule: single colon for pseudo-classes, double colon for pseudo-elements, though browsers still accept :before for legacy reasons. ::before and ::after render nothing without a content property, the first thing to check when one refuses to appear.
.link:hover { text-decoration: underline; } /* state */
.item:nth-child(odd) { background: #f5f5f5; } /* position */
.badge::after {
content: ""; /* required to render at all */
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}Key point: Give the 'state versus part' framing first, then mention content being required on ::before and ::after.
Combinators describe the relationship between two selectors: a space matches any descendant, > matches direct children only, + matches the immediately following sibling, and ~ matches all later siblings at the same level. They let you target structure, like the first paragraph after a heading, without adding classes everywhere.
The descendant space deserves caution in large codebases: .sidebar a reaches every link at any depth, including ones inside components you didn't mean to touch. Child and sibling combinators keep the blast radius small.
article p { } /* any descendant */
ul > li { } /* direct children only */
h2 + p { } /* the paragraph right after an h2 */
h2 ~ p { } /* every later sibling paragraph */Three ways: inline styles in a style attribute, internal CSS in a style block in the head, and external stylesheets linked with a link tag. External wins for real projects: one cached file styles every page, keeps markup clean, and gives the team a single place to work.
Inline styles carry the highest specificity, so they're painful to override and usually a code smell, except in HTML emails (where clients strip stylesheets) and for values computed by JavaScript. Internal blocks earn their place as inlined critical CSS for performance.
Key point: The email exception makes this stock question interesting: inline styles are wrong everywhere except the one place they're required.
The modern answer is flexbox on the parent: display: flex with justify-content: center and align-items: center. Grid is even shorter with place-items: center. For horizontal centering alone, a block element with a set width takes margin-inline: auto. Absolute positioning plus a transform handles out-of-flow overlays.
Interviewers ask for multiple methods to see whether you know why each exists: flex and grid for general layout, auto margins when there's no flex parent to touch, and the transform trick when the element is out of flow, like a modal.
/* 1. flexbox */
.parent { display: flex; justify-content: center; align-items: center; }
/* 2. grid, the shortest */
.parent { display: grid; place-items: center; }
/* 3. horizontal only, block element with a width */
.child { width: 400px; margin-inline: auto; }
/* 4. out-of-flow overlay */
.modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}Key point: Lead with grid's place-items: center; the one-liner buys you time, and the follow-ups let you show the rest.
For candidates with working experience: layout systems, responsive strategy, and the mechanics that separate users of CSS from people who understand it.
When two vertical margins meet, they merge into a single margin equal to the larger of the two rather than adding up. It happens between adjacent siblings, between a parent and its first or last child, and on empty blocks. Horizontal margins never collapse, and flex and grid items are immune.
The surprising case is parent-child: a child's margin-top can escape through the parent and move the parent itself down. Padding, a border, or a new formatting context on the parent (display: flow-root) stops the escape.
/* gap below is 24px, not 40px: the margins collapse */
h2 { margin-bottom: 24px; }
p { margin-top: 16px; }
/* stop a child's margin escaping its parent */
.parent { display: flow-root; }Key point: The three cases (siblings, parent-child, empty blocks) and that flex and grid items are immune; that is the complete technical set.
display: flex lays a container's children out along a main axis, set by flex-direction (row by default), with the cross axis running perpendicular to it. justify-content distributes items along the main axis, align-items aligns them along the cross axis, and gap spaces them without margin hacks.
Everything in flexbox is defined against those two axes, never against left, right, top, or bottom. That's the concept this question checks: switch flex-direction to column, and justify-content suddenly controls vertical distribution.
.toolbar {
display: flex;
flex-direction: row; /* main axis: horizontal */
justify-content: space-between; /* along the main axis */
align-items: center; /* along the cross axis */
gap: 12px;
}Key point: Say 'the axes flip with flex-direction' explicitly. Interviewers use column layouts to catch candidates who memorized row behavior.
Watch a deeper explanation
Video: Learn Flexbox in 15 Minutes (Web Dev Simplified, YouTube)
flex-basis sets an item's starting size along the main axis. flex-grow distributes leftover space proportionally when the container has room: an item with grow 2 gains twice what a grow-1 sibling gains. flex-shrink distributes the deficit the same way when the items would overflow the container.
The shorthand flex: 1 expands to 1 1 0%, meaning ignore the content's natural size and share all space equally, which makes it the standard 'equal columns' one-liner. flex: none locks an item at its basis.
.sidebar { flex: 0 0 240px; } /* fixed 240px rail: no grow, no shrink */
.content { flex: 1; } /* takes all remaining space */Key point: Expanding flex: 1 into 1 1 0% and explaining the 0% basis is the depth check hiding inside this question.
justify-content distributes items along the main axis. align-items aligns items within their line on the cross axis. align-content distributes the lines themselves when a wrapped flex container has multiple rows and spare cross-axis space; on a single-line container it does nothing at all.
That single-line detail is the differentiator: candidates who've fought a real wrapping bug know align-content only activates with flex-wrap: wrap and room to spare. Grid reuses the same names, with the justify-* properties controlling the inline axis.
Key point: If align-content 'isn't working', check for flex-wrap: wrap first; that's the practical version of this question.
display: grid turns a container into a two-dimensional layout: you define columns and rows with grid-template-columns and grid-template-rows, and children fill the cells automatically or place themselves with line numbers or named areas. gap handles spacing, and the fr unit shares leftover space proportionally.
Grid's break from every earlier technique is that the layout lives on the container, not the items. The parent declares the structure once and items slot in, which is why page-level scaffolding is grid's home ground.
.layout {
display: grid;
grid-template-columns: 240px 1fr; /* fixed rail + fluid content */
grid-template-rows: auto 1fr auto;
gap: 16px;
}Watch a deeper explanation
Video: Learn CSS Grid in 20 Minutes (Web Dev Simplified, YouTube)
grid-template-areas lets you name regions of a grid and draw the layout as text right in the CSS: each quoted string is a row, each word is a cell, and repeating a name spans that region across cells. Items then claim a region with grid-area: name.
Two payoffs: the stylesheet becomes a readable picture of the page, and responsive changes become a redraw, rearranging the entire layout inside one media query without touching any item's rules. A dot marks a deliberately empty cell.
.page {
display: grid;
grid-template-areas:
"header header"
"nav main"
"footer footer";
grid-template-columns: 200px 1fr;
}
.page > header { grid-area: header; }
.page > nav { grid-area: nav; }Key point: Mention redefining the areas inside a media query; layout-by-redrawing is this feature's whole selling point.
Both generate as many columns as fit, using repeat() with a minmax() track size. auto-fill keeps the empty tracks when there are fewer items than columns, so items sit at their minimum-based width. auto-fit collapses the empty tracks, letting the actual items stretch across the freed space.
The difference shows with few items in a wide container: auto-fit stretches two cards across the full row, auto-fill leaves them small beside ghost columns of empty space. Either way, this one line is the standard no-media-query responsive card grid.
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}Key point: 'auto-fit collapses empty tracks, auto-fill keeps them' is the whole answer; the two-cards-in-a-wide-row example proves you've seen it live.
Flexbox lays out along one axis at a time and lets content size drive the result, which suits toolbars, navs, and rows of tags. Grid controls two dimensions from the container, which suits page shells, dashboards, and any design where rows and columns must stay aligned with each other.
They compose rather than compete: a grid usually frames the page while flexbox arranges the innards of each cell. 'Content drives flexbox, the container drives grid' captures the design difference, not just the syntax difference.
| Flexbox | Grid | |
|---|---|---|
| Dimensions | One (row or column) | Two (rows and columns) |
| Sizing driven by | Content | Container's template |
| Best for | Toolbars, navs, tag rows | Page shells, dashboards, galleries |
| Item placement | In source order along the axis | Anywhere in the grid |
Key point: 'One dimension versus two' is table stakes. 'Content-out versus layout-in' is the phrasing that indicates senior.
Watch a deeper explanation
Video: Flexbox or grid - How to decide? (Kevin Powell, YouTube)
Media queries apply CSS conditionally based on viewport width, orientation, resolution, or user preferences like dark mode and reduced motion. Mobile-first means writing base styles for small screens and layering min-width queries on top as space grows, instead of desktop styles with max-width overrides undoing them.
Mobile-first tends to produce less code because small-screen layouts are naturally simple: stack everything. Each breakpoint then adds columns and chrome. The desktop-first inverse spends its queries switching complexity off, which is where override bugs breed.
/* base styles: mobile */
.cards { display: grid; gap: 16px; }
@media (min-width: 768px) {
.cards { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1200px) {
.cards { grid-template-columns: repeat(4, 1fr); }
}Key point: Add that breakpoints should follow the content (wherever the layout breaks), not device names; that's a design-maturity signal.
srcset lists the same image at multiple widths and lets the browser pick the best file for the current viewport and pixel density, guided by a sizes attribute that describes the image's layout width. The picture element goes further, switching sources by media condition or file format.
The division of labor: srcset for resolution switching (same picture, different file sizes), picture for art direction (a tight square crop on mobile, a wide shot on desktop) and for format fallbacks like AVIF down to JPEG.
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="Team on stage at the product launch"
/>Key point: 'srcset for resolution switching, picture for art direction' sorts the two tools in one sentence.
Without it, mobile browsers assume the page was built for desktop, render it on a virtual canvas around 980px wide, and zoom out until it fits, producing tiny unreadable text. width=device-width with initial-scale=1 matches the layout viewport to the device so responsive CSS actually engages.
It's the single line that makes everything else in responsive design work: media queries written for 375px never fire while the browser pretends to be 980px wide. Avoid user-scalable=no; blocking pinch zoom is an accessibility failure.
<meta name="viewport" content="width=device-width, initial-scale=1" />Key point: The accessibility problem with user-scalable=no; it turns a rote question into a judgment answer matters.
z-index only applies to positioned elements (plus flex and grid children), and it only competes within the element's own stacking context. A stacking context is a sealed layering group created by things like a positioned element with z-index, opacity below 1, or any transform. Children can't escape it.
That's the bug behind 'z-index: 999999 does nothing': the element is trapped inside a context whose root sits below the competitor. The fix is raising the context root or removing the property that created the context, never a bigger number.
.parent { opacity: 0.99; } /* quietly creates a stacking context */
.child {
position: relative;
z-index: 9999; /* still loses to... */
}
.rival {
position: relative;
z-index: 10; /* ...this, outside the context */
}Key point: Naming opacity and transform as accidental stacking-context creators is what proves you've debugged this for real.
A transition interpolates a property between two states when something triggers the change, like a hover or a class toggle: it needs a trigger and has an implicit start and end. An animation runs on its own timeline with @keyframes, any number of steps, looping, and direction control.
Choose by autonomy: feedback tied to an interaction is a transition; anything that plays by itself, loaders, pulsing indicators, multi-stage sequences, is an animation. Both should stick to transform and opacity when smoothness matters.
.button { transition: background-color 200ms ease; }
.button:hover { background-color: #222; }
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
.dot { animation: pulse 1.2s ease-in-out infinite; }| Transition | Animation | |
|---|---|---|
| Trigger | State change (hover, class toggle) | Runs on its own |
| Steps | Two implicit states | Any number via @keyframes |
| Looping | No | Yes (iteration-count) |
| Typical use | Hover and focus feedback | Loaders, sequences |
Key point: 'Transitions need a trigger, animations don't' is the crisp opener; the follow-up is usually performance, so have transform and opacity ready.
Watch a deeper explanation
Video: CSS3 Animation & Transitions Crash Course (Traversy Media, YouTube)
transform moves, rotates, scales, or skews an element visually without changing its place in layout: translate(), rotate(), scale(), and skew(), composable in a single declaration. Neighbors don't move, because layout still sees the original box; only the rendered pixels change position.
That layout independence is why transforms animate smoothly: the browser can composite them on the GPU without recalculating layout. It's also why translate(-50%, -50%) can center an element by its own dimensions, something top and left percentages can't do because they resolve against the parent.
.card:hover {
transform: translateY(-4px) scale(1.02);
transition: transform 150ms ease-out;
}Key point: The detail that scores: transform percentages resolve against the element itself, top and left percentages against the containing block.
Custom properties (--accent: #0a7f5b) are values you define once, usually on :root, and read anywhere with var(). Unlike Sass variables, they live in the browser at runtime: they cascade, inherit, respond to media queries and class changes, and JavaScript can read and update them live.
That runtime nature is the whole answer to 'why not Sass variables?': theming by swapping a class on body, dark mode via a media query, and per-component overrides all work without recompiling anything.
:root { --accent: #0a7f5b; --space: 8px; }
.dark { --accent: #7bd8b8; } /* theme swap, no recompile */
.button {
background: var(--accent);
padding: var(--space) calc(var(--space) * 2);
}Key point: 'They cascade at runtime, preprocessor variables vanish at compile time' is the one-line differentiator.
BEM (Block, Element, Modifier) is a naming convention: .card for the component, .card__title for a part of it, .card--featured for a variant. Every selector is one flat class, so specificity stays uniform across the codebase and styles can't leak between components by accident.
The underlying problem is that CSS is global: any selector can hit any element, and specificity wars escalate. BEM solves it by convention; CSS Modules and utility frameworks solve the same problem with tooling. Recognizing it's one problem with several answers is the senior part.
.card { }
.card__title { }
.card__title--muted { }
/* one flat class replaces fragile chains like: */
/* .card div.header h2.title { } */Key point: Frame BEM as one answer to CSS's global namespace, then name a tooling alternative; that shows judgment, not just vocabulary.
Some properties pass from parent to child automatically: mostly text properties like color, font-family, line-height, and text-align. Box properties like margin, padding, border, and width don't inherit, because layouts would collapse into chaos if they did. Inherited values rank below any author rule in the cascade.
You can steer it manually: inherit forces inheritance, initial resets to the spec default, unset picks whichever of those is natural for the property, and revert falls back to the browser stylesheet. Setting font-family once on body works entirely because of inheritance.
Key point: 'Text inherits, boxes don't' is the memorable rule, and it's accurate enough to survive follow-ups.
!important lifts a declaration above all normal declarations in the cascade, regardless of specificity. When two !important rules clash, specificity decides between them. It's a blunt instrument: once one override goes important, the next override has to as well, and the stylesheet starts an arms race.
The legitimate uses are narrow: utility classes that must always win, overriding third-party styles you can't edit, and user stylesheets for accessibility, where the cascade deliberately gives the user's !important the final word.
Key point: Admit you've used it, The third-party case, and describe the arms race; claiming you've never typed it indicates inexperience.
ARIA adds accessibility semantics HTML can't express: aria-expanded on a disclosure button, aria-label for an icon-only control, role="alert" for live announcements. The first rule of ARIA is to prefer native HTML: a real button ships keyboard support and semantics that role="button" on a div never will.
ARIA changes what assistive technology announces, never how elements behave: a role provides no keyboard handling, focus management, or default behavior. Wrong ARIA is worse than none, because it makes confident promises the widget doesn't keep.
<!-- prefer native elements -->
<button aria-expanded="false" aria-controls="menu">Filters</button>
<!-- icon-only controls need an accessible name -->
<button aria-label="Close dialog">×</button>Key point: 'The first rule of ARIA is don't use ARIA' plus one concrete aria-expanded example is the strongest shape for this answer.
Single-line truncation takes three declarations working together: overflow: hidden to clip the text, white-space: nowrap to stop it wrapping, and text-overflow: ellipsis to draw the dots. All three are required; text-overflow alone does nothing, which is why this classic snippet gets fumbled in interviews.
Multi-line truncation uses line-clamp (long written as -webkit-line-clamp with box-orient), now widely supported. Add the caveat: clipped text should stay reachable somewhere, a title attribute or an expandable state, or you've hidden content with no path back to it.
.one-line {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.three-lines {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}Key point: Interviewers watch whether you know all three single-line properties are required; naming just text-overflow fails silently.
Senior front-end rounds probe rendering internals, performance judgment, and accessibility at depth. Expect every answer here to draw a follow-up.
It's the sequence between receiving HTML and painting pixels: parse HTML into the DOM, parse CSS into the CSSOM, combine them into the render tree, run layout to compute geometry, then paint and composite. Anything that stalls an early stage delays the user's first view of the page.
The two classic stalls: CSS is render-blocking, because the browser refuses to paint until styles are known, and synchronous scripts block HTML parsing because they might document.write. Optimizing means shrinking or deferring whatever sits on that path.
The critical rendering path, stage by stage
Anything that stalls an early stage (render-blocking CSS, synchronous scripts) delays the user's first view.
Key point: Walk the stages in order, then The two blockers in practice. The technical sequence matters here.
Browsers won't paint until all applicable stylesheets have loaded and parsed, because painting first and restyling after would flash unstyled content and thrash layout. Every stylesheet linked in the head therefore delays first paint by at least its own download and parse time, on every page load.
The mitigations: inline the critical above-the-fold CSS directly in the head, split the rest per route, mark print styles with media="print" so they stop blocking, and preload fonts the CSS will request. Shipping less CSS beats clever loading every time.
<!-- downloads without blocking render: media doesn't match -->
<link rel="stylesheet" href="print.css" media="print" />
<!-- critical CSS inlined, the rest loaded normally -->
<style>/* hero and nav rules */</style>
<link rel="stylesheet" href="site.css" />Key point: A stylesheet with a non-matching media query still downloads but stops blocking; that detail is this question's core.
A plain script pauses HTML parsing while it downloads and executes. async downloads in parallel and executes the moment it arrives, in whatever order the network delivers. defer downloads in parallel but waits to execute until parsing finishes, preserving the order scripts appear in the document.
The practical mapping: defer for application code that touches the DOM, async for independent scripts like analytics that don't care about order, and end-of-body blocking scripts only in legacy codebases. Modules (type="module") defer by default.
| No attribute | async | defer | |
|---|---|---|---|
| Download | Blocks parsing | In parallel | In parallel |
| Executes | Immediately, blocking | As soon as it arrives | After parsing completes |
| Order preserved | Yes | No | Yes |
| Use for | Legacy only | Analytics, ads | Application code |
Key point: 'async is order-chaos, defer is order-safe' plus the analytics-versus-app-code mapping covers every follow-up.
Reflow (layout) recomputes element geometry; repaint redraws pixels. Changing width or top triggers layout for the element and often much of the tree, then paint, then composite. Changing transform or opacity can skip both stages: the compositor moves or fades an already-painted layer on the GPU.
That's the rule behind smooth animation: animate transform and opacity, avoid animating layout properties like height and margin, and batch DOM reads and writes in JavaScript so interleaved access doesn't force synchronous layout, the pattern known as layout thrashing.
Key point: Naming 'forced synchronous layout' when reads interleave writes shows you've profiled real jank, not just read the rule.
Browsers match selectors right to left: for .nav a, the engine starts from every anchor on the page and walks up looking for an ancestor with .nav, not the other way around. The rightmost part, the key selector, decides how many elements become candidates in the first place.
The honest performance answer: on modern engines, selector matching rarely shows up in profiles, and readability should win. It gets real with universal selectors or deep descendant chains across huge DOMs, but the right-to-left model is what interviews actually check.
Key point: Lead with the right-to-left mechanism, The closing step is 'and in practice it's rarely the bottleneck'. Both halves earn credit.
@layer lets you declare named layers in priority order (reset, vendor, components, utilities) and assign rules to them; the cascade compares layer order before specificity. A rule in a later layer beats any specificity in an earlier one, so utilities can override components with single-class selectors.
It's the language-level fix for specificity wars: import a heavy third-party stylesheet into an early layer and your later-layer styles win calmly, with no !important and no selector inflation. Unlayered styles beat all layered ones, the ordering detail most people miss.
@layer reset, vendor, components, utilities;
@layer components {
.button { background: #0a7f5b; }
}
@layer utilities {
.bg-none { background: none; } /* wins: later layer */
}Key point: The exam detail: unlayered styles beat layered ones. Most candidates guess the opposite.
Container queries style an element based on the size of an ancestor container rather than the viewport: mark the ancestor with container-type: inline-size, then write @container rules against its width. The same card component can render as one column in a sidebar and three in the main area.
That's the difference in one sentence: media queries respond to the page's context, container queries to the component's. They make genuinely reusable responsive components possible, which is why design systems adopted them quickly once cross-browser support landed.
.sidebar, .main { container-type: inline-size; }
@container (min-width: 400px) {
.card { grid-template-columns: 120px 1fr; }
}Key point: 'Viewport versus container' is the headline; the same-card-in-sidebar-and-main example makes it concrete in one breath.
Pick one scoping strategy and enforce it: a naming convention like BEM, build-time scoping like CSS Modules, utility-first like Tailwind, or cascade layers separating reset, vendor, components, and utilities. Keep specificity flat and boring, put design tokens in custom properties, and remove dead rules in the build.
the question needs trade-off literacy, not brand loyalty: conventions depend on team discipline, tooling buys guarantees at build complexity, utilities trade markup verbosity for zero naming and zero dead CSS. The you used, what hurt, and what you'd pick again.
Key point: Answer with a decision you actually made and its cost. War stories outscore taxonomy recitals in advanced rounds.
Holy grail is the classic page: header, footer, and a middle band of nav, main content, and aside, with the middle stretching to fill the viewport. Grid solves it in a few lines: template areas draw the regions, grid-template-rows: auto 1fr auto makes the middle absorb spare height.
The senior half is history and responsiveness: this layout took nested floats and negative margins for a decade, and the grid version collapses to a single column on mobile by redrawing grid-template-areas inside one media query.
body {
display: grid;
min-height: 100vh;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
grid-template-columns: 200px 1fr 240px;
grid-template-rows: auto 1fr auto;
}
@media (max-width: 768px) {
body {
grid-template-areas: "header" "nav" "main" "aside" "footer";
grid-template-columns: 1fr;
}
}Key point: Sketch the areas out loud before typing. The interviewer is watching how you decompose layout, not whether you recall property names.
clamp(min, preferred, max) locks a value between two bounds while the middle expression scales freely: font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3.5rem) grows smoothly with the viewport and never leaves the readable range. One declaration replaces a whole stack of breakpoint font-size overrides.
The accessibility caveat that marks production-ready answers: a pure-vw middle term ignores browser zoom and user font settings, so always mix a rem component into the preferred value, as above, to keep text scalable.
h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3.5rem); }
.container { width: clamp(320px, 90vw, 1200px); }Key point: The rem-plus-vw middle term is the hidden trap; pure vw type breaks browser zoom, and interviewers know to ask.
Fonts block text rendering while they download: browsers either hide text (FOIT) or show a fallback and swap (FOUT). font-display: swap chooses the swap behavior explicitly, preload fetches the file early, and self-hosting woff2 with subsetting cuts the payload itself rather than juggling it.
The remaining problem is layout shift at swap time; size-adjust and metric-matched fallback fonts shrink it. And the cheapest fix of all is fewer weights: every variant is another file on the critical path.
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-display: swap;
}Key point: The FOIT and FOUT explicitly and say which font-display value produces which; that vocabulary is what's being checked.
Four levers: modern formats (AVIF, WebP) with picture fallbacks, responsive files via srcset so phones don't download desktop pixels, loading="lazy" for below-the-fold images, and width and height attributes (or aspect-ratio) so the browser reserves space before a single byte of image arrives.
The layout-shift half is the differentiator: dimension attributes let the browser compute the box up front, which is most of fixing CLS. And never lazy-load the hero image; that delays the largest contentful paint instead of helping it.
<img
src="chart-800.avif"
width="800"
height="450"
loading="lazy"
decoding="async"
alt="Signups by month, January to June"
/>Key point: 'width and height attributes are back' is the modern answer; browsers map them to aspect-ratio and the layout stops jumping.
Never remove outlines without a replacement: keyboard users move through the page by focus, and outline: none with nothing else makes it unusable to them. :focus-visible applies focus rings only for keyboard-driven focus, resolving the old fight between designers hating mouse-click rings and accessibility requiring them.
Beyond styling, focus must be managed: opening a modal moves focus into it and traps tabbing, closing returns focus to the trigger, and custom widgets need tabindex and key handling that native elements would have provided free.
:focus-visible {
outline: 3px solid #0a7f5b;
outline-offset: 2px;
}
/* mouse clicks stay clean, keyboard users keep the ring */
button:focus:not(:focus-visible) { outline: none; }Key point: Bring up :focus-visible before they ask; it shows your accessibility knowledge is current, not folklore.
It's a media feature reflecting an OS-level setting for users whom motion makes ill: for people with vestibular disorders, large animated movement is physically nauseating. Sites check @media (prefers-reduced-motion: reduce) and tone animation down to opacity changes or nothing, while keeping every feature working.
The pragmatic pattern is a global override that shortens all animation to near-zero for those users, plus intent where it matters: reduce means reduce, not necessarily eliminate; a subtle fade is usually fine where a parallax fly-in isn't.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}Key point: Knowing the vestibular reason, not just the media query, is what separates caring from compliance in senior loops.
Shadow DOM gives an element its own encapsulated DOM subtree with scoped styles: outside CSS doesn't reach in, inside styles don't leak out. It's the browser-native isolation behind web components and built-in widgets like video controls, the same idea CSS Modules approximate at build time.
Crossing the boundary is deliberate: custom properties inherit through it (the standard theming API), ::part exposes named internals for outside styling, and :host styles the component from within. Knowing those three doors is the practical layer of this answer.
Key point: Custom properties piercing the shadow boundary is the key fact; it's how every web component exposes theming.
Reproduce and isolate first: confirm the exact browser and version, then cut the page down to a minimal case, deleting CSS until the bug disappears and the guilty declaration is obvious. Comparing computed styles in both browsers' dev tools usually names the property that resolves differently.
Then check support data (MDN, caniuse) and known bug trackers before writing a workaround, prefer a fix that's harmless everywhere over browser sniffing, and keep the reduced case in your notes or tests. The method is the answer; the specific bug barely matters.
Key point: Like every debugging question, this scores process: isolate, compare computed values, verify against support data, fix once.
Three layout mechanisms show up in interviews, and knowing which one fits which job is itself a signal. Float came first and was never meant for layout: it was built to wrap text around images, and the whole industry bent it into page scaffolding for a decade because nothing better existed. Flexbox and grid replaced that work. Flexbox lays content out along one axis at a time and lets content size drive the result, so it suits toolbars, navs, and rows of items. Grid controls two dimensions from the container, so it suits page shells, dashboards, and anything where rows and columns must stay aligned. Saying float belongs in the past, and reaching for flexbox or grid on purpose, is what the key signal is.
| Method | Dimensions | Best at | Watch out for |
|---|---|---|---|
| Flexbox | One axis at a time | Toolbars, navs, tag rows, centering | Two-dimensional grids force nesting and fights |
| Grid | Two (rows and columns) | Page shells, dashboards, card galleries | Overkill for a single row of items |
| Float | One (text wrap) | Wrapping text around an image | Layout use is legacy: needs clearfix, breaks easily |
Prepare in layers, and build things rather than only reading. Most front-end rounds move from concept questions to a live layout or component exercise to a debugging or design discussion, so rehearse each stage rather than only reading answers.
The typical front-end interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages, front-end stacks included. These questions reflect what actually gets asked and evaluated inside the interviews we host for 5,000+ HR teams.
See how the AI Coding Interviewer works