HTML & CSS fundamentals show up in nearly every frontend interview — the box model, layout systems, and specificity in particular.
Every element in CSS is a rectangular box made of four layers stacked from the inside out: `content`, `padding`, `border`, and `margin`. The content area holds the actual text or child elements, padding is the transparent cushion between content and border, the border wraps around padding, and margin is the space outside the border that separates the element from its neighbors. By default browsers use `box-sizing: content-box`, so width and height only apply to the content area and padding/border get added on top, which is why elements often look bigger than the width you set. Switching to `box-sizing: border-box` makes width and height include padding and border, which is much more predictable and is why most reset stylesheets apply it globally. Understanding this model is essential for debugging layout issues like unexpected overflow or elements not lining up.
Specificity is the algorithm the browser uses to decide which rule wins when multiple selectors target the same element. It's usually expressed as a four-part tuple, inline styles, IDs, classes/attributes/pseudo-classes, and elements/pseudo-elements, and you compare those counts left to right rather than adding them like decimal numbers. The cascade is the broader process layered on top of that: it first considers origin and importance, like browser styles versus author styles versus `!important` declarations, then specificity, and finally source order, where the last rule declared wins if everything else ties. A common gotcha is that a highly specific selector written early in the stylesheet still beats a low-specificity selector written later. In practice, keeping specificity low and consistent, avoiding IDs and `!important` in your CSS, makes styles far easier to override and maintain.
A `block` element, like a `div` or `p`, always starts on a new line and takes up the full width of its container by default, and you can freely set its width, height, and vertical margin/padding. An `inline` element, like `span` or `a`, only takes up as much width as its content needs, sits inline with surrounding text, and ignores width/height and top/bottom margin. `inline-block` is a hybrid, it flows inline with other content like an inline element but lets you set width, height, and vertical spacing like a block element. This makes `inline-block` useful for things like navigation items or buttons that need to sit side by side but still respect a fixed size. Modern layout tends to reach for flex or grid instead, but this trio still comes up constantly for quick component styling.
Flexbox is a one-dimensional layout model, it's built for arranging items in a single row or column and excels at things like navbars, button groups, or centering content where items need to grow, shrink, or distribute space along one axis. Grid is two-dimensional, it lets you define rows and columns simultaneously, so it's the better tool for overall page layouts, card grids, or anything where content needs to align across both axes at once. A practical rule of thumb is to reach for flex when you're laying out items in a line and grid when you're laying out a structure. They're not mutually exclusive either, a very common pattern is a grid for the page skeleton with flex used inside individual grid cells to align their content. Both are fully supported now, so the choice really comes down to whether your layout problem is fundamentally one axis or two.
`static` is the default, the element just follows normal document flow and top/left/right/bottom have no effect. `relative` keeps the element in normal flow but lets you nudge it visually with offsets, and importantly it establishes a positioning context for any absolutely positioned children. `absolute` removes the element from normal flow entirely and positions it relative to the nearest ancestor with a non-static position, or the initial containing block if none exists. `fixed` also removes it from flow but positions it relative to the viewport, so it stays put even when the page scrolls, unless an ancestor has a `transform`, `filter`, or similar property that creates its own containing block. `sticky` is a hybrid that behaves like `relative` until the element crosses a defined threshold during scrolling, at which point it locks into place like `fixed` within its parent's bounds. Knowing which ancestor becomes the reference point is usually the actual source of bugs, not the property values themselves.
Semantic HTML means using elements like `header`, `nav`, `main`, `article`, `section`, and `footer` for what they actually mean rather than wrapping everything in generic `div`s. It matters most for accessibility, because screen readers rely on this structure to build a navigable outline of the page, letting users jump straight to the main content or a specific landmark. It also helps SEO, since search engine crawlers weigh content inside meaningful tags like `article` or `h1` more heavily than the same content in an unstyled `div`. Beyond that, semantic markup makes code more self-documenting for other developers and often reduces how much ARIA you need to bolt on afterward, since elements like `button` or `nav` come with built-in roles and keyboard behavior. It's essentially free architecture that pays off in accessibility, discoverability, and maintainability all at once.
An `id` is meant to be unique, a single value should only appear once per page, and in CSS it's targeted with `#`. A `class` is reusable, you can apply the same class to as many elements as you want, and it's targeted with `.`. The practical difference that trips people up in interviews is specificity, an ID selector is weighted much higher than a class selector, so overusing IDs in your stylesheets makes styles harder to override later. IDs are still useful for JavaScript hooks, form label associations, or fragment links like `#section2`, but for styling, classes are almost always the better default because they compose more predictably. A good rule of thumb is to reserve IDs for uniquely identifying an element in the DOM and use classes for anything related to visual presentation.
`px` is an absolute unit, it doesn't scale with anything else, which makes it predictable but less flexible for accessibility since it won't respond well to a user's browser font-size settings. `em` is relative to the font-size of the current element's parent, which is powerful but can compound unexpectedly when you nest elements that each set their own font-size, since each em multiplies against the inherited value. `rem` is relative to the root `html` element's font-size only, so it doesn't have that compounding problem and gives you consistent scaling across a whole page, which is why it's become the default choice for spacing and typography. Percentage is relative to some property of the parent, usually width for layout properties, so a `width: 50%` child takes up half its parent's content box. In practice most teams use rem for font sizes and spacing, percentages for fluid widths, and px sparingly for things like borders that genuinely shouldn't scale.
Responsive design means building a layout that adapts to different screen sizes and devices rather than shipping a fixed-width page. Media queries are the CSS mechanism that makes this possible, they let you conditionally apply styles based on characteristics like viewport width, using syntax like `@media (max-width: 768px) { ... }`. Most teams follow a mobile-first approach, writing base styles for small screens and layering on `min-width` media queries to progressively enhance the layout for larger viewports, since that tends to produce leaner CSS than the reverse. Beyond media queries, responsive techniques include flexible units like percentages and `rem`, fluid images with `max-width: 100%`, and layout tools like flexbox and grid that naturally reflow. You also need the viewport meta tag in the HTML head, without it mobile browsers assume a desktop-width layout and media queries won't behave as expected.
`z-index` controls the vertical stacking order of positioned elements along the axis coming out of the screen, higher values sit visually on top of lower ones. It only has an effect on positioned elements, meaning anything other than `static`, with one common exception, flex and grid items respect z-index for stacking purposes even without an explicit position value set. A stacking context is a self-contained grouping for this order, created by things like a positioned element with a z-index, an element with `opacity` less than 1, or one with `transform`, `filter`, or `will-change` set. The tricky part interviewers like to probe is that z-index values are only compared within the same stacking context, so a child with `z-index: 9999` can still end up behind another element if its parent's stacking context itself is lower in the stack. This is a classic source of "why isn't my z-index working" bugs, and the fix is usually to trace which ancestor is creating a new stacking context rather than just cranking the number higher.
A pseudo-class, written with a single colon like `:hover` or `:nth-child(2)`, targets an element based on a state or position it's already in, it doesn't add anything new to the document. A pseudo-element, written with a double colon like `::before` or `::after`, lets you style or insert a specific part of an element that doesn't exist as its own node in the DOM, such as the first line of a paragraph or generated content. Browsers still accept a single colon for pseudo-elements for legacy reasons, but the double-colon syntax is the modern standard and makes the distinction clearer. A common practical example is using `:focus` to change a button's outline versus using `::before` to inject a decorative icon without adding extra markup. Knowing this distinction also matters for accessibility work, since pseudo-elements aren't part of the accessibility tree and can't hold interactive content.
The browser resolves this through the cascade, walking through origin and importance first, then specificity, then source order. If one rule has `!important` it wins over normal rules regardless of specificity, unless another `!important` rule with higher specificity fights it. Assuming neither is important, you compare specificity as a tuple of inline styles, IDs, classes/attributes/pseudo-classes, and elements, and the higher count at the first point of difference wins. If specificity is exactly equal, the tiebreaker is just source order, whichever rule was declared last in the cascade takes effect. This is why debugging "my CSS isn't applying" issues usually means opening dev tools, checking which rule is actually being applied and which is crossed out, and working backward from there rather than guessing.
`display: none` completely removes the element from the rendering tree, it takes up no space, and it's as if the element doesn't exist for layout purposes, though it's still in the DOM and accessible via JavaScript. `visibility: hidden` hides the element visually but it still occupies its normal space in the layout, so surrounding elements don't shift to fill the gap. Another difference is inheritance and interaction, `visibility: hidden` is inherited by children by default, though a child can override it back to visible, while `display: none` on a parent makes it impossible for any descendant to be shown regardless of its own display value. Neither state is announced by screen readers, but there's also `opacity: 0`, which is visually invisible yet still occupies space and remains focusable and clickable, which trips a lot of people up. Picking the right one usually comes down to whether you want the layout to reflow when the element disappears.
BEM stands for Block, Element, Modifier, and it's a naming methodology for CSS classes designed to keep large stylesheets predictable and to avoid specificity wars. A Block is a standalone component like `card` or `nav`, an Element is a part of that block written as `block__element` like `card__title`, and a Modifier is a variation of a block or element written as `block--modifier` like `card--highlighted`. The core idea is that every class is flat and self-describing, so you almost never need to nest selectors or rely on descendant combinators, which keeps specificity uniformly low across the whole codebase. This makes styles easier to reuse and safer to delete, because a class name like `card__title` tells you exactly where it belongs without needing to trace the HTML structure. It's especially valuable on teams working without a component framework's built-in style scoping, since it gives you scoping through naming discipline instead.
CSS custom properties, often called CSS variables, are declared with a double-dash prefix like `--main-color: #3366ff;`, typically on `:root` for global scope, and read with `var(--main-color)`. Unlike Sass variables, which are compiled away and resolved once at build time, custom properties are live in the browser, they participate in the cascade and can be read and updated at runtime with JavaScript or overridden inside a media query or a specific component's scope. That runtime behavior is what makes them genuinely useful for things like theming, you can redefine the same variable inside a `.dark-mode` class or a `prefers-color-scheme` media query and every element using `var()` updates automatically without rebuilding any CSS. They also inherit down the DOM tree like other CSS properties, so you can scope a variable to a component and have it cascade to children while still allowing a fallback value like `var(--gap, 16px)`. In modern codebases they're often used alongside a preprocessor rather than as a full replacement, since Sass is still handy for things like mixins and loops that custom properties can't do on their own.
By default, the `content-box` model calculates an element's declared width and height as applying only to the content area, so any padding and border you add get tacked on top and the element's total rendered size ends up bigger than the number you specified. `border-box` changes that calculation so width and height include the padding and border, meaning a `200px` wide box stays exactly `200px` wide no matter how much padding or border you add, with the content area shrinking to make room instead. This is why almost every reset stylesheet includes something like `*, *::before, *::after { box-sizing: border-box; }`, it makes sizing math far more predictable, especially in responsive grids where percentage widths combined with fixed padding would otherwise break your layout. It doesn't affect margin at all, margin always sits outside the box regardless of which box-sizing model you use. Once you're used to border-box, going back to the default model feels like a constant source of off-by-a-few-pixels bugs.
For a modern one-liner, making the parent a flex container with `display: flex; justify-content: center; align-items: center;` centers a child both ways regardless of its size, and it's usually the first thing to reach for. Grid offers the same result with `display: grid; place-items: center;` on the parent, which is even shorter. For a single element with an unknown width and height, the classic absolute-positioning trick is `position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);` relative to a positioned ancestor, which centers based on the element's own dimensions without needing to know them in advance. Text-only centering is simpler, `text-align: center` handles horizontal centering of inline content, and for a single line of text vertically you can just match `line-height` to the container's height. Interviewers often ask this specifically to see whether you reach for old hacks or know the flex/grid approach, so leading with flexbox or grid is usually the strongest answer.
The tag `<meta name="viewport" content="width=device-width, initial-scale=1.0">` tells mobile browsers to render the page at the actual width of the device rather than assuming a desktop layout, roughly 980px, and then zooming it out to fit the screen. Without it, your responsive CSS and media queries technically still run, but they're evaluated against that fake desktop-width viewport, so a `max-width: 600px` media query might never trigger even on a phone because the browser is reporting a much wider virtual width. `width=device-width` maps the CSS viewport to the device's actual screen width, and `initial-scale=1.0` sets the initial zoom level so content isn't pre-shrunk. It's one of those things that's easy to forget because a page will still look fine on desktop without it, the failure only shows up on real mobile devices, which makes it a common gotcha for people testing responsive designs only in a resized desktop browser window. It's genuinely one of the most impactful single lines you can add to make a page behave responsively at all.