Loading article...
Fetching the blog content...
Loading article...
Fetching the blog content...
Accessibility, state management, component design, media optimization, rendering strategies, fonts, testing, and deployment — the intermediate tier where most mid-level interviews actually live. Part 2 of the Frontend Roadmap series.
The mental models and edge cases that live at the intermediate tier — where most mid-level interviews actually live. Same approach as Part 1: not surface definitions, but the understanding that separates someone who has used these features from someone who actually gets them.
Accessibility is not a checklist you bolt on at the end. It is mostly a byproduct of using the right elements and managing focus, with ARIA as a patch for the gaps native HTML cannot cover.
Every interactive element must be reachable and operable by keyboard. Native elements (<button>, <a>, <input>) get this for free. A <div onclick> does not — which is the single biggest reason to use real elements.
order, grid placement) can desync the visual order from the tab order, which is disorienting. Keep the DOM in logical order.tabindex semantics: 0 puts an element in the natural tab order, -1 makes it focusable only programmatically (not by tab). Any positive value hijacks the order and is an anti-pattern. Use 0 and -1 only.ARIA is a set of role, state, and property attributes that describe semantics to assistive tech when native HTML cannot. The governing principle: do not use ARIA if a native element does the job. A <button> beats <div role="button" tabindex="0"> plus keyboard handlers every time.
The other rules: do not change native semantics (<button role="heading"> is wrong), all interactive ARIA controls must be keyboard usable, and do not put aria-hidden="true" on a focusable element.
The attributes that come up most:
aria-label and aria-labelledby give an accessible name when there is no visible label (an icon-only button).aria-describedby adds supplementary description (error text, hints).aria-expanded, aria-selected, aria-checked communicate state for custom widgets.aria-live regions announce dynamic changes (polite waits for a pause, assertive interrupts). This is how you announce "3 results loaded" to a screen reader.Bad ARIA is worse than no ARIA — it actively lies to assistive tech. Native first, ARIA only to fill genuine gaps.
A screen reader narrates the page and lets the user navigate non-visually. It does not read your DOM directly — it reads the accessibility tree, a parallel structure the browser derives from the DOM, combining semantics, ARIA, and computed properties.
Users navigate by structure: jump between headings, landmarks (<nav>, <main>), links, and form controls. This is why heading hierarchy and landmarks are navigation infrastructure, not decoration.
The accessible name of each element is computed from a priority order (aria-labelledby, then aria-label, then label or content). If a button announces as "button" with no name, that computation found nothing.
You are authoring two interfaces at once: the visual one and the one in the accessibility tree. Semantic HTML keeps them in sync automatically.
Contrast is the measurable ratio between text and background luminance. WCAG sets thresholds:
Two rules beyond the numbers: never rely on color alone to convey information (pair a red-only error state with an icon or text), and remember placeholder text and disabled states still need to be perceivable.
Focus management is what separates a technically accessible component from a usable one.
The situations that require manual management:
Use :focus-visible rather than :focus for focus rings — it shows for keyboard users but not on mouse click. Never remove the outline without replacing it.
The most important modern distinction is server state versus client state.
For client state, the patterns scale with scope:
Derive, do not duplicate. If a value can be computed from existing state, compute it rather than storing a second copy that can drift out of sync.
Predictability comes from a few constraints that the Flux/Redux lineage formalized:
(state, action) => newState with no side effects — same inputs, same output, testable and predictable.State machines (XState and the general concept) take this further by making invalid states unrepresentable. Instead of three booleans (isLoading, isError, isSuccess) that can contradict each other, you model explicit states with explicit transitions. This is the answer to "how do you prevent impossible UI states."
Composition means building complex UI by combining small, focused components. The principle: composition over configuration — instead of a single component with thirty props, expose smaller pieces that callers assemble.
The mechanisms (React vocabulary, general ideas):
children is the simplest composition: a component wraps whatever you put inside it without knowing what it is.<Tabs>, <Tab>, <TabPanel>), giving a clean declarative API while the coordination is hidden.The payoff is flexibility without prop explosion. The trap is over-decomposition: splitting things so finely that following the logic requires opening ten files.
Props are a component's inputs. The contract is data down, events up — a parent passes data through props, the child communicates back by calling callbacks the parent passed in. The child never reaches up to mutate the parent.
A pure component renders the same output for the same props and state, with no side effects during render. Purity is what makes rendering safe to optimize.
React.memo, useMemo, and useCallback skip work by assuming same inputs produce same output.useEffect) or event handlers, never in the render body.Do not memoize everything — memoization has its own cost (comparison, memory), and most components are cheap to re-render. Reach for it when you have measured a real problem.
Each unit should have one reason to change. The modern version of the container/presentational split is custom hooks for logic, components for markup. A useUserData hook owns the fetching and state, and the component consumes it and renders. Presentational components stay pure and reusable; logic is testable in isolation.
The honest caveat: do not dogmatically separate everything. Co-locating closely related logic and markup is often more readable than spreading it across files for the sake of a pattern.
Images and media are usually the heaviest thing on a page. This is where real-world performance is won or lost.
<picture> and multiple <source> elements.Two distinct problems, two tools:
srcset and sizes on <img>. srcset lists candidate files with widths, sizes tells the browser how wide the image renders at various breakpoints, and the browser picks the smallest sharp file. Getting sizes right matters — a wrong value makes the browser pick a poorly sized image.<picture> with <source media="...">.Always set explicit width and height (or aspect-ratio) on images to reserve space and prevent layout shift.
loading="lazy" on <img> and <iframe> — zero JS, browser-managed.Do not lazy-load above-the-fold images, especially the LCP image. Lazy-loading your hero image delays the very metric you are trying to optimize. Eager-load (and use fetchpriority="high") for the hero image, lazy-load below the fold.
The major engines: Blink (Chrome, Edge, most Chromium browsers), WebKit (Safari, and all iOS browsers), Gecko (Firefox).
The pipeline every performance topic refers back to:
transform and opacity can be handled entirely by the GPU on an already-painted layer, skipping layout and paint.Animate only transform and opacity for smooth 60fps animations.
Layout thrashing forces the browser to compute layout repeatedly within a single frame by interleaving reads and writes to the DOM.
Certain reads (offsetHeight, getBoundingClientRect, scrollTop, getComputedStyle) need an up-to-date layout, so if you read after a write, the browser does a forced synchronous layout right then:
JSfile.js1// Thrashing — each read forces layout 2for (const el of elements) { 3 const height = el.offsetHeight; // read forces layout 4 el.style.height = height * 2 + "px"; // write invalidates layout 5}
The fix is to batch all reads, then all writes:
JSfile.js1// Read phase 2const heights = elements.map((el) => el.offsetHeight); 3// Write phase 4elements.forEach((el, i) => { 5 el.style.height = heights[i] * 2 + "px"; 6});
Now layout happens once, not once per element.
These describe where and when your HTML is generated:
There is no single best strategy. Pick per route based on whether content is static or dynamic, how important first paint and SEO are, and how much interactivity the page needs.
Web fonts are a common, avoidable source of slow rendering and layout shift.
font-display controls what the browser shows while a web font downloads:
swap: show fallback text immediately, swap to the web font when it loads. Causes FOUT (Flash Of Unstyled Text). Common default.block: hide text briefly waiting for the font. Causes FOIT (Flash Of Invisible Text). Usually worse for users.optional: use the web font only if it loads almost instantly (from cache), otherwise stick with the fallback. Best for performance and avoiding layout shift, at the cost of the web font sometimes not appearing on first visit.Beyond font-display: preload critical fonts (<link rel="preload" as="font" crossorigin>) to fetch them early. Self-host rather than third-party CDN for control. Use WOFF2 (best compression). Subset fonts to only the characters you use. Variable fonts pack many weights into one file.
When text renders in a fallback font and swaps to a web font with different metrics (character widths, line height), the text reflows and everything below jumps, hurting CLS (Cumulative Layout Shift).
The modern fix is metric overrides on @font-face: size-adjust, ascent-override, descent-override, and line-gap-override tune the fallback font to occupy the same space as the web font, so the swap causes no reflow. Some frameworks (Next.js font optimization) do this automatically.
The point of a test suite is confidence per unit of cost. Different test types buy different confidence at different costs.
The testing trophy (Kent Dodds) argues integration tests give the best confidence-to-cost ratio for front-end work: a small base of static checks and unit tests, a large middle of integration tests, a thin top of E2E. The more current view for front-end.
Static hosting serves pre-built files (HTML, CSS, JS, assets) directly, with no application server per request. Cheap, scales effortlessly (just files behind a CDN), the natural home for SPAs and statically generated sites. Platforms: Vercel, Netlify, Cloudflare Pages, S3 plus CloudFront. Anything dynamic comes from APIs, serverless functions, or edge functions layered on top.
Push latency-sensitive, lightweight logic to the edge. Keep heavy or stateful work (database transactions, large computations) at the origin.
app.4f8a2c.js) get max-age=31536000, immutable — a year — because content never changes without the filename changing.no-cache so users pick up new asset references promptly.Design so that invalidation is rare and cheap. Rename what you can, cache it forever, and keep only a thin always-fresh layer (the HTML) on top.
Goal: Master the intermediate-tier concepts — accessibility, state management, component design, media, rendering strategies, fonts, testing, and deployment — that distinguish strong mid-level frontend engineers.
Continue learning with these related challenges
The mental models and edge cases that separate someone who has used HTML, CSS, JS, forms, responsive design, and HTTP from someone who actually understands them. Part 1 of the Frontend Roadmap series.
Build systems, security, offline-first patterns, internationalization, maintainable CSS, performance internals, and design systems — the advanced tier where senior-level depth is tested. Part 3 of the Frontend Roadmap series.
Core Web Vitals (LCP, INP, CLS), loading strategies (code splitting, tree shaking, resource hints, image optimization), runtime performance (layout thrashing, compositor animations, web workers, virtualization), and a structured debugging framework.
The mental models and edge cases that separate someone who has used HTML, CSS, JS, forms, responsive design, and HTTP from someone who actually understands them. Part 1 of the Frontend Roadmap series.
Build systems, security, offline-first patterns, internationalization, maintainable CSS, performance internals, and design systems — the advanced tier where senior-level depth is tested. Part 3 of the Frontend Roadmap series.
Core Web Vitals (LCP, INP, CLS), loading strategies (code splitting, tree shaking, resource hints, image optimization), runtime performance (layout thrashing, compositor animations, web workers, virtualization), and a structured debugging framework.