` in the HTML simply won't execute — the browser blocks it and logs a CSP violation.\n\nCSP is a defense-in-depth layer, not a replacement for proper input sanitization/escaping — you still need to prevent the injection in the first place (e.g. never using `dangerouslySetInnerHTML` with unsanitized user content), but CSP gives you a second line of defense that limits the blast radius if an XSS bug slips through."}},{"@type":"Question","name":"What's the difference between XSS and CSRF, and how do you defend against each?","acceptedAnswer":{"@type":"Answer","text":"XSS (Cross-Site Scripting) is about an attacker getting their JavaScript to run in *your* page, in the victim's browser session — typically by injecting a script through unsanitized user input that later gets rendered as HTML (a comment field, a search query reflected back, etc.). Once that script runs, it has full access to the page's DOM, cookies (unless `HttpOnly`), and can make authenticated requests as the victim. Defense: escape/sanitize all user-generated content before rendering it as HTML (React does this by default for text content; the danger is specifically `dangerouslySetInnerHTML` or raw `innerHTML`), plus CSP as a second layer.\n\nCSRF (Cross-Site Request Forgery) doesn't need to inject any code at all — it tricks the victim's browser into sending a legitimate, authenticated request to *your* site from a *different*, malicious site, relying on the browser automatically attaching cookies to same-site requests. E.g. a malicious page auto-submits a hidden form to `bank.com/transfer` — if the victim is logged into `bank.com`, their browser sends the request with valid session cookies attached, and the bank has no way to tell it wasn't intentional.\n\n```\n// Defense: CSRF tokens (server issues a token, checks it on state-changing requests)\n// and SameSite cookies, which stop the browser from attaching them to cross-site requests at all\nSet-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly\n```\n\nThe short version: XSS is about executing attacker code in your origin; CSRF is about forging requests that ride on the victim's existing authenticated session without any code execution needed."}},{"@type":"Question","name":"Explain the JavaScript event loop, and the difference between microtasks and macrotasks.","acceptedAnswer":{"@type":"Answer","text":"JavaScript runs on a single thread with a call stack, but asynchronous work (timers, I/O, promises) is handled by the browser/runtime outside that stack, coordinated via the event loop. When the call stack is empty, the event loop pulls the next task from a queue and runs it. There are two queues that matter here: the microtask queue (Promise callbacks, `queueMicrotask`, `async/await` continuations) and the macrotask queue (`setTimeout`, `setInterval`, I/O callbacks, UI events).\n\nThe key rule: after each single macrotask finishes, the event loop drains the ENTIRE microtask queue completely — including any new microtasks added while draining — before running even one more macrotask or repainting the UI. This is why promises always \"jump ahead\" of `setTimeout(fn, 0)`, no matter how the code is ordered:\n\n```js\nconsole.log('1');\nsetTimeout(() => console.log('2'), 0); // macrotask — queued for later\nPromise.resolve().then(() => console.log('3')); // microtask — runs before the next macrotask\nconsole.log('4');\n// Output: 1, 4, 3, 2\n```\n\nThis distinction explains real bugs: an infinite chain of `.then()` callbacks that keep scheduling more microtasks can starve the event loop entirely, blocking `setTimeout` callbacks and even UI rendering from ever getting a turn, since the browser won't move to the next macrotask (or repaint) until the microtask queue is fully empty."}},{"@type":"Question","name":"What's the difference between `Object.freeze()` and `const`?","acceptedAnswer":{"@type":"Answer","text":"`const` only prevents *reassigning the variable binding* — it says nothing about the value's internal mutability. `Object.freeze()` operates on the value itself, making an object's existing top-level properties non-writable, non-configurable, and preventing new properties from being added — but it's shallow, so nested objects inside a frozen object are still fully mutable.\n\n```js\nconst obj = Object.freeze({ name: 'Alice', address: { city: 'NYC' } });\nobj.name = 'Bob'; // silently fails (throws in strict mode)\nobj.address.city = 'LA'; // WORKS — freeze is shallow, nested object isn't frozen\nconsole.log(obj.address.city); // 'LA'\n\nconst arr = ['a', 'b'];\narr.push('c'); // fine — const only blocks reassigning `arr`, not mutating it\n```\n\nSo `const arr = []` and mutating it with `.push()` is completely legal, which surprises people expecting `const` to mean \"immutable.\" For real deep immutability you'd need to recursively freeze every nested object yourself, or reach for a library/pattern (Immer, `structuredClone` + freeze, or just consistently creating new objects instead of mutating) — `Object.freeze` alone at one level rarely gives you the guarantee people assume it does."}},{"@type":"Question","name":"What are the basics of web accessibility (a11y) — ARIA roles, semantic HTML, and keyboard navigation?","acceptedAnswer":{"@type":"Answer","text":"The first rule of accessibility is to prefer semantic HTML over generic `
`s with JS-attached behavior, because semantic elements come with built-in accessibility behavior for free: `\n```\n\nKeyboard navigation matters because many users (motor impairments, screen-reader users, power users) never touch a mouse — every interactive element needs to be reachable via Tab, operable via Enter/Space/Arrow keys as appropriate, and have a visible focus indicator; `outline: none` without a replacement focus style is one of the most common and damaging accessibility regressions."}},{"@type":"Question","name":"What is the accessibility tree, and how do screen readers actually use it?","acceptedAnswer":{"@type":"Answer","text":"Alongside the DOM tree and the render tree, browsers build a third tree specifically for assistive technology: the accessibility tree. It's a parallel, pruned/annotated representation of the page — each node carries a computed role, name, state, and value (e.g. \"button, 'Submit', not pressed\") derived from the element's tag, attributes, and ARIA overrides. Purely decorative elements (an empty `
` used for layout spacing) are typically excluded entirely.\n\nScreen readers (VoiceOver, NVDA, JAWS) don't read the DOM or the rendered pixels at all — they query the OS's accessibility API, which the browser populates from this accessibility tree, and narrate based on that. This is precisely why `aria-hidden=\"true\"` doesn't affect visual rendering but removes an element (and its descendants) from the accessibility tree entirely — useful for hiding purely decorative icons from screen readers — while `visibility: hidden`/`display: none` remove elements from both the visual render AND the accessibility tree.\n\n```jsx\n🎉 {/* decorative emoji, visually shown, but skipped by screen readers */}\n {/* icon hidden, label announced instead */}\n```\n\nA practical debugging tool most people don't know about: Chrome DevTools' \"Accessibility\" pane (inside Elements) lets you inspect the computed accessibility tree node for any element directly, which is the actual source of truth for what a screen reader will announce — not the visible text or the raw HTML."}},{"@type":"Question","name":"What's the difference between `display: none`, `visibility: hidden`, and `opacity: 0`?","acceptedAnswer":{"@type":"Answer","text":"All three make an element invisible, but they differ in whether the element still occupies space and whether it's still interactive/accessible. `display: none` removes the element from the layout entirely — it takes up no space, as if it weren't in the DOM at all (though it still is), and it's removed from the accessibility tree too. `visibility: hidden` keeps the element's space reserved in the layout but hides it visually — it's not clickable and, like `display: none`, is excluded from the accessibility tree. `opacity: 0` also keeps its layout space AND remains fully interactive (clickable, focusable, tabbable) and present in the accessibility tree — it's just visually transparent.\n\n```css\n.a { display: none; } /* gone from layout, gone from a11y tree, unclickable */\n.b { visibility: hidden; } /* space reserved, unclickable, gone from a11y tree */\n.c { opacity: 0; } /* space reserved, STILL clickable and tabbable, still in a11y tree */\n```\n\nThis matters practically for animations and hidden-but-interactive elements: a fade-in/fade-out transition needs `opacity` (you can't transition `display`), but leaving something at `opacity: 0` when it's meant to be fully gone leaves an invisible, still-clickable, still-tab-reachable element sitting on the page — a subtle bug that shows up as \"why can I tab into an invisible button\" or a layered click-through issue."}},{"@type":"Question","name":"What is a Service Worker and how does it enable offline support in a PWA?","acceptedAnswer":{"@type":"Answer","text":"A Service Worker is a JavaScript file that runs in a separate thread from your page, independent of any single tab, and acts as a programmable network proxy — it can intercept every fetch request the page makes and decide how to respond: pass it through to the network, serve a cached response instead, or generate a synthetic response. It has no DOM access and communicates with pages via `postMessage`, and critically, it keeps running (dormant, woken on events) even after the tab that registered it is closed.\n\nOffline support works by combining a Service Worker with the Cache Storage API: on install, the worker pre-caches the app shell (HTML/CSS/JS); on every subsequent `fetch` event, it intercepts the request and can serve the cached version if the network is unavailable.\n\n```js\n// sw.js\nself.addEventListener('install', (event) => {\n event.waitUntil(caches.open('v1').then((cache) => cache.addAll(['/', '/app.js', '/app.css'])));\n});\n\nself.addEventListener('fetch', (event) => {\n event.respondWith(\n caches.match(event.request).then((cached) => cached || fetch(event.request))\n );\n});\n```\n\nThis \"cache-first\" strategy is what lets a PWA still load and show a usable UI on a flaky or absent connection, instead of the browser's default offline error page — it's also the mechanism behind push notifications and background sync, both of which need a worker that survives beyond a single open tab."}},{"@type":"Question","name":"How would you implement infinite scroll efficiently using IntersectionObserver?","acceptedAnswer":{"@type":"Answer","text":"The naive approach — listening to `scroll` and checking `scrollTop`/`scrollHeight` math on every event — fires dozens of times per second during a scroll and forces layout reads constantly, which is exactly the kind of main-thread work you want to avoid. `IntersectionObserver` instead lets the browser tell you, asynchronously and efficiently, when a specific element (a \"sentinel\" placed at the bottom of the list) enters the viewport, with no manual scroll-position math and no layout thrashing.\n\n```jsx\nfunction useInfiniteScroll(loadMore) {\n const sentinelRef = useRef(null);\n\n useEffect(() => {\n const observer = new IntersectionObserver(\n ([entry]) => { if (entry.isIntersecting) loadMore(); },\n { rootMargin: '200px' } // trigger 200px before the sentinel actually enters view, for a smoother feel\n );\n if (sentinelRef.current) observer.observe(sentinelRef.current);\n return () => observer.disconnect();\n }, [loadMore]);\n\n return sentinelRef;\n}\n\n// usage: render
as the last element of the list\n```\n\nTwo details matter for real correctness: guard `loadMore` against being called again while a fetch for the previous page is still in flight (or you'll fire duplicate requests as the sentinel stays intersecting during the load), and for truly huge lists, pair this with virtualization (rendering only visible rows) since infinite scroll alone still keeps growing the DOM node count forever."}},{"@type":"Question","name":"When should you use `Array.map`, `forEach`, and `reduce`?","acceptedAnswer":{"@type":"Answer","text":"`forEach` runs a callback for each element purely for side effects — it always returns `undefined` and can't be chained; use it when you just want to do something per item (log it, push to an external array, mutate the DOM) without producing a new array. `map` transforms each element and returns a NEW array of the same length — use it whenever you need a derived array where each output corresponds 1:1 to an input. `reduce` folds the entire array down into a single accumulated value of any shape (a number, an object, even a different-length array) — use it when the output isn't naturally \"one item per input item.\"\n\n```js\nconst nums = [1, 2, 3, 4];\n\nnums.forEach((n) => console.log(n)); // side effect only, no return value\nconst doubled = nums.map((n) => n * 2); // [2, 4, 6, 8] — same length, transformed\nconst sum = nums.reduce((acc, n) => acc + n, 0); // 10 — collapsed to one value\nconst grouped = nums.reduce((acc, n) => {\n const key = n % 2 === 0 ? 'even' : 'odd';\n (acc[key] ??= []).push(n);\n return acc;\n}, {}); // { odd: [1, 3], even: [2, 4] } — reshaped into an object\n```\n\nA common code-review nitpick: using `map` purely for side effects and throwing away the returned array (`items.map(x => console.log(x))`) — that should be `forEach` instead, since creating and discarding an unused array wastes memory and misleads the reader about intent."}},{"@type":"Question","name":"What causes a race condition from stale closures in a React `useEffect`, and how do you fix it?","acceptedAnswer":{"@type":"Answer","text":"A stale closure happens when an async callback inside `useEffect` captures a variable from the render it was created in, but by the time the async work finishes, the component has re-rendered (or unmounted) and that captured variable is now outdated — yet the callback keeps using the old value because closures capture variables by reference to that specific render's scope, not a \"live\" binding to current state.\n\nThe classic race condition: a search box fires a new fetch on every keystroke, but network responses can resolve out of order — if the request for \"re\" happens to resolve AFTER the request for \"react\" (slower network, server load, whatever), the stale \"re\" response overwrites the correct \"react\" results on screen, even though \"react\" was typed more recently.\n\n```jsx\nuseEffect(() => {\n let cancelled = false;\n fetchResults(query).then((data) => {\n if (!cancelled) setResults(data); // guard: ignore this response if a newer effect run has started\n });\n return () => { cancelled = true; }; // cleanup runs before the NEXT effect (new query) or on unmount\n}, [query]);\n```\n\nThe fix pattern is always some form of \"ignore this result if it's stale\" — a `cancelled` flag set in the cleanup function (as above), or an `AbortController` aborted in cleanup so the actual network request is cancelled rather than just its result ignored. Both rely on React guaranteeing the cleanup function from the previous effect run always fires before the next run (or unmount), which is what gives you a reliable place to mark old work as stale."}},{"@type":"Question","name":"What are CSS container queries, and how do they differ from media queries?","acceptedAnswer":{"@type":"Answer","text":"A media query responds to the *viewport's* size — it has no idea what size the component itself is actually rendered at, which breaks down for reusable components that get placed in different-width containers (a sidebar vs. a full-width main area). A container query instead responds to the size of a specific *ancestor container* you designate, letting a component adapt to its own actual available space regardless of the viewport size.\n\n```css\n.card-container {\n container-type: inline-size; /* opt this element in as a query container */\n container-name: card;\n}\n\n@container card (min-width: 400px) {\n .card { flex-direction: row; } /* switches layout based on the CONTAINER's width, not the viewport's */\n}\n```\n\nThis solves a real, longstanding limitation for component libraries and design systems: the same `` component can now genuinely be \"responsive to itself\" — stacking vertically when placed in a narrow sidebar and going horizontal when placed in a wide main content area, on the exact same screen size, something media queries fundamentally couldn't express since they only ever see the viewport."}},{"@type":"Question","name":"Why are `transform` and `opacity` considered cheap to animate compared to properties like `width` or `top`?","acceptedAnswer":{"@type":"Answer","text":"The browser's rendering pipeline has several stages, each more expensive than the last: layout (compute size/position) → paint (rasterize pixels) → composite (combine layers on the GPU). Changing a property like `width`, `top`, or `margin` affects the geometry of the element, forcing the browser to redo layout for that element and potentially its neighbors, then repaint, then recomposite — all three stages, on every single animation frame, which is expensive enough to drop frames on complex pages.\n\n`transform` and `opacity`, by contrast, can be handled entirely in the composite stage. A `transform: translateX()` or `scale()` doesn't change the element's actual layout box at all — the browser can promote the element to its own compositor layer (often GPU-accelerated) and just move/scale/fade that already-painted layer, skipping layout and paint entirely.\n\n```css\n/* Expensive: triggers layout + paint + composite every frame */\n.slide-bad { transition: left 300ms; }\n.slide-bad.open { left: 0; }\n\n/* Cheap: composite-only, can run on the GPU, skips layout and paint */\n.slide-good { transition: transform 300ms; transform: translateX(-100%); }\n.slide-good.open { transform: translateX(0); }\n```\n\nThis is the concrete reasoning behind the widely-repeated advice \"animate transform/opacity, not layout properties\" — it's not a stylistic preference, it maps directly onto which stages of the rendering pipeline the browser has to re-run per frame, and it's exactly what DevTools' Performance panel visualizes as separate Layout/Paint/Composite bands on the timeline."}}]}
50+ Real Questions

Frontend Interview Questions & Answers

Commonly-asked and hard-to-find Frontend interview questions, each with a clear, example-based answer.

Practice These Live with AI

Showing 1–10 of 50

Ready to practice these live?

Start a Free Frontend Mock Interview