Frontend Developer Interview Questions · 2026

Frontend Developer Interview Questions: HTML, CSS, JS & React (2026)

JavaScript held onto its usual spot in 2025: 68.8 percent of professional developers used it for extensive development work in the past year, more than any other language, with HTML/CSS close behind (Stack Overflow, 2025). That volume is exactly why frontend developer interview questions have gotten harder to bluff through. Ten years ago a candidate could get by knowing jQuery and a handful of CSS tricks. In 2026, a mid-level frontend loop expects you to reason about the rendering pipeline, the event loop, and at least one modern framework, not just recite syntax.

Here's an opinion that might be wrong: most frontend prep still over-indexes on "what's the difference between let and var" and under-indexes on the layout bugs that only show up once a page has real content in it, an unexpected line wrap, a stacking context nobody planned for, a flex item that won't shrink no matter what you try. The syntax questions are five-minute warm-ups. The questions that separate a junior from a senior are the ones where something looks broken and you have to explain why, out loud, under a little bit of time pressure.

This page covers 52 frontend developer interview questions organized by the four areas almost every loop touches: HTML and CSS, JavaScript and TypeScript, React and other frameworks, and the performance and accessibility questions that show up more in 2026 loops than they did five years ago. It's not evenly split. JavaScript gets the most space because it's where interviewers spend the most time. Performance gets the least, not because it doesn't matter, but because most loops only budget one or two questions for it.

52Questions
HTML/CSS to ReactTopics
Live in-browser codingFormat
Junior to SeniorLevel

HTML and CSS fundamentals

These get asked at every level, even senior loops, mostly as a way to check whether you can explain something you use every day without groping for words.

Easy questions

14

Semantic elements tell the browser, assistive tech, and search crawlers what a piece of content actually is. A <nav> announces itself to a screen reader as a navigation landmark, so a user can jump straight to it. A <div> only describes how to draw a box, it says nothing about what's inside it.

Interviewers usually ask this to see whether you think about accessibility by default or bolt it on as an afterthought right before ship.

By default the browser uses content-box: width and height apply only to the content, and padding plus border get added on top, so a 200px box with 20px of padding renders at 240px wide. border-box folds padding and border into that 200px instead, so what you set is what you get.

Most CSS resets flip every element to border-box for exactly this reason. Once you're nesting boxes with padding, content-box math gets impossible to predict by eye.

Flexbox is one-dimensional, a single row or a single column, and it's built for distributing space and alignment along that one axis. Grid is two-dimensional, rows and columns at once, and it's built for laying out a whole page rather than a single component.

In practice most real UIs use both at once: Grid for the page shell, Flexbox for the toolbar and card internals sitting inside it.

css
/* Flexbox */
.parent { display: flex; justify-content: center; align-items: center; }

/* Grid */
.parent { display: grid; place-items: center; }

/* Absolute positioning */
.child {
 position: absolute;
 top: 50%; left: 50%;
 transform: translate(-50%, -50%);
}

I'd reach for the Flexbox version almost every time, it's the one every other frontend developer on the team will recognize instantly. place-items: center on a grid container is one line shorter and does the identical job. Absolute positioning with a transform is the oldest trick and still shows up in codebases where a flex or grid container isn't an option.

A pseudo-class (:hover, :focus, :nth-child()) targets an element in a particular state or position. It's still styling a real element that exists in the DOM. A pseudo-element (::before, ::after, ::first-line) creates something that isn't in the DOM at all, a generated box the browser inserts for you.

Browsers still accept single-colon syntax for pseudo-elements, for backward compatibility, but double-colon is the correct modern syntax and worth using consistently.

var is function-scoped and hoisted with its value initialized to undefined, so it's usable, as undefined, before the line it's declared on. let and const are block-scoped and sit in a "temporal dead zone" from the top of the block until their declaration line, referencing them earlier throws instead of silently returning undefined. const additionally can't be reassigned, though an object or array it points to can still be mutated.

The var-in-a-loop bug is the classic demonstration: a var declared inside a for loop is shared across every iteration, so callbacks scheduled from inside that loop all see the final value once the loop finishes, not the value from their own iteration.

== performs type coercion before comparing: it converts one operand so the types match, then compares, so the string '5' gets coerced to the number 5 and the comparison succeeds. === skips coercion entirely and compares both type and value, so a string is never equal to a number under strict equality no matter what it contains.

Almost every style guide bans == outright for exactly this reason. The coercion rules have enough edge cases, [] == false is true, that it's not worth the mental overhead of remembering them.

javascript
const { name: userName, role = 'guest' } = user;

This pulls name off user but binds it locally as userName, and if role doesn't exist on user at all it falls back to 'guest' instead of being undefined.

It's mostly syntax, but candidates who've only ever used the plain const { name } = user form sometimes freeze the first time they see renaming and defaults combined in one destructure.

A file can have exactly one default export, imported under whatever name you choose, and any number of named exports, imported by their exact name. import() as a function, not a static statement, returns a promise and loads the module at runtime instead of build time, the mechanism behind route-based code splitting in most bundlers.

It's the reason a Next.js or Vite app can ship a small initial bundle and only fetch a page's code once the user actually navigates there.

An effect with no dependency array runs after every render, roughly componentDidMount plus componentDidUpdate combined. An empty dependency array runs the effect once, after the first render only, matching componentDidMount. The cleanup function returned from inside an effect runs before the next effect fires and once more on unmount, covering what componentWillUnmount used to handle.

It's not a perfect one-to-one mapping. Hooks group logic by concern instead of by lifecycle stage, which is most of the actual argument for preferring them.

useState triggers a re-render every time you update it. useRef doesn't, mutating .current is invisible to React's render cycle entirely.

Use state for anything the UI needs to reflect on screen. Use a ref for things you need to keep around without redrawing anything, a DOM node reference, a previous value for comparison, a timer ID you'll need to clear later.

Keys give React a stable identity for each item across renders. Without one, React falls back to matching items by position, so inserting a new item at the front of a list can make React think every existing item changed.

It won't crash, React just logs a console warning, but any local state or an uncontrolled input's value tied to a specific item can end up attached to the wrong item once the list reorders. Using the array index as a key is fine for a genuinely static list and a foot-gun for anything filterable or sortable.

Adding loading="lazy" to offscreen <img> tags defers their download until they're close to entering the viewport, at essentially zero implementation cost. Beyond that: serving modern formats (WebP or AVIF instead of JPEG/PNG), a srcset for responsive sizing so mobile doesn't download a desktop-sized image, and setting explicit width and height attributes so the browser reserves space before the image loads, which also directly reduces CLS.

That last one gets skipped constantly, and it's usually the actual cause of a layout-shift complaint that someone blames on "images" without being specific about why.

localStorage persists indefinitely, holds roughly 5 to 10MB, and never gets sent to the server automatically, JavaScript has to read and attach it manually. Cookies are tiny by comparison, about 4KB, but get sent with every matching request automatically, and support httpOnly, which blocks JavaScript from reading them at all.

That last part is the real answer to the question: an auth token that JavaScript should never be able to read, to limit what an XSS bug could steal, belongs in an httpOnly cookie, not localStorage, no matter how much simpler localStorage's API is to use.

Medium questions

28

display: none removes the element from layout entirely, no space reserved, and it's gone from the accessibility tree too. visibility: hidden keeps the space reserved but hides the element, and screen readers skip it. opacity: 0 is the odd one out: the element stays fully in the layout and in the accessibility tree, just invisible to the eye, which means it's still clickable and still focusable unless you handle that separately.

That last part catches people. An opacity: 0 element sitting on top of something else can silently eat clicks meant for whatever's underneath it.

Sticky positions an element like relative until it crosses a scroll threshold, then it behaves like fixed within its nearest scrolling ancestor, only for as long as that ancestor is in view.

It breaks the moment a parent has overflow: hidden or overflow: auto set anywhere above it in the tree. The sticky element quietly stops sticking, and nobody notices until QA reports it on one specific page.

css
#nav.menu li   /* 1 id, 1 class, 1 element -> 1-1-1 */
.menu li.active  /* 0 ids, 2 classes, 1 element -> 0-2-1 */

Specificity is calculated as (inline styles, IDs, classes/attributes/pseudo-classes, elements). #nav.menu li scores one ID, one class, one element: 1-1-1. .menu li.active scores two classes, one element: 0-2-1. Compared column by column from left to right, the ID column decides it. One beats zero, so #nav.menu li wins regardless of how many classes the other selector stacks on.

This is exactly why teams that lean on utility classes avoid IDs in CSS almost entirely. IDs make specificity fights nearly impossible to reason about once a stylesheet grows past a few hundred lines.

Custom properties (--primary-color) are live values the browser resolves at render time, so you can read and change them from JavaScript with element.style.setProperty(), and they respect the cascade, a value set inside a media query or overridden on a nested element cascades normally. Sass variables get compiled away before the browser ever sees them. Once the CSS ships, they're just static values baked in.

That's the whole trick behind runtime theme switching: flip one custom property on :root and every element referencing var(--primary-color) updates instantly, no rebuild required.

Media queries respond to the viewport, which works fine until a component gets reused in a sidebar half the width of the page and still tries to lay itself out for a full-width screen. Container queries fix that: a component responds to the size of its own containing element instead of the viewport, so the same card component looks right whether it's dropped into a three-column grid or a narrow sidebar.

Browser support cleared the last major holdouts back in 2023, so container queries are safe to reach for by default in 2026, past the point of being an experimental feature you mention and never actually use.

javascript
for (var i = 0; i < 3; i++) {
 setTimeout(() => console.log(i), 0);
}

A closure is a function that remembers the variables from the scope it was created in, even after that outer scope has finished running. Here, all three setTimeout callbacks close over the exact same var i, and by the time any of them actually runs, the loop has already finished and i is 3.

Swap var for let and it prints 0, 1, 2, because let creates a fresh binding of i for every iteration of the loop, so each closure captures its own separate copy instead of sharing one.

javascript
list.addEventListener('click', (e) => {
 if (e.target.matches('li')) {
  handleItemClick(e.target);
 }
});

Events bubble up the DOM from the element that was actually clicked to every ancestor above it, so a single listener on the parent catches clicks on any child, including children added after the listener was attached. Attaching a listener to every individual item means re-attaching one every time you add a new item dynamically, for strictly more memory and the same result.

e.target gives you the actual element that was clicked, which might be a <span> nested inside the <li>, so checking with .matches() or .closest() is usually safer than a plain equality check.

All three let you explicitly set what this refers to inside a function. call invokes the function immediately with arguments passed one by one. apply invokes it immediately too but takes arguments as a single array. bind doesn't invoke anything, it returns a new function with this permanently locked in, ready to be called later.

bind is the one that shows up most in real code now, usually for passing a class method as a callback without losing track of this once it's detached from the object it belongs to.

Function declarations hoist completely, the whole function body moves to the top of scope, so you can call one before the line it's written on. var declarations hoist too, but only the declaration, not the assignment, so the variable exists but holds undefined until execution reaches the assignment. let and const hoist to the top of the block in the sense that the engine knows they exist, but accessing them before their declaration line throws a ReferenceError instead of returning undefined. That gap is the temporal dead zone.

Function expressions and arrow functions assigned to a const or let don't hoist their body at all, only the variable binding does, a distinction people frequently get wrong out loud in interviews.

javascript
async function getUser(id) {
 try {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Request failed');
  return await res.json();
 } catch (err) {
  console.error('getUser failed:', err);
  throw err;
 }
}

await on a rejected promise throws, so a plain try/catch around it works exactly like it would around synchronous code that throws. The part people forget: fetch only rejects on a network failure, a 404 or 500 response still resolves successfully, so you have to check res.ok yourself before assuming the request actually worked.

Skipping that check is probably the single most common real bug in fetch-based code I've seen candidates write live.

Every object has an internal link to another object, its prototype, and property lookups that miss on the object itself walk up that chain until they find a match or hit null. Object.create(proto) builds an object whose prototype is set explicitly. class syntax is mostly sugar over the exact same mechanism.

Nothing gets copied. A method defined once on a prototype is shared by every object that inherits from it, which is the entire memory-saving point of doing it this way instead of attaching a fresh copy of every method to every instance.

Arrow functions don't have their own this, they inherit it lexically from whatever scope they were defined in. A regular function's this is determined by how it's called, not where it's defined, so once you detach a regular method and pass it as a callback, this inside it is whatever called it, undefined in strict mode, not the original object anymore.

Class fields defined as arrow functions sidestep this entirely, which is exactly why that pattern took over from .bind(this) in constructors once class fields shipped.

Interfaces can be declared twice and TypeScript merges them into one. A type alias throws a duplicate-identifier error if you try the same thing. Interfaces can also be extended with extends, while types combine through intersections (&). For plain object shapes the practical difference is small, but interfaces are the more common default for anything meant to be extended, like a public API surface a library exposes.

TypeScript sits at 48.8 percent adoption among professional developers now, sixth among all languages measured (Stack Overflow, 2025), so most frontend loops now assume you've used it rather than treat it as a bonus skill.

typescript
function first<T>(arr: T[]): T | undefined {
 return arr[0];
}

first([1, 2, 3]);  // number | undefined
first(['a', 'b']); // string | undefined

The <T> is a placeholder TypeScript fills in based on whatever gets passed at the call site, so the return type stays accurate for every call instead of being locked to one specific type or widened all the way to any.

The follow-up worth expecting: what happens if the array is empty. arr[0] is undefined at runtime in that case, and the T | undefined return type is what forces callers to actually handle that instead of assuming an element always exists.

It's a lightweight in-memory tree of plain objects mirroring the real DOM. When state changes, React builds a new version of that tree, diffs it against the previous one, and computes the smallest set of real DOM mutations needed rather than rebuilding the page from scratch.

Direct DOM writes are the expensive part, layout, paint, and reflow all cost real time, so the win isn't that the virtual DOM itself is fast. It's that diffing plain JavaScript objects is cheap compared to touching the real DOM repeatedly.

A controlled input's value lives in React state plus an onChange handler, so React is the single source of truth and every keystroke re-renders the component. An uncontrolled input keeps its value in the DOM itself, read later through a ref, and React never re-renders on every keystroke because it isn't watching the field.

On a form with dozens of fields, a fully controlled setup re-renders the whole form on every keystroke unless each field is its own component. That's the real argument for libraries like react-hook-form, which lean on refs and only re-render on submit or validation.

When two sibling components need to share the same piece of state, the fix is moving that state to their nearest common parent and passing it down as props to both. It stops making sense once the state has to pass through four or five layers of components that don't actually use it themselves, just to reach a component buried deep in the tree, that's prop drilling, and it's the actual problem Context was built to solve.

Composition, passing a component as a prop or as children, solves a chunk of the same problem without reaching for Context at all, and it's worth mentioning if you want to show you know there's more than one tool for this.

Context is a poor fit for state that changes often and is read by many consumers, because every consumer of a context re-renders whenever that context's value changes, there's no built-in way to subscribe to just a slice of it. A theme toggle that flips twice a session is a fine fit. A value that updates on every click or every keystroke is a worse one without extra work.

Splitting a fast-changing context from a rarely-changing one, or memoizing the value object itself, are the usual fixes once this actually shows up as a real performance problem instead of a theoretical one.

React.memo wraps a component and skips re-rendering it when its props are shallowly equal to the previous render's. It does nothing for state changes inside the component itself, and it does nothing if you're passing a new object, array, or function reference as a prop on every render, since an inline arrow function is never === to the one from the previous render.

Wrapping a component in memo without stabilizing the props feeding it with useMemo or useCallback is one of the most common "why isn't memo doing anything" bugs I've seen in real code reviews.

jsx
const Settings = React.lazy(() => import('./Settings'));

function App() {
 return (
  <Suspense fallback={<Spinner />}>
   <Settings />
  </Suspense>
 );
}

React.lazy wraps a dynamic import(), so the component's code isn't included in the main bundle at all, it's fetched only when that component actually needs to render. Suspense shows a fallback while that fetch is in flight.

This matters most for routes or heavy components a lot of users never visit in a given session, a settings page, an admin panel, a rarely-opened modal.

CSR ships a mostly empty page and lets JavaScript render everything in the browser, fast subsequent navigation, slower first paint, and real SEO challenges unless it's crawled with JS execution. SSR generates HTML per request on the server, better first paint and SEO, at the cost of server load and per-request latency. SSG pre-renders HTML once at build time, so a request just serves a static file, the fastest possible response with zero per-request server cost.

SSG is the right call when content doesn't change per visitor and doesn't change often, a blog post, a marketing page, a documentation page. The moment content depends on the logged-in user or needs to be fresh within seconds of an update, SSR, or client-side fetching on top of a static shell, takes over.

Vue wraps reactive data in a Proxy (Vue 3) that intercepts property reads and writes directly. When a tracked property changes, Vue knows exactly which parts of the DOM depend on it and updates only those, no virtual DOM diff of an entire component's output required for that update to happen. React instead re-runs the whole component function on a state change and diffs the resulting tree to find what actually changed.

Neither approach is strictly faster in every case. Vue's fine-grained tracking can do less work per update, but React's model is simpler to reason about since a component's output is always a plain function of its current props and state.

The browser parses HTML into a DOM tree and CSS into a CSSOM tree, combines the two into a render tree (only the parts that will actually be painted, nothing with display: none), calculates layout, position and size for every node, also called reflow, then paints pixels, then composites layers together if any exist.

Layout and paint are the expensive steps. Changing a property that only affects paint, like background-color, skips layout entirely. Changing one that affects geometry, like width, triggers layout, paint, and composite all over again, which is the whole reason transform and opacity are the properties you animate if you care about frame rate.

LCP (Largest Contentful Paint) measures loading, good at or under 2.5 seconds. CLS (Cumulative Layout Shift) measures visual stability, good at or under 0.1. INP (Interaction to Next Paint) measures responsiveness, good at or under 200 milliseconds, and it replaced FID (First Input Delay) as the official responsiveness metric in March 2024 because INP measures the full interaction, not just the delay before it starts (web.dev, Interaction to Next Paint).

A candidate who still says "FID" instead of "INP" isn't wrong about the concept, just a year or two behind on which metric Google actually measures now. Worth knowing which one you're being asked about before you answer.

Tree-shaking removes exports a bundle never actually imports, but only works reliably with ES modules, not require(), since the bundler needs to statically see what's used. Checking for duplicate dependencies, two versions of the same library pulled in by different packages, and swapping heavy libraries for lighter alternatives, moment.js for something like date-fns, are the other two levers that usually move the needle most.

Running a bundle analyzer before guessing is worth saying out loud in an interview. Most engineers assume they know what's bloating a bundle and are wrong about at least one entry when they actually look.

WCAG groups guidelines under Perceivable (content must be presentable in ways users can perceive, alt text on images), Operable (interface elements must be usable, everything reachable and operable by keyboard alone), Understandable (content and operation must be predictable, form errors identified in text, not color alone), and solid (content works across current and future assistive tech, valid semantic markup a screen reader can parse) (W3C, WCAG 2.1 Quick Reference).

Most frontend interviews don't expect you to recite WCAG success criteria numbers. They do expect you to notice, unprompted, when a design relies on color alone to show an error or a mouse to operate a dropdown.

The browser blocks the response from reaching your JavaScript because the server didn't send back an Access-Control-Allow-Origin header permitting your page's origin to read it. This is enforced entirely client-side, by the browser, as a protection for the user, the request usually still reaches the server and the server may still process it, the browser just refuses to hand the response back to your script.

The fix lives on the server, adding the right CORS headers, not in the frontend code making the request. A proxy that forwards the request from your own origin is the common workaround when you don't control the API's server.

A Service Worker runs on a separate thread, independent of any open tab, and it can intercept every network request the page makes, serving a cached response instead of hitting the network, even while the user is offline. It keeps running, subject to the browser terminating it to save memory, after the tab that registered it closes, which is what makes background sync and push notifications possible.

The trade-off is real complexity: a badly configured cache strategy can serve stale content indefinitely, since the Service Worker, not a normal cache header, decides what gets served.

Hard questions

10

z-index only has an effect within a stacking context, and plenty of common CSS properties create a new one without anyone intending it: position combined with a z-index, opacity under 1, transform, filter, will-change. If a parent somewhere up the tree creates its own stacking context, your z-index of 999 only ever gets compared against siblings inside that same context. It can never climb above an element that lives in a sibling stacking context, no matter how high the number goes.

I've seen a modal get stuck behind a header because a completely unrelated ancestor had transform: translateZ(0) on it from an old Safari repaint hack. Finding that bug took longer than writing the modal did.

css
form:has(input:invalid) {
 border-color: red;
}

:has() is a parent selector: it lets a rule match an element based on what's inside it. .card:has(img) styles only cards that contain an image. label:has(input:checked) styles a label based on the checked state of an input nested inside it, no JavaScript required.

It shipped across all major browser engines by the end of 2023, so it's fair game in interviews now, and it quietly replaces a decent chunk of JavaScript that used to exist purely to toggle a class based on a child element's state.

javascript
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

It logs 1, 4, 3, 2. Synchronous code runs first and completely, in order, so 1 and 4 fire before anything queued. Promise callbacks go on the microtask queue, which the engine fully drains before it looks at the next item on the macrotask queue, where setTimeout callbacks live, so 3 beats 2 even though the timeout was scheduled first.

This is the single most useful mental model for debugging real async bugs: trace what's synchronous, what's a microtask, and what's a macrotask, in that order, before touching the code.

javascript
function debounce(fn, delay) {
 let timer;
 return function (...args) {
  clearTimeout(timer);
  timer = setTimeout(() => fn.apply(this, args), delay);
 };
}

Every call clears whatever timer is already pending and starts a fresh one, so fn only actually fires once the calls stop coming in for a full delay window. A search-as-you-type box wrapped in this fires one network request per pause in typing, not one per keystroke.

Interviewers usually follow up by asking for throttle next, which is the easy trap: throttle guarantees a call fires at most once per window regardless of how often the event fires, it doesn't wait for silence the way debounce does.

typescript
type FetchState<T> =
 | { status: 'idle' }
 | { status: 'loading' }
 | { status: 'success'; data: T }
 | { status: 'error'; error: string };

With optional fields, nothing stops you from constructing an impossible state, { status: 'loading', data: someData } is perfectly valid syntax even though loading and having data at the same time shouldn't happen. A discriminated union makes each shape mutually exclusive, and once you check status in an if or switch, TypeScript narrows the type inside that branch automatically, so .data is only accessible where status is actually 'success'.

This is the TypeScript question I'd bet on tripping up more candidates than generics does. Generics get more attention in prep material because they demo well in a blog post. Narrowing feels like something you're supposed to just absorb on the job, and plenty of engineers with years of production TypeScript have never had to write one of these by hand.

Server-side rendering generates HTML on the server, then the client "hydrates" that HTML by attaching event listeners and reconciling it against what React would have rendered on its own. A mismatch happens when those two don't agree, most often because something rendered differently on the server than it does in the browser: Date.now(), Math.random(), checking window or localStorage directly during render, or a browser extension injecting markup before hydration runs.

The fix is usually pushing the environment-dependent value into a useEffect so it only ever renders after the client has mounted, accepting a one-render flash of the server-safe default first, rather than trying to make the server guess what the browser will do.

The real reason is selector-based subscription. Redux, through useSelector, and libraries like Zustand let a component subscribe to one specific slice of state and only re-render when that slice changes. Context can't do that natively, any change to a context's value re-renders every consumer of that context, whether or not the part that changed is the part a given consumer actually reads.

My take here could be wrong, but most teams in 2026 don't need Redux specifically. Zustand, or plain Context paired with useReducer, covers most of what Redux used to get reached for by default, and it's a smaller mental model to hand a new hire on day one.

Classic Angular change detection runs on zone.js, which patches async browser APIs, setTimeout, event listeners, promises, so Angular knows something might have changed and re-checks the entire component tree from the root down, by default, on nearly every async event in the app. Signals, introduced starting in Angular 16 and stabilized in later versions, are reactive primitives that track their own dependents directly, so a signal update can trigger change detection only for the components that actually read that signal, without a zone.js-triggered full tree walk.

This is a meaningful shift for interview purposes. An Angular candidate in 2026 who can only explain zone.js and has never touched signals is showing prep from a few years out of date. Most greenfield Angular work now leans on signals for exactly this reason.

ARIA attributes override how assistive tech interprets an element, they don't change anything about how it behaves visually or functionally. Slap role="button" on a div without also giving it tabindex="0" and a keydown handler for Enter and Space, and a screen reader announces "button" for something a keyboard user still can't actually activate, which is worse than not labeling it at all, because now it's actively lying about what it does.

The fix in almost every one of these cases is using the native element that already does the right thing by default, a real <button> instead of a styled div with a role bolted on. Native elements come with keyboard behavior, focus handling, and correct semantics for free. Recreating all of that by hand with ARIA is where most of the bugs come from.

Open Chrome DevTools' Performance tab, record a scroll, then look at the flame chart for long tasks, anything over 50ms blocks the main thread and can't respond to input. If layout or paint dominates the chart, check what's being changed on scroll, a scroll listener recalculating layout-triggering properties on every event is the classic cause. If scripting dominates instead, look for expensive work running inside the scroll handler itself.

The single fix that resolves this most often: throttle the scroll handler, and read layout-triggering values like getBoundingClientRect() once per animation frame instead of once per scroll event, using requestAnimationFrame to batch the work instead of running it dozens of times a second.

How to prepare for a frontend developer interview in 2026

Skip re-reading a flashcard deck of definitions. Open DevTools on a real site, break something small on purpose, delete a key prop, remove box-sizing: border-box from a layout, comment out a dependency array, and watch what actually happens. Predicting the failure before you cause it, and being right about why, builds the kind of intuition a flashcard never will.

Across mock interviews run through LastRoundAI tagged frontend, the CSS specificity and stacking-context questions trip up more candidates than the React hooks questions do, even though React gets far more attention in most prep guides. There's no clean percentage to put on that, only that it's a pattern that keeps showing up in review. My guess is that people study React because it feels like "the real interview," and treat CSS as something they'll just pick up, right up until an interviewer asks why a z-index isn't working and the room goes quiet.

When a concept doesn't click from reading about it, LastRoundAI's Concept Explainer breaks it down the way an interviewer actually tests it, stacking contexts, the event loop, reconciliation, rather than the textbook definition. It's built for the moment you realize you can use something correctly without being able to explain why it works.

Get the reps in before the real thing

Reading an answer isn't the same as defending it once an interviewer changes one detail on you mid-question. LastRoundAI's AI Interview Copilot listens during a live call and surfaces guidance in under 200 milliseconds, across 50-plus languages, so it keeps up whether the interview is in English or not. It runs on the desktop app or in-browser, there's no native mobile app, so plan around a laptop or desktop for the actual call.

The free plan includes 15 credits a month that reset monthly rather than carrying over, and Starter is $19/mo if that's not enough runway some months. Once your answers hold up under a follow-up question, the slower part of the job hunt is usually just getting in front of enough companies to ask them. Auto-Apply queues tailored applications for your review, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

AI Interview Copilot
Get live help in your interview

LastRoundAI listens to the call and suggests clear, structured answers to questions like the ones above, in real time and invisible on screen share.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

What should a frontend developer put on their resume for interviews?

Outcomes with numbers attached, and the specific tools you personally used rather than the team stack. Interviewers pick questions from your resume, so anything listed there should be something you are happy to be interrogated about.

How do I stand out as a frontend developer candidate?

Bring one thing that went wrong and what you changed afterwards. Candidates who can narrate a failure honestly consistently read as more senior than candidates with an unbroken record of successes.

What questions should a frontend developer ask the interviewer?

Something that only applies to this team. Asking what the last thing they shipped was, or what the on-call rotation actually looks like, tells you more than a question about culture and signals that you were listening.

What does a frontend developer interview usually cover?

A mix of practical skill, judgement on trade-offs, and how you work with people who disagree with you. The technical portion tends to be scoped to what the team actually does rather than a generic syllabus, so read the job description closely.

Leave a Reply

Your email address will not be published. Required fields are marked *