Fundamentals
Every HTML element is rendered as a rectangular box. The box model describes the four layers that make up that box, from inside out:
- Content — the area where text and images appear. Sized by
widthandheight. - Padding — transparent space between the content and the border. Increases the element's visual size.
- Border — a line around the padding. Has width, style, and color.
- Margin — transparent space outside the border. Pushes other elements away.
/* box-sizing: content-box (default) — width/height applies to content only */
.box { width: 200px; padding: 20px; border: 2px solid; }
/* total rendered width: 200 + 20+20 + 2+2 = 244px */
/* box-sizing: border-box — width/height includes padding AND border */
.box { box-sizing: border-box; width: 200px; padding: 20px; border: 2px solid; }
/* total rendered width: 200px exactly */
Always use *, *::before, *::after { box-sizing: border-box; } as a global reset — it makes sizing predictable.
Specificity is the browser's algorithm for deciding which CSS rule wins when multiple rules target the same property. It's expressed as a three-column score (a, b, c):
- a — count of ID selectors (
#id) - b — count of class selectors (
.class), attribute selectors ([attr]), pseudo-classes (:hover) - c — count of element selectors (
div,p) and pseudo-elements (::before)
Inline styles beat all selector specificity. !important overrides inline styles. When scores are equal, the last rule in source order wins (the cascade).
p /* (0,0,1) */
.intro /* (0,1,0) */
#header /* (1,0,0) */
#header .nav li a:hover /* (1,1,2) */
display: none, visibility: hidden, and opacity: 0?| Property | Space in layout | Clickable | Screen reader |
|---|---|---|---|
display: none | Removed | No | Hidden |
visibility: hidden | Preserved | No | Hidden |
opacity: 0 | Preserved | Yes | Visible |
position: relative, absolute, fixed, and sticky.- relative — element stays in normal flow;
top/left/right/bottomoffsets it visually from its normal position. Creates a positioning context for absolutely positioned children. - absolute — removed from normal flow; positioned relative to the nearest positioned ancestor (or
<html>if none). Overlaps other elements. - fixed — removed from normal flow; positioned relative to the viewport. Stays in place during scrolling (sticky headers, modals).
- sticky — hybrid: acts like
relativeuntil it reaches a scroll threshold, then acts likefixed. Common for sticky table headers and navigation bars.
Layout
Flexbox is one-dimensional — it lays items out in a single row or column. Best for: navigation bars, button groups, aligning items within a component, any linear sequence of items.
Grid is two-dimensional — it manages rows AND columns simultaneously. Best for: page-level layouts, image galleries, card grids, any component where items need to align across both axes.
Rule of thumb: if you're thinking in one direction (row or column), use Flexbox. If you're thinking in both directions at once, use Grid. They work great together — a Grid for overall layout, Flexbox inside each Grid cell for component-level alignment.
/* Method 1: Flexbox (most common) */
.parent {
display: flex;
align-items: center;
justify-content: center;
}
/* Method 2: CSS Grid */
.parent {
display: grid;
place-items: center; /* shorthand for align-items + justify-items */
}
/* Method 3: Absolute + transform (works without known dimensions) */
.child {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
}
/* Method 4: Absolute + inset + auto margins */
.child {
position: absolute;
inset: 0;
margin: auto;
width: fit-content;
height: fit-content;
}
em, rem, vw/vh, and px?- px — absolute pixels. Does not respect user's browser font-size preference.
- em — relative to the element's own
font-size. Compounds through nesting (a nested element with1.2eminside another1.2emelement = 1.44em at root). - rem — relative to the root element's (
:root/<html>) font-size. No compounding. Use for typography to respect user preferences. - vw / vh — 1% of viewport width / height. Use for full-screen sections, large hero elements.
- % — relative to the parent's value for the same property.
Best practice: use rem for font sizes, padding, margins; px for borders and fine details; vw/vh for viewport-relative sizes; em for component-internal spacing that should scale with the component's font size.
z-index and what is a stacking context?z-index controls the stacking order of elements on the Z axis (front to back). Higher values appear in front. It only works on positioned elements (relative, absolute, fixed, sticky).
A stacking context is an isolated layer in the stacking order. Elements inside a stacking context are stacked relative to each other within that context — they cannot overlap elements outside it regardless of their z-index. A new stacking context is created by: position + z-index ≠ auto, opacity < 1, transform, filter, will-change, and several other properties.
Responsive Design
Mobile-first means writing the base CSS for small screens, then using min-width media queries to add styles for larger screens. Contrast with desktop-first, which writes for large screens and overrides with max-width queries.
Mobile-first is preferred because: (1) it forces you to prioritize core content and functionality, (2) the CSS base is smaller and faster on mobile devices which typically have slower connections and processors, (3) it's easier to add complexity than to subtract it, and (4) Google's indexing is mobile-first.
min-width and max-width in media queries?/* min-width — applies when viewport IS AT LEAST this wide (mobile-first) */
@media (min-width: 768px) { /* tablet and above */ }
@media (min-width: 1024px) { /* desktop and above */ }
/* max-width — applies when viewport IS AT MOST this wide (desktop-first) */
@media (max-width: 767px) { /* tablet and below */ }
@media (max-width: 480px) { /* mobile only */ }
Effects & Performance
transform instead of top/left for animations?Changing top or left triggers layout reflow — the browser must recalculate the position of every element affected. This is the most expensive rendering operation and causes jank (stuttering) at even 100ms intervals.
Changing transform (and opacity) only triggers the compositor — a separate GPU thread. The browser skips layout and paint entirely. The result is consistently smooth 60fps animation even on mobile. Always animate transform: translateX/Y instead of left/top.
will-change in CSS and when should you use it?will-change: transform tells the browser that this element will be animated soon — allowing the browser to create a separate compositor layer in advance, preventing janky first-frame rendering. Use it only on elements you know will animate (modals about to open, elements with hover transitions). Overusing it consumes significant GPU memory and can degrade overall performance.
- Transitions — animate a property from one state to another, triggered by a state change (hover, focus, class toggle). Simple A → B. Defined with a
transitionproperty on the element. - Animations — multi-step, looping, or self-starting. Defined with
@keyframesand applied withanimationproperty. Don't require user interaction to start.
Advanced Topics
CSS custom properties are declared with --name: value and read with var(--name). They are runtime variables — the browser keeps them live and they can be changed with JavaScript, media queries, or by overriding in a nested scope.
Sass variables are compile-time — they are resolved during the build step and replaced with their values in the output CSS. They are more like constants. CSS variables also cascade and inherit; Sass variables do not.
BEM (Block–Element–Modifier) is a CSS naming convention: .block__element--modifier. Block is the standalone component (.card), Element is a part of it (.card__title), Modifier is a variation (.card--featured). BEM is useful because it keeps all selectors at a single-class level (zero specificity wars), makes the relationship between CSS and HTML explicit, and prevents name collisions between components.
box-sizing: border-box do and why is it commonly applied globally?By default (content-box), width applies only to the content area — padding and border are added on top, making the total rendered size larger than specified. With border-box, width includes the padding and border, so width: 200px is always exactly 200px rendered regardless of padding. Applied globally as *, *::before, *::after { box-sizing: border-box; } it makes sizing predictable everywhere.
:nth-child() and :nth-of-type()?:nth-child(n) counts all siblings regardless of type, then checks if the counted element matches the selector. :nth-of-type(n) counts only siblings of the same element type. If a <div> contains <h2>, <p>, <p>: p:nth-child(2) matches the first <p> (it's the 2nd child); p:nth-of-type(2) matches the second <p>.
- ::before — decorative icons, quote marks, badges — content inserted before the element.
- ::after — clearfix hack, tooltip arrows, ribbon corners.
- ::first-letter — magazine-style drop cap on the first paragraph letter.
- ::placeholder — style input placeholder text color and font.
- ::selection — change the highlight color when a user selects text.
The cascade is the algorithm that determines which CSS declaration wins when multiple rules target the same property. Priority order (highest wins): user !important → author !important → author inline styles → author stylesheet rules (by specificity, then source order) → user stylesheet → browser defaults.
Modern CSS adds @layer — named cascade layers where later-declared layers always win over earlier ones regardless of specificity, giving developers explicit control over override order without fighting specificity.
min-content, max-content, and fit-content?- min-content — the smallest size that avoids overflow. For text, it's the width of the longest word.
- max-content — the ideal unconstrained size. For text, no wrapping — all on one line.
- fit-content — uses max-content up to the available space, then wraps. Like
min(max-content, available-width).
Bonus Questions
/* Automatic dark mode based on user OS preference */
@media (prefers-color-scheme: dark) {
:root {
--bg: #121212;
--text: #f1f1f1;
--primary: #4fc3f7;
}
}
/* CSS-only toggle using a hidden checkbox */
#dark-toggle:checked ~ body { /* or ~ .app */
background: #121212;
color: #f1f1f1;
}
@keyframes in CSS? Write a simple spinning loader.@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loader {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top-color: #1572B6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
contain: layout do and when is it useful?contain: layout tells the browser that layout changes inside this element don't affect elements outside it. This allows the browser to skip recalculating the rest of the page's layout when a contained element changes, which can significantly improve performance for dynamic widgets, infinite scroll lists, and frequently updating components.
aspect-ratio in CSS and how do you use it?/* Maintains a 16:9 ratio as width changes */
.video-wrapper { aspect-ratio: 16 / 9; width: 100%; }
/* Square avatars */
.avatar { width: 48px; aspect-ratio: 1; border-radius: 50%; }
/* Old technique (before aspect-ratio) — padding hack */
/* No longer needed in modern CSS */
fr unit work?fr (fractional unit) represents a fraction of the available space in the grid container after fixed and min-content tracks are sized. grid-template-columns: 1fr 2fr 1fr gives the middle column twice the space of each side column. 1fr is equivalent to minmax(0, 1fr) — the track can shrink to zero. Extremely useful for responsive layouts that distribute remaining space proportionally.
clamp() in CSS?clamp(min, preferred, max) clamps a value between a minimum and maximum. font-size: clamp(1rem, 2.5vw, 2rem) sets a font size that grows with the viewport but never goes below 1rem or above 2rem. Eliminates many media query breakpoints for typography and spacing by making values naturally fluid.
::before pseudo-element and what property is required for it to render?::before inserts a virtual node as the first child of the element. The content property is required — without it, the pseudo-element box is not generated. Use content: "" for decorative shapes. The inserted node is inline by default; change with display: block or position: absolute as needed.
[data-tooltip] { position: relative; }
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 4px 10px;
border-radius: 4px;
white-space: nowrap;
font-size: 0.8rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
[data-tooltip]:hover::after { opacity: 1; }
:focus-visible pseudo-class and why should you use it instead of removing focus outlines?:focus-visible applies only when the browser determines the element was focused by keyboard navigation (Tab key), not by mouse click. This lets you show clear focus rings for keyboard users (critical for accessibility — WCAG 2.1 requires visible focus indicators) while not showing the ring when a mouse user clicks a button. Never use * { outline: none } — it makes your site inaccessible to keyboard users.
@layer in CSS and what problem does it solve?@layer (CSS Cascade Layers) groups CSS declarations into named layers with an explicit priority order. Later-declared layers win over earlier ones regardless of specificity. This solves the problem of integrating third-party CSS (resets, component libraries): put them in low-priority layers so your application styles always win without needing !important. Example: @layer reset, base, components, utilities; — utilities always wins, reset always loses, all without specificity battles.