Web platform
What the browser does with a page regardless of the framework: how it lays out boxes, how to make the UI accessible, how it turns HTML into pixels and how to make that faster. Angular and React run on top of this layer, so fullstack interviews check it separately.
Before this topic: JavaScript and TypeScript.
How does CSS layout work: the box model, flexbox and grid?
Section titled “How does CSS layout work: the box model, flexbox and grid?”What it is
- Box model. An element is content, padding, border and margin. By default
widthdoes not include padding and border. display.blocktakes the whole line,inlineflows within text,flexandgridlay out the element’s children.- Flexbox places children in a row or a column, grid — in a grid of rows and columns.
How to use
- For all elements —
box-sizing: border-box. - Page skeleton — grid, items inside a block — flex, spacing —
gap. - Default styles are for phones, wider screens get
@media (min-width: 768px). Themd:prefix in Tailwind works the same way.
Common mistake
Laying out blocks with float and position: absolute where flex or grid would do. That layout breaks on the first long piece of text.
Takeaway: the box model sets an element’s size, flex lays out along one axis, grid along two.
How it works
- Cascade. When several rules match an element, the one higher on this list wins:
!important, inline style, a rule outside@layeror in a later layer, higher specificity, later position in the source. - Specificity is a triple: ids; classes, attributes and pseudo-classes; type selectors and pseudo-elements. It is compared left to right, so
#nav a(1,0,1) beats.menu .item a(0,2,1). - Positioning.
relativeshifts an element and keeps its space.absolutetakes it out of flow and positions it relative to the nearest positioned ancestor.fixed— relative to the viewport,stickysticks while scrolling.
When to choose what
| Flexbox | Grid | |
|---|---|---|
| Axes | one: row or column | two: rows and columns |
| Size comes from | content | container |
| Example | menu, row of buttons, card | page skeleton, gallery, form |
Pitfalls
- Margin collapsing. Vertical margins of adjacent blocks in normal flow do not add up; the larger one wins. Flex and grid do not do this.
z-index“doesn’t work”. It is compared within one stacking context, and one is created by, for example,opacitybelow 1 ortransformon an ancestor.- A flex item won’t shrink. A flex item’s default minimum width is its content width; a long word stretches the column, and
min-width: 0fixes it.
Rule of thumb
Grid for the skeleton, flex for components, gap instead of margins between siblings, media queries mobile-first.
Takeaway: you choose the layout by the number of axes, and style conflicts are settled by the cascade, not by shuffling file order.
Under the hood
- The flex algorithm first takes each item’s base size (
flex-basis, otherwise the content), then hands out free space byflex-growor takes it away byflex-shrink. That is why columns with different text end up with different widths. - Grid first builds tracks from
grid-template-columns, andfrsplits the remaining space.repeat(auto-fit, minmax(12rem, 1fr))gives a responsive gallery without media queries. container-type: inline-sizeturns on size containment: the container’s width does not depend on its content. Otherwise there would be a loop: style depends on size, size depends on style.
Trade-offs
- Media query or container query. A media query suits the page skeleton. A card that appears both in the sidebar and in the main column should respond to its own container. Container queries are Baseline since 2023, and Tailwind v4 has them built in (
@container,@md:). - Units.
remin font sizes respects the user’s font size setting,pxdoes not. Afont-sizeinvwalone does not grow when the page is zoomed, soclamp()gets aremterm. On mobile100vhincludes the area under the browser toolbar;100dvhis more accurate. - Controlling the cascade.
@layer reset, library, appfixes the layer order, and unlayered styles beat any layer.:where()zeroes the specificity of base styles so they are easy to override. Utility CSS such as Tailwind and CSS modules end the war because all classes weigh the same.
How to check in production
- DevTools → Elements → Computed shows which rule won, and struck-through ones lost. The
flexandgridbadges in the element tree toggle a layout overlay. - Playwright screenshot tests at several widths catch broken layouts before release.
Takeaway: grid sizes from the top down, flex from the bottom up; a component adapts through its container, and style priorities come from layers, not !important.
*, *::before, *::after { box-sizing: border-box; }
/* Skeleton: mobile-first — one column */.page { display: grid; grid-template-columns: 1fr; gap: 1rem;}@media (min-width: 768px) { .page { grid-template-columns: 16rem 1fr; } /* sidebar + content */}
/* Gallery without media queries: as many columns as fit */.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 1rem;}
/* The card adapts to its container, not to the viewport */.card-slot { container-type: inline-size; }.card { display: flex; flex-direction: column; gap: 0.5rem; }@container (min-width: 30rem) { .card { flex-direction: row; } /* image on the left, text on the right */}
/* From 24px to 40px with a 16px root font */h1 { font-size: clamp(1.5rem, 1rem + 2vw, 2.5rem); }- What is the difference between
display: noneandvisibility: hidden? — The first removes the element from layout, the second hides it but keeps its space. - What does
flex: 1mean? —flex-grow: 1,flex-shrink: 1,flex-basis: 0%: items share free space equally. - How does
auto-fitdiffer fromauto-fillin grid? —auto-fitcollapses empty tracks and stretches the items,auto-fillkeeps the empty tracks. - What is
position: absolutepositioned against? — The nearest ancestor whosepositionis notstatic, otherwise the initial containing block. - What is the specificity of
:is()and:where()? —:is()takes its most specific argument,:where()is always zero.
How do you make a UI accessible?
Section titled “How do you make a UI accessible?”What it is
Accessibility means people with visual, hearing and motor impairments can use the interface: with a screen reader, with the keyboard only, with enlarged text.
How to use
- An action is a
<button>, navigation is an<a href>, not adivwith a click handler: they are focusable and work with the keyboard. - Every field has a
<label for="...">. A placeholder is not a label: it disappears as you type. - An image has an
altthat conveys its meaning; a decorative one gets an emptyalt="". - Headings
h1–h6go in order, regions are marked up withheader,nav,main. - Normal text contrast against the background is at least 4.5:1.
Common mistake
Removing the focus ring with outline: none because it “looks ugly”. Keyboard users lose track of where they are. Style it with :focus-visible instead of removing it.
Takeaway: the right HTML element gives you accessibility almost for free.
How it works
- From the DOM and CSS the browser builds the accessibility tree: role (
button), accessible name (“Save”) and state (disabled,expanded). - A screen reader — NVDA, JAWS, VoiceOver, TalkBack — gets this tree through the OS accessibility API and reads it out.
- ARIA changes only the tree, not behaviour:
role="button"on adivgives it neither focus nor a response to Enter and Space.
When to choose what
| Task | Solution |
|---|---|
| Field without a visible label | aria-label or aria-labelledby |
| Field error | aria-invalid="true" and aria-describedby pointing to the error text |
| Message after an action | live region: role="status" or role="alert" |
| Modal | <dialog> and showModal(): background is inert, Esc closes it |
| Widget state | aria-expanded, aria-selected, aria-current |
Pitfalls
- ARIA on top of native. First rule of ARIA: if there is an HTML element with the semantics and behaviour you need, use it.
tabindexabove zero breaks the natural tab order. Only0and-1are used.- Route changes in an SPA. Focus and announcements do not change by themselves; you move focus to the new page’s heading manually.
- Colour only. An error shown only by a red border is invisible to a colour-blind user and silent for a screen reader — add text.
Rule of thumb
Semantic HTML first, then ARIA for what is missing; walk every flow with the keyboard: Tab, Enter, Space, Esc, arrows.
Takeaway: accessibility is a correct accessibility tree plus keyboard operation, and ARIA only fills in what HTML lacks.
Standards and law
- WCAG 2.2 (W3C, October 2023) — success criteria at three levels: A is the minimum, AA is the target of laws and contracts, AAA is for special cases.
- New in 2.2 at level AA: focus not hidden by sticky bars (2.4.11), target size at least 24×24 CSS pixels (2.5.8), an alternative to dragging (2.5.7), login without a cognitive test — for example, pasting a password from a manager (3.3.8). Criterion 4.1.1 Parsing was removed.
- The EAA (Directive 2019/882) applies from 28 June 2025 to products and services on the EU market: online shops, banking, transport, e-books. Microenterprises providing services are exempt. The technical basis is the EN 301 549 standard, which references WCAG 2.1 AA.
Trade-offs
- Custom widget or ready-made. A combobox, tabs or a tree in ARIA means roles, states and a keyboard model from the ARIA Authoring Practices. It is usually cheaper to take a proven library or the Angular CDK
a11ypackage than to write your own. - Automation or manual testing. axe-core and Lighthouse find low contrast, empty labels and wrong roles. They cannot tell whether the focus order makes sense or whether an
altis meaningful.
How to check in production
- axe-core in e2e tests (
@axe-core/playwright) and Lighthouse CI in the pipeline catch regressions. - Manually: walk the main flow with the keyboard only and with a screen reader — NVDA on Windows, VoiceOver on macOS.
- DevTools → Elements → Accessibility shows a node’s role, name and state and the full accessibility tree.
Takeaway: the target is WCAG 2.2 AA, built into design-system components and automated tests, while manual testing covers what automation cannot see.
<form novalidate> <!-- for/id ties the label to the field: clicking the label focuses the field --> <label for="email">Email</label> <input id="email" type="email" autocomplete="email" aria-invalid="true" aria-describedby="email-error"> <!-- The screen reader reads the error text together with the field --> <p id="email-error">Enter a valid email</p>
<!-- Icon button: accessible name via aria-label, the icon is hidden --> <button type="button" aria-label="Close"> <svg aria-hidden="true" width="16" height="16"><path d="M2 2l12 12M14 2L2 14"/></svg> </button>
<img src="chart.png" alt="Sales grew 20% in Q3"> <img src="divider.png" alt=""> <!-- decorative: the screen reader skips it -->
<!-- Live region: text inserted here gets announced --> <div role="status"></div>
<button type="submit">Save</button></form>/* Focus ring only for keyboard navigation, not for mouse clicks */button:focus-visible,a:focus-visible { outline: 3px solid #1a73e8; outline-offset: 2px;}- What is the first rule of ARIA? — If a native HTML element gives you the semantics and behaviour you need, use it instead of a role on a
div. - How does
aria-labeldiffer fromaria-labelledby? — The first sets the name as a string, the second points to the id of visible text. - How does
role="alert"differ fromrole="status"? —alertis announced immediately, interrupting speech;statuspolitely, after the current phrase. - What contrast does WCAG AA require? — 4.5:1 for normal text, 3:1 for large text and for UI components.
- What did the European Accessibility Act change? — Since 28 June 2025 accessibility is mandatory for many digital services in the EU, with EN 301 549 and WCAG as the reference.
How does the browser turn HTML into pixels?
Section titled “How does the browser turn HTML into pixels?”What it is
The path from HTML to pixels:
- DOM. HTML is parsed into a tree of nodes.
- CSSOM. CSS is parsed into a tree of rules.
- Render tree. Only visible elements with their computed styles:
display: noneis left out. - Layout. Sizes and coordinates of every box are computed.
- Paint. Pixels are filled in: text, colours, shadows, images.
- Composite. Layers are assembled into the final frame.
How to use
- CSS goes in the
<head>, scripts get thedeferattribute. - Animate
transformandopacity, nottop,leftorwidth.
Common mistake
Adding a large script to the <head> without defer. While it downloads and runs, HTML is not parsed and the screen stays blank.
Takeaway: DOM and CSSOM → render tree → layout → paint → composite; anything that delays these steps delays the first frame.
How it works
- CSS blocks rendering. Without the CSSOM the browser does not paint, otherwise the user would see an unstyled page. A synchronous script also waits for the stylesheets before it, because it may read styles.
- JS blocks parsing. A plain
<script>stops the parser until it is downloaded and executed. Meanwhile the preload scanner looks ahead in the HTML and starts fetching images and stylesheets early. - JS and rendering share the main thread. A frame is rendered between tasks, so a long handler delays it. How the event loop works is covered in JavaScript and TypeScript.
When to choose what
<script> attribute |
HTML parsing | Execution |
|---|---|---|
| none | stops | immediately, in order |
async |
continues | as soon as loaded, no order |
defer |
continues | after parsing, in order, before DOMContentLoaded |
type="module" |
continues | like defer by default |
What a change costs
| Change | Stages |
|---|---|
width, top, font-size, new nodes |
layout, paint, composite (reflow) |
color, background, box-shadow |
paint, composite (repaint) |
transform, opacity |
composite only |
Pitfalls
- Layout thrashing. A style write followed right away by a read of
offsetHeightorgetBoundingClientRect()forces layout to be recomputed immediately; in a loop — on every iteration. asyncfor dependent scripts. Order is not guaranteed: a library may load after the code that uses it.
Rule of thumb
Scripts — defer or modules, layout reads — batched before writes, animations — on transform and opacity.
Takeaway: CSS blocks display, synchronous JS blocks parsing, and after load the cost of a change depends on which pipeline stage it restarts from.
Under the hood
- In Chromium the paint stage only records drawing commands. Rasterisation and frame assembly are done by the compositor thread and the GPU, not the main thread.
- A
transformandopacityanimation on its own layer runs entirely in the compositor, so it stays smooth even when the main thread is busy. Layers are created by, for example,will-change: transform,<video>and<canvas>. - The frame budget at 60 Hz is about 16.7 ms for everything: JS, styles, layout and paint.
Cost
- A forced synchronous layout in a loop over N elements means N layout passes instead of one.
- Every layer is a texture in GPU memory. Hundreds of layers on a low-end phone are worse than an extra repaint.
- A large DOM makes every style recalculation and layout more expensive.
Trade-offs
- Separate DOM reads from writes: first all measurements, then all changes, with visual changes in
requestAnimationFrame. content-visibility: autoskips rendering of off-screen blocks, but they needcontain-intrinsic-size, otherwise the scrollbar jumps.- Critical CSS inlined into the
<head>speeds up the first frame, but it is not cached separately and makes every HTML response bigger.
How to check in production
- DevTools → Performance: purple Layout and Recalculate Style blocks inside long tasks, and the Forced reflow insight points to the line of code.
- Rendering tab → Paint flashing highlights repainted areas, and the Layers panel shows the layers.
- In the field — the Long Animation Frames API (Chromium): each script entry has
forcedStyleAndLayoutDuration.
Takeaway: there is one main thread for JS and rendering: don’t force it into synchronous layout, and hand animations to the compositor.
<head> <!-- CSS blocks rendering: keep it small and in the head --> <link rel="stylesheet" href="/app.css"> <!-- media="print": does not block rendering on screen --> <link rel="stylesheet" href="/print.css" media="print">
<script src="/legacy.js"></script> <!-- stops HTML parsing --> <script defer src="/app.js"></script> <!-- after parsing, in order --> <script async src="/analytics.js"></script> <!-- as soon as loaded, no order --> <script type="module" src="/main.js"></script> <!-- deferred, like defer --></head>const items = document.querySelectorAll('.item');
// Bad: a read after a write on every iteration — layout is recomputed N timesfor (const item of items) { item.style.width = item.parentElement.offsetWidth / 2 + 'px';}
// Good: all reads first, then all writes — one recalculationconst widths = [...items].map((item) => item.parentElement.offsetWidth);requestAnimationFrame(() => { items.forEach((item, i) => { item.style.width = widths[i] / 2 + 'px'; });});/* Compositor-only animation: no layout and no paint */.toast { transition: transform 200ms, opacity 200ms; }.toast.hidden { transform: translateY(100%); opacity: 0; }- What is the difference between reflow and repaint? — Reflow recomputes geometry and drags paint along, repaint only redraws pixels.
- Why do
display: noneandvisibility: hiddenaffect layout differently? — The first removes the element from the render tree, the second keeps its space. - When does
DOMContentLoadedfire, and whenload? —DOMContentLoaded— after HTML parsing anddeferscripts,load— after all images and stylesheets. - Does
<link rel="stylesheet" media="print">block rendering? — No, it is not needed on screen; the file is still downloaded at low priority. - What does the preload scanner do? — While the parser is stuck on a script, it looks ahead in the HTML and starts fetching resources early.
How do you measure and speed up a web app’s loading?
Section titled “How do you measure and speed up a web app’s loading?”What it is
| Metric | What it measures | Good |
|---|---|---|
| LCP | when the largest element of the first screen is rendered | up to 2.5 s |
| INP | delay from a click or key press to the next frame | up to 200 ms |
| CLS | the sum of unexpected layout shifts | up to 0.1 |
How to use
- Measure: Lighthouse in DevTools or PageSpeed Insights.
- Give images
widthandheight: the browser reserves space in advance and the layout doesn’t jump. - Images below the first screen get
loading="lazy". - Load heavy pages when the user navigates to them: lazy routes in Angular,
React.lazyand dynamicimport().
Common mistake
Putting loading="lazy" on the main image of the first screen. It starts loading later and LCP gets worse.
Takeaway: three metrics — loading speed, responsiveness and stability; you improve them by sending less, and sending what the first screen needs sooner.
How it works
| Metric | Good | Poor |
|---|---|---|
| LCP | up to 2.5 s | over 4 s |
| INP | up to 200 ms | over 500 ms |
| CLS | up to 0.1 | over 0.25 |
The assessment uses the 75th percentile of visits, separately for mobile and desktop. INP replaced FID in March 2024: FID measured only the delay of the first input, INP counts all clicks, taps and key presses.
Lab and field
| Lab data | Field data | |
|---|---|---|
| Source | Lighthouse, DevTools Performance | CrUX, the web-vitals library, RUM |
| Conditions | one device, a set network | real devices and networks |
| Purpose | debugging, regressions in CI | what users actually see |
Lighthouse does not measure INP during a page load: in the lab, Total Blocking Time stands in for it.
Pitfalls
- A green Lighthouse score on a powerful laptop while field data from phones is poor.
preloadfor everything: resources compete for bandwidth and the important ones load later.
Rule of thumb
LCP — the image in the HTML with fetchpriority="high", INP — less JS and shorter tasks, CLS — dimensions for everything that loads later. Find the problem in the field, the cause in the lab.
Takeaway: metrics are assessed in the field, debugged in the lab, and each one is fixed with its own lever.
Under the hood
- LCP = time to first byte + resource load delay + resource load duration + element render delay. Almost all of the time should be spent loading the HTML and the resource itself.
- INP = input delay + processing duration + presentation delay. The worst interaction of the visit is taken, and with many interactions one worst per 50 is discarded.
- HTTP/2 multiplexes requests over one connection, so bundling everything into one file is no longer necessary. HTTP/3 runs over QUIC and does not block other streams when a packet is lost.
Cost
- A kilobyte of JS costs more than a kilobyte of image: it has to be parsed, compiled and executed on the main thread.
- Fonts: WOFF2 and subsetting.
font-display: swapshows text right away but shifts it when the font swaps in. - Hashed static files are cached for a year (
Cache-Control: max-age=31536000, immutable), HTML getsno-cache. The server side is in Background jobs and caching, CDN in System design.
Trade-offs
- SSR and hydration. The server sends ready HTML, so LCP comes sooner. But the client still downloads the JS and attaches handlers, and until hydration finishes the page is visible but unresponsive. Details are in React and Next.js.
- Code splitting. The initial bundle is smaller, but navigating to a lazy route waits for the chunk. Prefetching when idle or on hover helps.
preconnectandpreload. The first opens a connection to another origin in advance, the second fetches a late-discovered resource. Extra hints steal bandwidth from what matters.
How to check in production
- RUM: the
web-vitalslibrary sends LCP, INP and CLS with attribution to your analytics; CrUX and Search Console show the 28-day result. - Budgets in CI:
budgetsinangular.json, Lighthouse CI with thresholds, bundle analysis withsource-map-explorer.
Takeaway: measure in the field, break the metric into parts and fix the longest one; the main long-term lever is less JavaScript on the client.
<head> <!-- Open the connection to the image CDN early: DNS, TCP, TLS --> <link rel="preconnect" href="https://img.example.com"> <!-- The font is needed right away, but the browser would find it late in CSS --> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <style> @font-face { font-family: Inter; src: url(/fonts/inter.woff2) format("woff2"); font-display: swap; /* text right away, in the fallback font */ } </style></head><body> <!-- LCP image: high priority, no lazy, dimensions against CLS --> <img src="https://img.example.com/hero-800.avif" srcset="https://img.example.com/hero-800.avif 800w, https://img.example.com/hero-1600.avif 1600w" sizes="100vw" width="1600" height="900" fetchpriority="high" alt="Autumn sale">
<!-- Below the first screen: loads as it approaches the viewport --> <img src="/promo.webp" width="600" height="400" loading="lazy" alt="Gift cards"></body>import { onLCP, onINP, onCLS } from 'web-vitals';
// RUM: every value goes to your analyticsfunction send(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating }); navigator.sendBeacon('/rum', body);}onLCP(send);onINP(send);onCLS(send);
// Code splitting: the charts module loads only on clickdocument.querySelector('#report').addEventListener('click', async () => { const { renderChart } = await import('./chart.js'); renderChart();});- Why did INP replace FID? — FID measured only the delay before processing the first input, INP the full delay to the next frame for all interactions.
- How does lab data differ from field data? — The lab is reproducible and good for debugging, the field shows real devices and networks.
- How does
preloaddiffer fromprefetch? —preloadis for the current page and loads right away;prefetchis for the next navigation, when idle. - How do you reduce CLS from web fonts? —
font-display: optionalor matching the fallback font’s metrics withsize-adjust. - Can you inline critical CSS under a strict CSP? — Yes, with a nonce or hash in
style-src; see Web security basics.