DOM, HTML & CSS
DOM manipulation, display:none vs visibility:hidden, HTML semantics, and CSS fundamentals (box model, flexbox, grid, specificity, positioning).
The DOM is the live, in-memory representation of the page that JavaScript reads and mutates; HTML gives that tree meaning; CSS lays it out. This chapter is the working vocabulary for all three — the distinctions that come up daily and the ones interviewers probe.
DOM manipulation, and why it’s slow
The DOM is a tree of objects. JavaScript reshapes the page by creating nodes (createElement), inserting them (append, insertBefore), removing them, or editing their attributes and text. Every structural change can invalidate layout — so the cost isn’t the JS, it’s the reflows it provokes.
The classic optimization is to batch DOM writes so the browser reflows once. Building nodes off-document (in a DocumentFragment) and inserting the whole subtree in a single operation is the canonical pattern.
// SLOW — 1000 separate insertions, each a potential reflow
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
list.appendChild(li); // touches the live DOM each time
}
// FAST — build off-document, insert once (one reflow)
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
frag.appendChild(li); // not in the live DOM yet
}
list.appendChild(frag); // single insertion of the whole subtreeA DocumentFragment is a lightweight container that isn’t part of the page, so mutating it costs nothing. Appending it moves its children into the DOM in one shot.
display:none vs visibility:hidden vs opacity:0
Three ways to “hide” an element that are not interchangeable — they differ in whether the element occupies space and whether it’s in the render tree at all.
| Property | In render tree? | Occupies space? | Clickable? | Reachable by tab/AT? |
|---|---|---|---|---|
display:none | no (removed) | no — collapses | no | no |
visibility:hidden | yes | yes — reserves its box | no | no |
opacity:0 | yes | yes | yes — still receives clicks | yes |
display:none removes the node from the render tree, so it triggers a reflow when toggled and the element is fully gone for layout and accessibility purposes. visibility:hidden keeps the box (and its space) but paints nothing. opacity:0 keeps it fully present and interactive — it’s just transparent — which is a common a11y trap: an opacity:0 button is still focusable and clickable by screen readers.
HTML semantics
Semantic elements carry meaning that a <div> does not. <header>, <nav>, <main>, <article>, <aside>, <section>, and <footer> describe the role of a region, which buys you three things for free:
- Accessibility. Screen readers expose landmarks (
<nav>,<main>) so users can jump straight to the navigation or main content. A<button>is keyboard-focusable and announces its role; a clickable<div>is neither without extra ARIA andtabindex. - SEO and machine readability. Search engines and readers use the structure to understand the page.
- Less code. Native elements come with built-in behavior (a
<button>fires on Enter/Space; an<a href>is focusable and navigable) that you’d otherwise reimplement.
The box model
Every element is a rectangular box composed, from the inside out, of content → padding → border → margin. The one thing to get right is how width is measured:
/* default: width = content only; padding + border add to the rendered size */
.legacy { box-sizing: content-box; width: 200px; padding: 20px; } /* renders 240px wide */
/* sane: width includes padding + border — what you almost always want */
.modern { box-sizing: border-box; width: 200px; padding: 20px; } /* renders 200px wide */
box-sizing: border-box makes width mean the total rendered width (padding and border included), which is why most resets set it globally. Note that vertical margins between adjacent block elements collapse — the larger of the two wins rather than summing.
Flexbox vs Grid
Both are modern layout systems; the rule of thumb is Flexbox for one dimension, Grid for two. The single image that makes it click: Flexbox distributes items along one axis (a row or a column), while Grid positions items into cells defined by two axes at once.
| Flexbox | Grid | |
|---|---|---|
| Dimensions | one axis (a row or a column) | two axes (rows and columns at once) |
| Driven by | content — items size to their content, then flex | the container — you define the track template up front |
| Best for | toolbars, nav bars, centering, distributing a row of items | page layouts, dashboards, card galleries, any real grid |
| Key props | justify-content, align-items, flex, gap | grid-template-columns, grid-template-rows, gap, grid-area |
Specificity
When two rules set the same property on the same element, specificity decides the winner. Compute it as a tuple (inline, ids, classes, elements) and compare left to right — like comparing version numbers, where a single higher digit on the left beats any number of digits on the right:
| Selector | (inline, id, class, element) | Wins against |
|---|---|---|
div p | (0, 0, 0, 2) | almost everything |
.menu a | (0, 0, 1, 1) | any pure element selector |
#nav | (0, 1, 0, 0) | any number of classes/elements |
style="…" (inline) | (1, 0, 0, 0) | any selector — short of !important |
Positioning
position controls how an element is placed and what top/right/bottom/left mean relative to.
| Value | Positioned relative to | In normal flow? | Use for |
|---|---|---|---|
static | nothing (the default; offsets ignored) | yes | ordinary document flow |
relative | its own original position | yes — keeps its space | nudging an element; creating a positioning context for children |
absolute | nearest positioned ancestor | no — removed from flow | tooltips, badges, overlays anchored to a parent |
fixed | the viewport | no — removed from flow | sticky headers, modals, back-to-top buttons |
sticky | scroll position (relative until a threshold, then fixed) | yes, then pins | section headers that stick while their section is in view |
An absolute element looks for the nearest ancestor that is itself positioned (relative/absolute/fixed/sticky); if none exists, it anchors to the initial containing block. The standard idiom is to set position:relative on a parent precisely to give its absolute children something to anchor to.
01 Learning objectives
0 / 3 done02 Knowledge check
- 01easy
display:none vs visibility:hidden — which still occupies layout space?
- 02medium
Higher CSS specificity is given to:
03 Interview questions
browse all ↗What gets asked on this topic — tap a card for how to approach it, the follow-ups, and the trap. Company tags are best-effort & sourced.
-
What is the difference between display:none and visibility:hidden?
display:noneremoves the element from the render tree entirely: it occupies no space, is not painted, and is skipped by most assistive tech. Toggling it triggers reflow.visibility:hiddenkeeps the element in layout — it still occupies its box and affects siblings — but is not painted (invisible). It is not interactive.A third option,
opacity:0, is fully painted and still interactive (clickable) and laid out; it just renders transparent.Follow-ups they push on- Which of the three is keyboard-focusable / clickable?
- Which triggers reflow when toggled vs only repaint?
Red flag Saying visibility:hidden removes the element from layout — it still occupies space. Confusing opacity:0 (still clickable) with display:none.
source: MDN — visibility ↗ -
Explain the CSS box model and the box-sizing property.
Every element is a box with four areas, from inside out: content, padding, border, margin.
With the default
box-sizing: content-box, thewidthyou set applies to the content only; padding and border are added on top, so the rendered box is wider thanwidth.With
box-sizing: border-box,widthincludes content + padding + border, so the element stays the size you set. This is why many resets apply* { box-sizing: border-box; }.Follow-ups they push on- Why do margins collapse vertically between block elements?
- Does margin count toward the element's width in either box-sizing mode?
Red flag Forgetting that margin is always outside the box (never part of width), or not knowing border-box folds padding/border into the declared width.
source: MDN — The box model ↗ -
How does CSS specificity work, and what wins between an ID selector and 10 classes?
Specificity is scored as a tuple (inline, IDs, classes/attributes/pseudo-classes, elements). Higher tuples win; ties are broken by source order (last wins).
An ID is (0,1,0,0). Ten classes is (0,0,10,0). The ID still wins because the ID column outranks the class column regardless of count — it is not a base-10 sum where 10 classes overflow into the ID column.
!importantoverrides normal specificity; inline styles outrank selectors. Use these sparingly.Follow-ups they push on- Where do !important and inline styles sit in the cascade?
- How do :where() and :is() affect specificity?
Red flag Treating specificity as a single base-10 number so '10 classes beat 1 ID' — columns do not carry over.
source: MDN — Specificity ↗ -
When would you use Flexbox versus CSS Grid?
Flexbox is for one-dimensional layout — a row or a column — where you distribute space along a single axis (nav bars, toolbars, centering, equal-height items in a row).
Grid is for two-dimensional layout — rows and columns together — where you place items into a defined grid (page layouts, card galleries, dashboards).
They compose: a grid cell can itself be a flex container. Reach for Grid when you care about both axes at once; Flexbox when content drives a single axis.
Follow-ups they push on- How do you center an element both horizontally and vertically with each?
- What does flex: 1 actually mean (flex-grow/shrink/basis)?
Red flag Calling Grid 'just for grids of images' or Flexbox 'two-dimensional'. The key distinction is 1D vs 2D.
source: MDN — Relationship of grid layout to other layout methods ↗ -
Explain CSS position values: static, relative, absolute, fixed, and sticky.
static— default; in normal flow, top/left ignored.relative— stays in flow but offset from its normal spot; becomes a positioning context for absolute children.absolute— removed from flow; positioned relative to the nearest positioned ancestor (else the initial containing block).fixed— removed from flow; positioned relative to the viewport, so it stays put on scroll.sticky— a hybrid: behaves likerelativeuntil it crosses a scroll threshold, then sticks likefixedwithin its container.Follow-ups they push on- What makes an ancestor a 'positioned' ancestor for absolute children?
- Why might position:sticky silently not work (overflow on an ancestor)?
Red flag Saying absolute is relative to the viewport (that is fixed), or that sticky is independent of its containing block.
source: MDN — position ↗ -
Why does semantic HTML matter? Give examples beyond <div> and <span>.
Semantic elements describe meaning, not just appearance, which benefits accessibility, SEO, and maintainability.
Elements like
<header>,<nav>,<main>,<article>,<section>,<aside>,<footer>create landmarks that screen readers and the accessibility tree expose, letting users jump between regions.<button>,<a>,<label>,<input>come with built-in keyboard behavior and roles.A
<div>with a click handler has none of that — you would have to re-add role, tabindex, and key handling manually.Follow-ups they push on- What do you lose by using <div onClick> instead of <button>?
- How do landmark elements help screen-reader navigation?
Red flag Treating semantics as purely cosmetic, or reinventing a button from a div without role/tabindex/keyboard support.
source: MDN — HTML: A good basis for accessibility ↗ -
What is the difference between the HTML attribute and the DOM property (e.g. input value)?
The attribute is what is written in the HTML source; the property is the live value on the DOM object. They are linked at parse time but can diverge.
For an
<input value="hi">:getAttribute("value")returns the original"hi"(the default), whileinputEl.valuereflects what the user has currently typed. Editing the box changes the property, not the attribute.Some attributes are reflected (
id,className), others are not symmetric (value,checked). This is why React tracksvalueas state.Follow-ups they push on- Why does setAttribute('value', ...) not update what the user sees after they have typed?
- How does this relate to controlled vs uncontrolled inputs in React?
Red flag Assuming attribute and property always stay in sync. For value/checked the attribute is just the initial default.
source: MDN — Attributes ↗ -
How would you efficiently insert 1,000 DOM nodes without causing 1,000 reflows?
Build the nodes off the live DOM and insert once, so layout is recomputed a single time.
Use a
DocumentFragment:const frag = document.createDocumentFragment();for (const item of items) { const li = document.createElement("li"); li.textContent = item; frag.appendChild(li); }list.appendChild(frag);Appending to the fragment does not touch the rendered tree; the single
appendChild(frag)inserts all children in one operation. AvoidinnerHTML +=in a loop (re-parses everything each time) and avoid appending one-by-one to the live list.Follow-ups they push on- Why is innerHTML += in a loop both slow and unsafe?
- When would you use virtualization instead of inserting all 1,000 nodes?
Red flag Appending each node directly to the live DOM in the loop, or using innerHTML += which re-parses the whole list every iteration.
source: MDN — DocumentFragment ↗ -
What is a stacking context, and why might a higher z-index element still appear behind a lower one?
z-indexonly orders elements within the same stacking context. A stacking context is a self-contained layer: once formed, its children are painted as a unit, and theirz-indexvalues cannot escape it.So if element A (z-index: 9999) lives inside a parent that forms a stacking context with a low z-index, and element B (z-index: 1) is in a *sibling* context with a higher one, B paints on top — A's huge z-index is meaningless across contexts.
New stacking contexts are created by more than
position + z-index:opacity < 1,transform,filter,will-change,isolation: isolate, and being a flex/grid child with a z-index, among others. This is the usual cause of 'my z-index isn't working'.What a strong answer coversz-indexis only comparable within one stacking context, never across them.Once an ancestor forms a context, a child's z-index is trapped inside it.
Contexts are created by
position+z-indexbut alsoopacity < 1,transform,filter,will-change,isolation: isolate.A z-index:9999 child of a low context loses to a z-index:1 element in a higher-ranked sibling context.
Quick self-checkWhich of these does NOT, by itself, create a new stacking context?
-
Any opacity below 1 creates a stacking context.
-
A non-none transform creates a stacking context.
-
Correct — static positioning alone never forms a stacking context (and z-index is ignored on it).
-
This property exists precisely to create a stacking context.
Follow-ups they push on- Name three properties besides position that create a stacking context.
- How does `isolation: isolate` help contain z-index without side effects?
Red flag Assuming z-index is globally comparable. A larger z-index loses if its element sits in a lower-ranked ancestor stacking context.
source: MDN — Stacking context ↗ -
What does flex: 1 actually mean? Break down flex-grow, flex-shrink, and flex-basis.
flexis shorthand for three properties:- flex-grow — how much a item grows to fill leftover free space, relative to siblings.
- flex-shrink — how much it shrinks when there isn't enough space.
- flex-basis — the item's starting size before grow/shrink (its 'ideal' main size).flex: 1expands toflex: 1 1 0%— grow 1, shrink 1, basis 0%. Because basis is 0, items size purely by their grow ratio, so equalflex: 1items become equal width regardless of content. Contrastflex: auto(1 1 auto), where content size is the starting point, so items differ by content length.What a strong answer coversflex: <grow> <shrink> <basis>;flex: 1=1 1 0%.flex-grow distributes free space; flex-shrink distributes overflow; flex-basis is the pre-grow size.
flex: 1on siblings gives equal sizes (basis 0);flex: auto(basis auto) sizes from content first.flex-basistakes priority overwidthfor the main-axis starting size.
Follow-ups they push on- What's the difference between flex: 1 and flex: auto?
- When does flex-shrink: 0 matter (preventing an item from collapsing)?
Red flag Thinking flex: 1 sets a width directly, or confusing flex: 1 (basis 0, equal sizes) with flex: auto (basis auto, content-driven sizes).
source: MDN — flex ↗ -
How would you center a div both horizontally and vertically? Give more than one approach.
Flexbox (most common): on the parent,
display: flex; align-items: center; justify-content: center;Grid (terse): on the parent,
display: grid; place-items: center;Absolute + transform (no flex/grid): on the child,
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);The transform trick offsets by the element's own size (the 50% in translate is relative to the element), so it centers regardless of dimensions. Flexbox/grid are preferred for in-flow content; absolute centering suits overlays where the child is taken out of flow.
What a strong answer coversFlexbox:
align-items: center; justify-content: center;on the container.Grid:
place-items: center;— the shortest form.Absolute +
translate(-50%, -50%)centers without knowing the element's size.Prefer flex/grid for in-flow content; absolute centering for overlays/modals.
Follow-ups they push on- Why does translate(-50%, -50%) work without knowing the element's dimensions?
- What changes if the parent has a fixed height vs auto height?
Red flag Using `margin: auto` for vertical centering on a block (works horizontally, not vertically without flex), or forgetting the parent needs a height for flex centering to be visible.
source: MDN — Box alignment (centering) ↗ -
Why and when do vertical margins collapse between block elements?
Margin collapsing is when adjacent vertical margins combine into a single margin equal to the largest of them, rather than summing. It applies only to block-level boxes in normal flow along the block (vertical) axis — never horizontal margins.
Three cases: adjacent siblings (the bottom margin of one and top margin of the next collapse); a parent and its first/last child (if no border/padding/content separates them); and an empty block (its own top and bottom margins collapse).
It does not happen for flex/grid items, floated or absolutely-positioned elements, or when a border, padding, or
overflow: autoseparates the boxes. This trips people up when a child's margin unexpectedly pushes the parent.What a strong answer coversCollapsing takes the max of the two margins, not the sum — vertical only.
Happens between siblings, parent/first-or-last child, and within empty blocks.
Prevented by a border, padding,
overflowother than visible, or a BFC.Does not apply to flex/grid items, floats, or absolutely positioned boxes.
Follow-ups they push on- How does establishing a block formatting context (BFC) stop collapsing?
- Why does a child's top margin sometimes push the parent down?
Red flag Expecting margins to add up, or thinking collapsing applies to flex/grid items (it doesn't) or to horizontal margins (it doesn't).
source: MDN — Mastering margin collapsing ↗ -
What is the difference between rem, em, %, vw/vh, and px? When would you reach for each?
px is an absolute (device-independent) pixel — fixed, predictable, but ignores user font preferences.
em is relative to the current element's font-size (for most properties), so it compounds when nested. rem is relative to the root
<html>font-size — no compounding, which makes it the go-to for scalable, accessible typography and spacing.% is relative to the parent's corresponding dimension. vw/vh are 1% of the viewport's width/height, useful for full-screen sections.
Practical default:
remfor type and spacing (respects user zoom/root size),%/fr/vwfor fluid layout,pxfor hairline borders.What a strong answer coverspx: absolute and fixed; doesn't scale with user font settings.em: relative to the element's own font-size — compounds when nested.rem: relative to the root font-size — no compounding; best for accessible, scalable type.%is relative to the parent;vw/vhare 1% of viewport width/height.
Follow-ups they push on- Why can nested em values produce surprising sizes?
- Why is rem preferred over px for font sizes from an accessibility standpoint?
Red flag Confusing em (element-relative, compounds) with rem (root-relative), or using px for font sizes and breaking user zoom/font-size preferences.
source: MDN — CSS values and units (length) ↗ -
What is the difference between event.target and event.currentTarget on a bubbling event?
event.targetis the element where the event originated — the deepest node that was actually clicked/typed-in.event.currentTargetis the element whose listener is currently running — i.e. the element you calledaddEventListeneron.During bubbling,
targetstays constant as the event travels up, whilecurrentTargetchanges at each ancestor whose listener fires. In a delegated handler on a<ul>,currentTargetis the<ul>, andtargetis the specific<li>(or a child of it, which is why you often usetarget.closest('li')).Note: in an arrow function
thiswon't be the element, butevent.currentTargetalways is.What a strong answer coverstarget= where the event started (deepest element); constant through bubbling.currentTarget= the element whose listener is running now; changes per ancestor.In delegation,
currentTargetis the parent you bound to;targetis the actual descendant.currentTargetequalsthisin a normal function handler, but not in an arrow function.
Quick self-checkA click listener is on a <ul>. The user clicks a <span> inside an <li>. Inside the handler, what are target and currentTarget?
-
Backwards — currentTarget is the listening element (the <ul>).
-
Correct — target is the clicked <span>; currentTarget is the <ul> the listener is bound to.
-
target is the deepest clicked node (the <span>), not the <li>.
-
currentTarget is the bound element (<ul>), not the clicked node.
Follow-ups they push on- Why use target.closest('li') instead of target directly in a delegated handler?
- What is event.target inside a handler bound directly to the element itself?
Red flag Swapping the two: target is the origin, currentTarget is the listening element. In delegation, acting on target directly can grab a nested child instead of the row.
source: MDN — Event: currentTarget ↗ -
What is the difference between stopPropagation and preventDefault?
They are orthogonal.
preventDefault()cancels the browser's default action for the event — following a link, submitting a form, checking a checkbox — but the event still propagates to other listeners.stopPropagation()stops the event from traveling further through the capture/bubble phases to other elements, but does not cancel the default action. (stopImmediatePropagation()additionally prevents other listeners on the *same* element.)So: prevent the default behavior with
preventDefault; stop the event reaching parents withstopPropagation. Returningfalsefrom a jQuery handler does both, but in plain DOMreturn falseonly works in inlineon*attributes.What a strong answer coverspreventDefault()cancels the default browser action; propagation still happens.stopPropagation()halts travel to other elements; default action still happens.stopImmediatePropagation()also blocks other listeners on the same element.They're independent — you sometimes call both, sometimes one.
Quick self-checkA form submit handler calls only event.stopPropagation(). What happens?
-
Only propagation stops — the default submit still fires.
-
Correct — stopPropagation doesn't cancel the default submit; you'd need preventDefault for that.
-
stopPropagation does stop parents from receiving it.
-
The default submit action still occurs.
Follow-ups they push on- When would you call both on the same event?
- Why is calling stopPropagation broadly considered risky for delegation?
Red flag Believing stopPropagation also cancels the default action (it doesn't), or that preventDefault stops bubbling (it doesn't).
source: MDN — Event: preventDefault() ↗ -
What is a block formatting context (BFC), and name two ways to create one. Why is it useful?
A block formatting context is a self-contained region of layout where block boxes lay out and floats are managed independently of the outside. Inside a BFC, vertical margins don't collapse with elements outside it, and the BFC contains its floated children.
Ways to create one:
overflowother thanvisible(e.g.overflow: hidden/auto),display: flow-root(the purpose-built, side-effect-free option), being a flex/grid item,display: inline-block, or floating/absolute positioning.Classic uses: clearing floats (a floated child no longer overflows its parent's height), and stopping margin collapse between a parent and child.
display: flow-rootis the modern, intention-revealing way to do both.What a strong answer coversA BFC is an isolated layout region; floats and margins inside don't leak out.
Create with
display: flow-root,overflow≠ visible, flex/grid item, float, or absolute.Contains floats (no parent collapse) and blocks external margin collapsing.
display: flow-rootis the clean, side-effect-free way to establish one.
Follow-ups they push on- Why was `overflow: hidden` historically used to clear floats?
- What advantage does display: flow-root have over the overflow hack?
Red flag Using `overflow: hidden` to clear floats and accidentally clipping content or scrollbars; `display: flow-root` avoids those side effects.
source: MDN — Block formatting context ↗