Frontend LLD Interview Guide: Low-Level Design for Frontend Devs

Practice frontend LLD questions and React machine coding interview questions with requirements, planning steps, code solutions, and common mistakes.
Author
GreatFrontEnd Team
16 min read
Jun 3, 2026
Frontend LLD Interview Guide: Low-Level Design for Frontend Devs

Frontend LLD questions are machine coding interview questions where you design and build a small UI feature from scratch. In frontend interviews, "LLD" usually means proving that you can break a component into requirements, state, interactions, edge cases, and readable code before the timer runs out.

This guide covers five practical frontend LLD problems:

  • Infinite scroll list
  • Star rating
  • Autocomplete search
  • Drag-and-drop list
  • Toast notification system

Each one includes the requirement, a two-minute planning outline, a React solution, and the mistakes interviewers notice quickly.

Use this article as a practice guide, then solve more user interface coding interview questions on GreatFrontEnd in a real coding environment.

What frontend LLD means in React interviews

LLD stands for low-level design. In backend interviews, it often means designing classes, APIs, relationships, and object behavior. In frontend interviews, the same term is usually applied to UI implementation.

For a frontend developer, frontend LLD usually tests whether you can:

  • Clarify product requirements before coding
  • Split the UI into sensible components
  • Choose the right state model
  • Handle events, async behavior, loading states, errors, and empty states
  • Keep the code small enough to explain
  • Add basic accessibility where the component needs it
  • Ship a working demo under time pressure

That is why frontend LLD questions overlap heavily with React machine coding round questions and solutions. The interviewer is not only checking whether the component works. They are checking whether the implementation has a design.

If you are preparing for LLD for frontend developer roles, treat each prompt as both a design problem and a coding problem. The same prompts often appear under names like frontend machine coding round questions in React, React live coding interview questions, or React UI coding questions.

How to approach any frontend LLD question

Before writing React code, spend two minutes turning the prompt into a plan.

1. Restate the requirement

Say what you are building in one sentence.

For example: "I will build an autocomplete input that fetches suggestions after the user types, shows loading and empty states, and lets the user select a suggestion."

This catches misunderstandings early.

2. Identify the core states

Most frontend machine coding round questions fail in state design. List the important states before coding:

  • What data is displayed?
  • What user input is controlled?
  • Is the component idle, loading, successful, or in error?
  • Is there a selected, active, hovered, expanded, or dragged item?
  • Which values are derived instead of stored?

Avoid storing the same idea twice. If visibleItems can be derived from items and filter, derive it.

3. Sketch the component tree

Keep it practical:

Autocomplete
SearchInput
SuggestionsList
SuggestionItem

You do not need a production-grade architecture. You need boundaries that make the code easier to review.

4. Decide the first shippable version

Do not start with polish. Start with the smallest demo that proves the requirement:

  1. Render static UI
  2. Add state
  3. Add the main interaction
  4. Add edge cases
  5. Clean up names and structure

This rhythm is what separates strong React live coding interview questions from half-finished code.

Question 1: Infinite scroll list

Requirement

Build a list that loads the next page of items when the user scrolls near the bottom. Show loading, error, and "no more results" states.

This is a common frontend LLD problem because it tests async state, browser APIs, cleanup, duplicate request prevention, and edge cases around slow responses.

Two-minute plan

  • Keep items, page, status, and hasMore in state.
  • Fetch whenever page changes.
  • Use an IntersectionObserver on a sentinel element at the bottom of the list.
  • Only increment the page after the previous request succeeds.
  • Disconnect the observer during cleanup.
  • Do not fetch more pages after hasMore becomes false.

React solution

import { useEffect, useRef, useState } from "react";
export default function InfiniteScrollList({ fetchPage }) {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [status, setStatus] = useState("idle"); // idle | loading | success | error
const [hasMore, setHasMore] = useState(true);
const sentinelRef = useRef(null);
useEffect(() => {
let cancelled = false;
async function loadPage() {
setStatus("loading");
try {
const result = await fetchPage(page);
if (cancelled) {
return;
}
setItems((currentItems) => [...currentItems, ...result.items]);
setHasMore(result.hasMore);
setStatus("success");
} catch {
if (!cancelled) {
setStatus("error");
}
}
}
loadPage();
return () => {
cancelled = true;
};
}, [fetchPage, page]);
useEffect(() => {
const sentinel = sentinelRef.current;
if (sentinel == null || status !== "success" || !hasMore) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// Stop observing until the next page finishes loading.
observer.unobserve(sentinel);
setPage((currentPage) => currentPage + 1);
}
},
// Start loading before the user reaches the absolute bottom.
{ rootMargin: "200px" },
);
observer.observe(sentinel);
return () => {
observer.disconnect();
};
}, [hasMore, status]);
return (
<section>
<ul>
{items.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
{status === "loading" && <p>Loading...</p>}
{status === "error" && <p>Unable to load more items.</p>}
{status === "success" && items.length === 0 && <p>No results found.</p>}
{items.length > 0 && !hasMore && <p>No more results.</p>}
<div ref={sentinelRef} aria-hidden="true" />
</section>
);
}

Common mistakes

  • Observing the sentinel while the first request is still loading, which can skip straight to page two.
  • Not disconnecting the IntersectionObserver.
  • Not guarding against duplicate requests.
  • Appending results with a stale items value instead of a functional state update.
  • Forgetting the empty state when the first page has no results.

In a real interview, call out pagination details explicitly: "I am assuming the API returns { items, hasMore }. If it returns total count or next cursor instead, I will adapt the state model."

Question 2: Star rating

Requirement

Build a star rating component that allows users to select a rating from one to five. It should show hover preview, selected value, and support a controlled value prop.

Practice this problem directly on GreatFrontEnd: Star Rating in React.

Two-minute plan

  • Accept max, value, and onChange.
  • Store internal value only when the component is uncontrolled.
  • Store hover value separately.
  • Render each star as a button, not a plain span.
  • Use the hover value for preview and the selected value for the committed rating.

React solution

import { useState } from "react";
export default function StarRating({ max = 5, value, onChange }) {
const [internalValue, setInternalValue] = useState(0);
const [hoveredValue, setHoveredValue] = useState(0);
const selectedValue = value ?? internalValue;
const displayValue = hoveredValue || selectedValue;
function selectRating(nextValue) {
if (value == null) {
setInternalValue(nextValue);
}
onChange?.(nextValue);
}
return (
<fieldset>
<legend>Rating</legend>
<div
onMouseLeave={() => {
setHoveredValue(0);
}}
>
{Array.from({ length: max }, (_, index) => {
const rating = index + 1;
const isFilled = rating <= displayValue;
return (
<button
aria-label={`${rating} star${rating === 1 ? "" : "s"}`}
aria-pressed={rating === selectedValue}
key={rating}
onClick={() => {
selectRating(rating);
}}
onMouseEnter={() => {
setHoveredValue(rating);
}}
type="button"
>
{isFilled ? "★" : "☆"}
</button>
);
})}
</div>
</fieldset>
);
}

Common mistakes

  • Rendering stars as clickable text with no button semantics.
  • Mixing hover state and selected state into one variable.
  • Supporting controlled mode poorly, then mutating internal state even when a value prop is provided.
  • Forgetting type="button", which can accidentally submit a parent form.
  • Hardcoding five stars when the prompt asks for configurability.

A strong follow-up is half-star ratings. The design change is clear: replace each full-star button with either two hit areas or pointer-position logic, then store values in 0.5 increments.

Question 3: Autocomplete search

Requirement

Build an autocomplete search input that fetches suggestions as the user types. It should debounce requests, cancel stale requests, show loading and empty states, and allow keyboard selection.

Autocomplete is one of the best frontend LLD questions because it touches forms, async behavior, race conditions, keyboard interactions, positioning, and accessibility. For a deeper architecture discussion, read GreatFrontEnd's Autocomplete front end system design question.

Two-minute plan

  • Keep query, suggestions, status, and activeIndex in state.
  • Do not search until the query has enough characters.
  • Debounce the search so every keystroke does not call the API.
  • Use AbortController so older requests cannot overwrite newer results.
  • Support Arrow Down, Arrow Up, Enter, and Escape.

React solution

import { useEffect, useState } from "react";
export default function Autocomplete({ search }) {
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState([]);
const [status, setStatus] = useState("idle"); // idle | loading | success | error
const [activeIndex, setActiveIndex] = useState(-1);
useEffect(() => {
const trimmedQuery = query.trim();
if (trimmedQuery.length < 2) {
setSuggestions([]);
setStatus("idle");
setActiveIndex(-1);
return;
}
// AbortController lets us cancel this request if the user types a newer query.
const controller = new AbortController();
// Debounce the request so every keystroke does not call the API.
const timeoutId = window.setTimeout(async () => {
setStatus("loading");
try {
const results = await search(trimmedQuery, {
signal: controller.signal,
});
setSuggestions(results);
setStatus("success");
setActiveIndex(results.length > 0 ? 0 : -1);
} catch (error) {
if (error?.name !== "AbortError") {
setStatus("error");
}
}
}, 250);
return () => {
// Cancel both the scheduled search and any in-flight request for the old query.
window.clearTimeout(timeoutId);
controller.abort();
};
}, [query, search]);
function selectSuggestion(suggestion) {
setQuery(suggestion.label);
setSuggestions([]);
setStatus("idle");
setActiveIndex(-1);
}
function handleKeyDown(event) {
if (suggestions.length === 0) {
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
setActiveIndex((currentIndex) =>
Math.min(currentIndex + 1, suggestions.length - 1),
);
}
if (event.key === "ArrowUp") {
event.preventDefault();
setActiveIndex((currentIndex) => Math.max(currentIndex - 1, 0));
}
if (event.key === "Enter" && activeIndex >= 0) {
event.preventDefault();
selectSuggestion(suggestions[activeIndex]);
}
if (event.key === "Escape") {
setSuggestions([]);
setActiveIndex(-1);
}
}
return (
<div>
<label htmlFor="search">Search</label>
<input
aria-activedescendant={
activeIndex >= 0
? `search-suggestion-${suggestions[activeIndex].id}`
: undefined
}
aria-autocomplete="list"
aria-controls="search-suggestions"
aria-expanded={suggestions.length > 0}
autoComplete="off"
id="search"
onChange={(event) => {
setQuery(event.target.value);
}}
onKeyDown={handleKeyDown}
role="combobox"
value={query}
/>
{status === "loading" && <p>Loading suggestions...</p>}
{status === "error" && <p>Unable to load suggestions.</p>}
{status === "success" && suggestions.length === 0 && (
<p>No suggestions found.</p>
)}
{suggestions.length > 0 && (
<ul id="search-suggestions" role="listbox">
{suggestions.map((suggestion, index) => (
<li
aria-selected={index === activeIndex}
id={`search-suggestion-${suggestion.id}`}
key={suggestion.id}
onMouseDown={(event) => {
// Keep focus on the input while selecting with the mouse.
event.preventDefault();
selectSuggestion(suggestion);
}}
role="option"
>
{suggestion.label}
</li>
))}
</ul>
)}
</div>
);
}

Common mistakes

  • Debouncing the input value instead of the side effect, then creating confusing state.
  • Not cancelling stale requests. A slower response for "rea" can overwrite a faster response for "react".
  • Closing the dropdown on blur before the click on a suggestion runs. onMouseDown avoids that common issue.
  • Ignoring empty results and failed requests.
  • Adding ARIA attributes without keeping active state and selected state consistent.

If the interviewer asks for production-level accessibility, discuss the full combobox pattern and keyboard expectations. In a timed React live coding interview, a clear partial implementation plus the right explanation is usually better than a broken full implementation.

Question 4: Drag-and-drop list

Requirement

Build a list where users can reorder items by dragging one item and dropping it before another item.

This is a useful LLD problem because it tests list state, stable keys, event handling, and how well you can avoid overcomplicating the first version.

Two-minute plan

  • Store list items in order.
  • Track the dragged item ID.
  • On drop, remove the dragged item from the current list.
  • Insert it before the drop target.
  • Use stable IDs, not array indexes, as keys.
  • Mention that HTML drag and drop has touch-device limitations.

React solution

import { useState } from "react";
const initialItems = [
{ id: "todo", label: "Todo" },
{ id: "doing", label: "Doing" },
{ id: "review", label: "Review" },
{ id: "done", label: "Done" },
];
export default function DragAndDropList() {
const [items, setItems] = useState(initialItems);
const [draggedId, setDraggedId] = useState(null);
function moveItemBefore(targetId) {
if (draggedId == null || draggedId === targetId) {
return;
}
setItems((currentItems) => {
const draggedItem = currentItems.find((item) => item.id === draggedId);
if (draggedItem == null) {
return currentItems;
}
const remainingItems = currentItems.filter(
(item) => item.id !== draggedId,
);
// Find the target after removing the dragged item so the insertion index is correct.
const targetIndex = remainingItems.findIndex(
(item) => item.id === targetId,
);
if (targetIndex === -1) {
return currentItems;
}
return [
...remainingItems.slice(0, targetIndex),
draggedItem,
...remainingItems.slice(targetIndex),
];
});
}
return (
<ul>
{items.map((item) => (
<li
draggable
key={item.id}
onDragEnd={() => {
setDraggedId(null);
}}
onDragOver={(event) => {
event.preventDefault();
}}
onDragStart={() => {
setDraggedId(item.id);
}}
onDrop={() => {
moveItemBefore(item.id);
}}
>
{item.label}
</li>
))}
</ul>
);
}

Common mistakes

  • Using array indexes as keys, then getting strange reorder behavior.
  • Mutating the existing items array with splice instead of returning a new array.
  • Trying to support every drag-and-drop feature before the basic reorder works.
  • Forgetting that browser drag and drop is not a complete mobile solution.
  • Not resetting the dragged state after the drag ends.

If the prompt expects a polished implementation, add a drop zone after the final item so the user can move an item to the end. If the prompt expects mobile support, explain that you would likely use pointer events or a well-tested drag-and-drop library in production.

For related practice, solve Transfer List in React, which tests similar item movement and state updates.

Question 5: Toast notification system

Requirement

Build a toast notification system that can show multiple messages, auto-dismiss each toast after a delay, and allow manual dismissal.

This frontend LLD question tests queues, timers, cleanup, rendering a stack, and accessibility for status updates.

Two-minute plan

  • Store an array of toast objects.
  • Generate stable IDs locally.
  • Add a showToast function.
  • Auto-dismiss each toast with setTimeout.
  • Clear timers when a toast is manually dismissed or when the component unmounts.
  • Render the toast region with live-region semantics.

React solution

import { useCallback, useEffect, useRef, useState } from "react";
export default function ToastDemo() {
const [toasts, setToasts] = useState([]);
const nextIdRef = useRef(1);
// Keep timer IDs outside render state so they can be cleared reliably.
const timersRef = useRef(new Map());
const dismissToast = useCallback((id) => {
const timerId = timersRef.current.get(id);
if (timerId != null) {
window.clearTimeout(timerId);
timersRef.current.delete(id);
}
setToasts((currentToasts) =>
currentToasts.filter((toast) => toast.id !== id),
);
}, []);
const showToast = useCallback(
(message, variant = "info") => {
const id = nextIdRef.current;
nextIdRef.current += 1;
setToasts((currentToasts) => [
...currentToasts,
{ id, message, variant },
]);
const timerId = window.setTimeout(() => {
dismissToast(id);
}, 4000);
timersRef.current.set(id, timerId);
},
[dismissToast],
);
useEffect(() => {
return () => {
timersRef.current.forEach((timerId) => {
window.clearTimeout(timerId);
});
timersRef.current.clear();
};
}, []);
return (
<section>
<button
onClick={() => {
showToast("Profile saved", "success");
}}
type="button"
>
Show toast
</button>
<div aria-live="polite" aria-relevant="additions" role="status">
{toasts.map((toast) => (
<div data-variant={toast.variant} key={toast.id}>
<span>{toast.message}</span>
<button
aria-label="Dismiss notification"
onClick={() => {
dismissToast(toast.id);
}}
type="button"
>
×
</button>
</div>
))}
</div>
</section>
);
}

Common mistakes

  • Using one boolean like isToastVisible, which cannot represent multiple toasts.
  • Starting timers without clearing them.
  • Using the array index as the toast ID.
  • Removing the newest toast when the oldest timer finishes.
  • Rendering a visual notification with no live-region announcement.

For a stronger answer, mention how you would expose this through context in a real app:

ToastProvider
useToast()
showToast()
dismissToast()
ToastViewport

That shows you understand the difference between an interview implementation and a reusable application service.

How to compare React machine coding solutions

When practicing react machine coding round questions and solutions, do not only ask "does it work?" Ask whether the solution will survive follow-up questions.

ComponentFirst follow-upWhat it tests
Infinite scrollAdd retry and cursor paginationAsync state and API assumptions
Star ratingSupport half-star ratingsComponent API and event handling
Autocomplete searchAdd caching and stale response handlingAsync correctness and performance
Drag-and-drop listMove items across two listsData modeling and immutable updates
Toast notificationAdd toast placement and max queue sizeState queues and reusable APIs

Good frontend LLD preparation means practicing the base problem and then asking: "What would break if the interviewer adds one more requirement?"

A practical preparation path

If you have one week before a frontend LLD or React machine coding round, use this sequence:

  1. Day 1: Basic state and events Build counter, accordion, tabs, todo list, and star rating.

  2. Day 2: Lists and derived state Build transfer list, selectable cells, data table, and drag-and-drop reorder.

  3. Day 3: Async UI Build autocomplete, job board, infinite scroll, and a search results page.

  4. Day 4: Timers and queues Build progress bars, stopwatch, traffic light, and toast notifications.

  5. Day 5: Accessibility pass Revisit tabs, accordion, modal dialog, autocomplete, and star rating.

  6. Day 6: Timed mocks Pick two problems and solve each in 45 to 60 minutes.

  7. Day 7: Review and redo Redo the weakest problem without looking at your previous code.

GreatFrontEnd has many of these as browser-based practice questions. Start with React coding interview questions, then add the broader UI coding question set.

What interviewers are looking for

In frontend LLD interviews, the strongest candidates make their thinking easy to follow. They do not simply type React code quickly.

Interviewers usually look for:

  • A clear requirement breakdown
  • A reasonable component structure
  • State that does not contradict itself
  • Functional updates where previous state matters
  • Cleanup for effects, subscriptions, observers, and timers
  • Edge cases such as empty, loading, error, disabled, and repeated interactions
  • Basic accessibility for interactive controls
  • A working demo path before time runs out

The best habit is simple: say the design before you code it. A practical sentence like "I will keep hover preview separate from committed rating because they represent different states" gives the interviewer confidence that your code is intentional.


Frontend LLD questions are not a different category from React machine coding questions. They are the design layer inside the coding round.

If you can clarify requirements, model state, build the main interaction, handle edge cases, and explain trade-offs while coding, you are preparing for the round that many frontend interviews now use to separate React familiarity from real UI engineering skill.

Related articles

Practice 50 React Coding Interview Questions with SolutionsPractice 50 React coding interview questions with solutions. Essential for front end developers aiming to excel in their 2025 job interviews
5 Most Important User Interface Questions to Master for Front End InterviewsPractice these user interface questions if you are short on time to prepare for your front end interviews.