Semantic HTML means choosing the element that says what the content is, not how it looks. Browsers turn that meaning into the accessibility tree, so screen readers can jump between landmarks and headings, search engines read the structure, and native elements bring behaviour for free.
Landmarks: <header>, <nav>, <main>, <aside> and <footer> become regions a screen reader user can list and jump to; a page has one <main>.
Headings: <h1> to <h6> form the outline most screen reader users navigate by. Pick the level by structure and the size with CSS.
**<article> is self-contained content (a post, a comment, a product card); <section> is a themed part of a page with its own heading.
Behaviour**: <button> is focusable and fires on Enter and Space, <a href> can open in a new tab, <label> focuses its input on click. A <div> does none of this.
<div> and <span> stay right for wrappers that exist only for layout.
ReactJunior
HTML & CSS
Semantic HTML, the box model, display and positioning, units, Flexbox and Grid, margin collapse, responsive design and container queries, custom properties and theming, CSS Modules, Tailwind and CSS-in-JS
Questions closed
0 / 18
- 0Review
- 11Asked often
Questions
Every element is a box of content, padding, border and margin. With the default content-box, width sets only the content, so .card is 344 px wide (300 + 2 × 20 + 2 × 2); with border-box, width includes padding and border, so it is 300 px and the content shrinks to 256 px.
Margin is outside the box: the 16 px margins push neighbours away but are not part of the width, and vertical margins between blocks can collapse into one.
**Why border-box**: width: 100% plus padding no longer overflows the parent, and changing the padding does not change the size of the card.
Most projects set it for every element in a reset, as below; Tailwind's Preflight does the same.
height follows the same rule; outline and box-shadow take no space at all.
A block element starts on a new line, fills the width of its parent and accepts width, height and all margins. An inline element flows inside a line of text, is as wide as its content and ignores width, height and vertical margins. inline-block flows inline but sizes like a block.
Defaults: div, p, h1, ul and section are block; span, a, strong and code are inline. img is inline but, as a replaced element, does take a width and height; button and input are inline-block.
Inline padding: horizontal padding pushes neighbours apart; vertical padding paints a background but does not move the lines around it.
Gaps: whitespace between inline-block items in the HTML renders as a gap of about 4 px, and an inline img sits on the text baseline with a few pixels below it, which display: block removes.
Today a flex or grid parent lays out rows of items, and then the parent's display decides the layout.
display never changes the HTML rules: a <div> inside a <p> is still invalid markup.
display: none removes the element from layout and from the accessibility tree. visibility: hidden keeps its space but hides it from sight, clicks and screen readers. opacity: 0 only makes it transparent: it keeps its space, still takes clicks and focus, and is still read out.
Layout: only display: none lets the neighbours move into the freed space.
Clicks: an opacity: 0 element can still be clicked and tabbed to, the classic invisible overlay that swallows clicks.
Animation: opacity fades smoothly while display switches instantly, so a fade-out animates opacity and switches visibility at the end.
Children: under a visibility: hidden parent a child can set visibility: visible and reappear; under the other two it cannot.
For screen readers only: hide an icon button's label with a visually-hidden class, not with any of the three.
static is normal flow; relative stays in the flow and shifts visually by top and left; absolute leaves the flow and is placed against its nearest positioned ancestor; fixed against the viewport; sticky scrolls normally until it reaches its top offset, then sticks inside its parent.
Positioned ancestor: the nearest ancestor whose position is not static, which is why the parent gets position: relative with no offsets.
**relative keeps its original space, so moving it does not push the neighbours.
fixed that scrolls away**: an ancestor with transform or filter becomes its containing block, so a modal inside an animated card moves with the card. A portal to body avoids it.
**sticky that does not stick**: it needs a top value, an ancestor with overflow: hidden or auto becomes the box it sticks in, and it never leaves its parent.
absolute boxes shrink to their content unless given a width; inset: 0 stretches one over its containing block.
px is fixed, em is relative to the element's font size (the parent's, inside font-size), rem to the root font size, % to a size of the parent, and vh and dvh to the viewport height. Use rem for font sizes, so text follows the font size the user chose in the browser.
**Why not px for text**: raising the browser's default font size scales rem text but not px text. Page zoom scales both.
**em compounds**: 1.2em inside 1.2em is 1.44 times the base; good for padding that grows with a button's text, bad for nested font sizes.
**%**: width, padding and even vertical margin are shares of the parent's width; height: 50% works only when the parent has a set height.
**vh on phones**: 100vh is the height with the browser toolbar hidden, so a full-screen block is cut off while the toolbar shows. dvh follows the toolbar, svh and lvh are its smallest and largest heights; all major browsers since 2022.
Fluid type: clamp(1.75rem, 1.2rem + 2vw, 2.5rem) grows with the screen between two limits.
Make the parent a grid with place-items: center, or a flex container with justify-content: center and align-items: center. Both center a child of any size on both axes without knowing its dimensions.
Horizontal only: margin-inline: auto on a block with a width, text-align: center for inline content such as text.
**margin: auto** inside a flex or grid parent centers the child on both axes; margin-left: auto pushes one toolbar item to the end.
Over other content: position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%), the classic trick for a box of unknown size.
A plain block: align-content: center now centers content vertically without flex or grid (all major browsers since 2024).
It needs room: vertical centering does nothing until the parent is taller than the child, for example with min-height: 100dvh.
vertical-align: middle applies to inline and table-cell content, which is why it does nothing on a div.
A pseudo-class (:hover, :focus-visible, :nth-child()) selects an existing element in a state or position; a pseudo-element (::before, ::after, ::placeholder) styles a part that is not a DOM element. :has() matches an element by its contents, so CSS can style a parent by its children.
Syntax: one colon for pseudo-classes, two for pseudo-elements; the old ones such as :before still work with one.
**::before and ::after** need a content value, even "". They suit decoration such as arrows; their text cannot be selected, so real content stays in the HTML.
Worth knowing: :focus-visible shows a focus ring for keyboard users only, :user-invalid marks a field only after the user has interacted with it, :is() and :where() group selectors.
**:has()**: .card:has(img) styles cards that contain an image, form:has(:invalid) .submit dims the button, replacing scripts that only toggled a class on a parent. All major browsers since December 2023.
Vertical margins of adjacent blocks merge into one, the larger of the two, instead of adding up, and a first child's top margin can escape through its parent. A block formatting context (BFC) is an isolated layout region: margins do not collapse across its edge and it contains its floats.
Siblings: margin-bottom: 24px above margin-top: 16px leaves 24 px, not 40 px. Horizontal margins never collapse.
Parent and child: a heading's margin-top in a card with no padding or border pushes the whole card down, outside its background. Padding, a border or a BFC on the card keeps it inside.
No collapsing: between flex or grid items, on floats and absolutely positioned elements, and across a BFC edge.
What creates a BFC: display: flow-root (made for exactly this), overflow other than visible or clip, floats, absolute positioning, inline-block, and flex and grid items.
Floats: a BFC parent grows to contain floated children, which replaced the clearfix hack.
In practice: gap, or margins in one direction only, leave nothing to collapse.
flex-basis is an item's starting size along the main axis, flex-grow its share of the free space left over, and flex-shrink how much it gives up when the items do not fit. flex: 1 means 1 1 0%: every item starts from zero and the whole row is split equally.
Default: 0 1 auto: items keep their content size, never grow, and shrink if they must.
**flex: 1 vs flex: auto**: flex: auto (1 1 auto) starts from the content size and shares only the leftover, so a longer label gets a wider item; flex: 1 gives two items in a 600 px row 300 px each, whatever their text.
Shrinking is weighted: the overflow is taken in proportion to flex-shrink times the basis, so a wide item gives up more pixels.
**flex: none** (0 0 auto) or flex-shrink: 0 keeps an icon or a button from being squeezed.
**The min-width: auto floor**: an item does not shrink below its longest word or unbroken URL unless you set min-width: 0, the usual reason text overflows a flex row.
.info is a flex item with the default min-width: auto, so it cannot shrink below its content, and with nowrap the content is the whole file name. .name is never narrower than its text, the ellipsis has nothing to cut, and the row overflows instead. min-width: 0 on .info fixes it.
Why the ellipsis never fires: text-overflow works only when the element is narrower than its text, and .name is as wide as .info, which is as wide as the text.
Why it works without the wrapper: the same three lines on a direct flex item do truncate, because an item with overflow other than visible gets a minimum size of 0. The trap is the extra div.
The fix: min-width: 0 on the flex item that contains the text. In a column it is min-height: 0, the usual reason a scrollable panel inside a flex column refuses to scroll.
The buttons: flex-shrink: 0 keeps them at full size once the name starts giving way.
Grid has the same floor: 1fr means minmax(auto, 1fr), and the fix there is minmax(0, 1fr).
fr is a share of the space left after fixed tracks, minmax(min, max) lets a track flex between two sizes, and repeat() writes a track list once. With auto-fill or auto-fit it creates as many columns as fit; auto-fit then collapses the empty ones, so a few items stretch across the row.
**fr comes last**: in 200px 1fr 2fr on an 800 px grid, the remaining 600 px are split 200 and 400.
**1fr has a floor**: it means minmax(auto, 1fr), so a long word keeps its column wide and two 1fr columns come out unequal; minmax(0, 1fr) makes them equal.
**auto-fill vs auto-fit**: three cards with room for five 200 px columns stay 200 px wide with auto-fill, which keeps the empty tracks; with auto-fit they share the full width. A full row looks the same with either.
No media queries: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); the inner min() prevents overflow on screens narrower than 240 px.
Named areas: grid-template-areas draws the layout as text, so a media query can rearrange it without touching the HTML. Keep the visual order close to the DOM order, which keyboard users follow.
Custom properties (--accent: #2563eb, read with var(--accent)) are real CSS values in the browser: they inherit down the DOM, can be redefined per element or media query and changed at runtime. Sass variables are replaced with fixed values at build time and do not exist in the shipped CSS.
Themes: tokens on :root, new values under [data-theme="dark"], and every component using var(--surface) switches with no extra rules. The attribute comes from the saved choice or from prefers-color-scheme.
Scope: --card-padding set on one container changes only that subtree, a knob without new classes.
Fallbacks: var(--card-padding, 16px) covers an undefined property; a value that is invalid for the property makes it unset, it does not fall back to the previous rule.
**color-scheme: dark** darkens scrollbars and form controls too; light-dark(#fff, #111) picks a value by the active scheme (all major browsers since 2024).
No flash: set the attribute on <html> from a tiny inline script in <head>, before the first paint. Changing a variable from JavaScript restyles the page without a React re-render.
Mobile-first means the base styles are for the narrowest screen and @media (min-width: 768px) blocks add columns as space grows. Media queries ask about the viewport; container queries ask about the width of a component's container, so a card adapts wherever it is placed.
The viewport tag: without <meta name="viewport" content="width=device-width, initial-scale=1"> a phone lays the page out about 980 px wide and zooms out, so the phone styles never apply.
Why mobile-first: each breakpoint only adds to the simple layout instead of undoing desktop rules. Put breakpoints where the content breaks, not at device widths.
Fluid first: flex-wrap, Grid with auto-fit and clamp() font sizes remove most breakpoints.
Container queries: container-type: inline-size on the wrapper, @container (min-width: 420px) in the component. The same card goes horizontal in a wide column and stays stacked in a sidebar, which a media query cannot tell apart. Supported everywhere since 2023.
Limit: an element cannot query its own size, and the container's width can no longer come from its content, so a shrink-to-fit wrapper collapses.
srcset lists the same image in several widths and sizes tells the browser how wide it will be shown, so it picks the smallest file that is sharp on that screen. <picture> switches the file or format by condition. The width and height attributes reserve the space before the image loads.
Width descriptors: with 480w and 960w files and sizes="100vw", a 400 px phone with a 2x screen needs 800 pixels and gets the 960 one.
**Without sizes** the browser assumes 100vw, so a grid thumbnail downloads a full-screen file. Describe the layout: sizes="(min-width: 1024px) 33vw, 100vw".
**<picture>**: <source type="image/avif"> before the <img> fallback serves new formats where supported; <source media> gives phones a different crop. The inner <img> is required and carries alt.
No layout shift: the attributes give the aspect ratio up front, while CSS width: 100%; height: auto still scales the image.
Loading: loading="lazy" below the fold, never on the hero, which gets fetchpriority="high" as the likely LCP element.
A transition animates a property from its old value to the new one when it changes, for example on :hover or a class toggle. A @keyframes animation plays a defined sequence by itself, can loop and needs no change of state. Animate transform and opacity, which skip layout and paint.
Transitions have two states: transition: transform 200ms ease-out on the element, the new value under .is-open. Interrupted halfway, they reverse smoothly from where they are.
Keyframes suit loaders, pulses and entrances: animation: spin 1s linear infinite.
Expensive properties: width, height, top and margin re-run layout every frame; slide with translate and grow with scale instead.
**will-change: transform gives the element its own layer ahead of time; use it on a few elements, since every layer costs GPU memory.
Reduced motion**: turn large movement off under @media (prefers-reduced-motion: reduce).
**From display: none**: @starting-style sets the first frame, so an element can fade in when it is shown (all major browsers since 2024). Fading out to display: none with transition-behavior: allow-discrete works in Chrome and Safari, not yet in Firefox.
Plain CSS is global, so each approach avoids clashes differently: BEM by naming, CSS Modules by renaming classes per file at build time, Tailwind with utility classes instead of names, CSS-in-JS by generating styles in the component. Only runtime CSS-in-JS costs JavaScript in the browser.
BEM (.card__title--active): no tooling and flat specificity, but only discipline enforces it.
CSS Modules: styles.button becomes something like Button_button__x7a2 in the build. Plain CSS, zero runtime, built into Vite and Next.js; styling a child from outside needs a className prop.
Tailwind: utilities in the markup from a fixed scale, and only the classes you use reach the CSS. The cost is long class lists; clsx and tailwind-merge handle conditional and conflicting ones.
Runtime CSS-in-JS (styled-components, Emotion): styles can read props and a theme, but CSS is built and injected while rendering, the library adds to the bundle, and server rendering needs extra setup.
Zero-runtime CSS-in-JS (vanilla-extract, Panda CSS, StyleX): styles in TypeScript, compiled to static CSS.
Dynamic values: pass a CSS variable through style instead of generating a class per value.
Runtime CSS-in-JS turns styles written in JavaScript into CSS while components render: the library ships to the browser, injects rules during rendering and must flush them next to streamed HTML. Server Components have no context and aim to ship no JavaScript, and these libraries rely on both.
What it costs
Render work: each render of a styled component serializes its styles, hashes them into a class name and inserts any new rule; color: ${(p) => p.color} creates a class for every distinct value.
Style recalculation: every injected rule makes the browser recalculate styles. The React docs advise against runtime <style> injection and recommend static CSS files plus inline styles for dynamic values.
Streaming SSR: the server must send each chunk's rules before its HTML (in the App Router, a registry Client Component with useServerInsertedHTML), or the page flashes unstyled.
Why Server Components make it worse
ThemeProvider and the style registry live in React context, which Server Components cannot read, so for years every styled file needed "use client" and its code went back into the bundle.
Since 6.3 (January 2026) styled-components renders in Server Components by emitting inline <style> tags, but ThemeProvider does nothing there, so themes move to CSS variables. Emotion still works only in Client Components.
What teams use instead
CSS Modules or plain CSS with custom properties, and Tailwind: static CSS built ahead of time, fine in Server Components.
Zero-runtime CSS-in-JS: vanilla-extract, Panda CSS, StyleX or Linaria compile typed styles to static CSS.
Migration: new code the static way, leaf components first, prop-driven styles replaced by CSS variables.