Types (email, url, tel, number, range, date, datetime-local, color, file, search, password…) bring the right mobile keyboard and built-in checks; tel has no format check.
Validation attributes: required, minlength/maxlength, min/max/step, pattern (must match the whole value). novalidate on the form or formnovalidate on a button skips them.
JS: checkValidity(), reportValidity(), el.validity.valueMissing (typeMismatch, patternMismatch, tooShort…), setCustomValidity('msg') (invalid until reset to '').
:invalid matches on page load; :user-invalid only after the user interacts.
Only named fields submit; unchecked checkboxes and disabled fields are skipped, readonly ones are sent.
A <button> in a form defaults to type="submit". Group radios with <fieldset> + <legend>. A placeholder is not a label.
Accessibility
Rules of ARIA use (W3C): prefer native HTML; don’t change native semantics unless you must; interactive ARIA controls must work by keyboard; never put role="presentation" or aria-hidden="true" on a focusable element. Every interactive element needs an accessible name.
ARIA changes what assistive tech is told, never behavior: a div role="button" still needs tabindex="0", Enter and Space handlers, and a focus style.
Common roles: landmarks, button, checkbox, switch, tab/tabpanel, combobox, listbox/option, menu, dialog; live regions status (polite) and alert (assertive).
Name precedence: aria-labelledby > aria-label > native label (<label>, alt, <legend>) > text content > title/placeholder.
Keyboard: Tab between widgets; arrow keys inside composite widgets (tabs, menus, listboxes) via roving tabindex or aria-activedescendant; Enter activates links and buttons, Space buttons and checkboxes; Esc closes dialogs and menus.
Focus: tabindex="0" joins the tab order, -1 is focusable by script only, positive values scramble order. Style :focus-visible. Move focus into a dialog on open and back to the trigger on close; showModal() makes the page behind inert and closes on Esc.
Alt text: the image’s purpose, not its pixels. Decorative: alt="" (with no alt, the file name may be read out). Icon-only link or button: describe the action (“Search”).
Contrast (WCAG AA): 4.5:1 for text, 3:1 for large text (24px, or about 18.7px bold) and for UI components and focus indicators.
Don’t rely on color alone; honor prefers-reduced-motion; announce async updates with a live region.
srcset with w descriptors + sizes = resolution switching: the browser picks by slot width × pixel ratio (no sizes means 100vw). x descriptors suit fixed-size images.
<picture> = art direction (<source media>) or format fallback (<source type>); the first match wins, and the required <img> carries alt and sizing.
Set width and height (or aspect-ratio) to reserve space and avoid layout shift.
loading="lazy" below the fold only; the LCP image gets fetchpriority="high" instead.
CSS is render-blocking; synchronous scripts are parser-blocking. The preload scanner finds resources early, but not those referenced in CSS or injected by JS.
Reflow (layout): size, margin, font or DOM changes, resize; it can ripple to other elements. Repaint only: color, background, box-shadow, visibility. Composite only: transform, opacity on their own layer.
Reading layout (offsetHeight, getBoundingClientRect(), getComputedStyle) after a write forces a synchronous layout; in a loop that’s layout thrashing.
// Bad: each read after a write forces a fresh layoutitems.forEach((el) => { el.style.width = box.offsetWidth + 'px'; });// Good: read once, then write (or batch writes in requestAnimationFrame)const w = box.offsetWidth;items.forEach((el) => { el.style.width = w + 'px'; });
JavaScript
Cascade & specificity
Order of comparison: origin & importance → inline style → @layer → specificity → order of appearance (the cascade, explained).
Unlayered styles beat layered ones; among layers the last declared wins. !important flips this: the first layer wins, and layered !important beats unlayered.
Specificity = (IDs, classes/attributes/pseudo-classes, types/pseudo-elements), compared left to right. * and combinators add nothing.
:where() adds 0; :is(), :not() and :has() take their most specific argument.
Inherited values have no specificity: any rule targeting the element beats them. Text properties inherit (color, font-*, custom properties); box properties don’t.
Box = content + padding + border + margin. content-box (default): width is content only; border-box: width includes padding and border. Reset: *, *::before, *::after { box-sizing: border-box; }.
outline and box-shadow take no space. Percentage padding and margin, even vertical, resolve against the containing block’s width.
Margin collapsing is vertical and block-layout only: adjacent siblings, a parent with its first/last child (no border, padding or content between), empty blocks. Result: the largest margin (with negatives, largest positive + most negative).
No collapsing for floats, absolute boxes, flex or grid children, or across a BFC.
A block formatting context contains its floats, avoids outside floats and stops margin collapsing. Made by display: flow-root (cleanest), overflow other than visible/clip, floats, absolute positioning, inline-block, flex/grid items, contain: layout/paint.
Display, position & stacking
display
Behavior
block
new line, fills available width
inline
flows with text; ignores width, height, vertical margins
inline-block
in the line, but honors width, height, margins
flex / grid
container for flex or grid children (inline-* variants)
flow-root
block with a new BFC
contents
own box disappears; children take its place
none
no box, gone from the accessibility tree
position
In flow?
Offsets relative to
static
yes
none: top and z-index ignored
relative
yes, space kept
its normal position; anchors absolute children
absolute
no
nearest positioned ancestor, else the initial containing block
fixed
no
viewport, unless an ancestor has transform, filter or perspective
sticky
yes
relative until a threshold (top: 0) in its nearest scroll container
z-index applies to positioned elements and flex/grid items, and only orders boxes within one stacking context: a child can’t rise above its parent’s context.
Stacking contexts come from: the root; positioned + z-index not auto; fixed/sticky; flex/grid items with z-index; opacity < 1; transform, filter, clip-path, mask; isolation: isolate; will-change on those; contain: paint; the top layer.
place-items (items in cells), place-content (tracks in container)
subgrid to reuse the parent’s tracks
fr shares out space left over after fixed tracks. 1fr is minmax(auto, 1fr), so long content widens a column; minmax(0, 1fr) keeps columns equal.
auto-fill keeps empty tracks; auto-fit collapses them so items stretch. They differ only when items don’t fill a row.
Centering: display: grid; place-items: center · flex with justify-content + align-items: center · margin-inline: auto on a block with a width · absolute + inset: 0; margin: auto + a size · absolute + top: 50%; left: 50%; translate: -50% -50% · text-align: center for inline content.
Units & responsive design
Unit
Relative to
Use for
px
the CSS pixel (not a device pixel)
borders, shadows
em
element’s font size (parent’s, inside font-size)
spacing that scales with text; compounds
rem
root font size (usually 16px)
type and spacing; respects user settings
%
the containing block (varies by property)
fluid widths
vw / vh
1% of viewport width / height
full-bleed sections
svh / lvh / dvh
small / large / dynamic viewport
mobile full height: 100dvh
ch
advance width of “0”
line length: max-width: 65ch
On mobile vh equals lvh (toolbars retracted), so 100vh can hide content under browser UI.
Mobile-first: base styles for small screens, then @media (min-width: 48em) adds more. Break where the content breaks.
Container queries: container-type: inline-size on a wrapper, then @container (min-width: 30rem) {…} styles descendants by that container’s width. An element can’t query itself.
Custom properties (--brand: #0a7, var(--brand, fallback)) inherit, cascade and can change per subtree, in media queries or from JS (style.setProperty); Sass variables compile away. @property registers typed, animatable ones.
clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX)): font-size: clamp(1rem, 0.8rem + 1vw, 1.5rem); keep a rem term so zoom works. width: min(100% - 2rem, 70ch).
Motion & hiding elements
Transition
Animation
Starts
on a property change (:hover, class)
as soon as applied
Steps
start → end
any @keyframes
Loop, reverse, pause
no
yes
Best for
UI state changes
spinners, entrances
Animate transform and opacity (no layout or paint); width, top or margin reflow every frame. Use will-change sparingly and avoid transition: all.
Technique
Keeps space
Focusable / clickable
Screen readers
display: none
no
no
hidden
visibility: hidden
yes
no
hidden (children can be visible)
opacity: 0
yes
yes
still read
hidden attribute
no (display: none, loses to CSS display)
no
hidden
aria-hidden="true"
yes, still visible
yes
hidden
.sr-only (1px, clipped)
no
yes
read
inert
yes, still visible
no
hidden
Quick answers
async vs defer? Both fetch in parallel; defer runs in order after parsing, async whenever it arrives.
em vs rem?em follows the element’s font size and compounds; rem follows the root.
display: none vs visibility: hidden? Both hide from everyone; only visibility keeps the space.
Why doesn’t z-index work? The element isn’t positioned or a flex/grid item, or an ancestor’s stacking context traps it.
Flexbox or grid? Flex for a line of self-sizing items; grid for aligned rows and columns.
Center a div?display: grid; place-items: center on the parent.
What does border-box do?width includes padding and border, so padding doesn’t grow the box.