
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:
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.
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:
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.
Before writing React code, spend two minutes turning the prompt into a plan.
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.
Most frontend machine coding round questions fail in state design. List the important states before coding:
Avoid storing the same idea twice. If visibleItems can be derived from items and filter, derive it.
Keep it practical:
AutocompleteSearchInputSuggestionsListSuggestionItem
You do not need a production-grade architecture. You need boundaries that make the code easier to review.
Do not start with polish. Start with the smallest demo that proves the requirement:
This rhythm is what separates strong React live coding interview questions from half-finished code.
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.
items, page, status, and hasMore in state.page changes.IntersectionObserver on a sentinel element at the bottom of the list.hasMore becomes false.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 | errorconst [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>);}
IntersectionObserver.items value instead of a functional state update.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."
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.
max, value, and onChange.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><divonMouseLeave={() => {setHoveredValue(0);}}>{Array.from({ length: max }, (_, index) => {const rating = index + 1;const isFilled = rating <= displayValue;return (<buttonaria-label={`${rating} star${rating === 1 ? "" : "s"}`}aria-pressed={rating === selectedValue}key={rating}onClick={() => {selectRating(rating);}}onMouseEnter={() => {setHoveredValue(rating);}}type="button">{isFilled ? "★" : "☆"}</button>);})}</div></fieldset>);}
value prop is provided.type="button", which can accidentally submit a parent form.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.
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.
query, suggestions, status, and activeIndex in state.AbortController so older requests cannot overwrite newer results.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 | errorconst [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><inputaria-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) => (<liaria-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>);}
onMouseDown avoids that common issue.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.
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.
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) => (<lidraggablekey={item.id}onDragEnd={() => {setDraggedId(null);}}onDragOver={(event) => {event.preventDefault();}}onDragStart={() => {setDraggedId(item.id);}}onDrop={() => {moveItemBefore(item.id);}}>{item.label}</li>))}</ul>);}
items array with splice instead of returning a new array.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.
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.
showToast function.setTimeout.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><buttononClick={() => {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><buttonaria-label="Dismiss notification"onClick={() => {dismissToast(toast.id);}}type="button">×</button></div>))}</div></section>);}
isToastVisible, which cannot represent multiple toasts.For a stronger answer, mention how you would expose this through context in a real app:
ToastProvideruseToast()showToast()dismissToast()ToastViewport
That shows you understand the difference between an interview implementation and a reusable application service.
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.
| Component | First follow-up | What it tests |
|---|---|---|
| Infinite scroll | Add retry and cursor pagination | Async state and API assumptions |
| Star rating | Support half-star ratings | Component API and event handling |
| Autocomplete search | Add caching and stale response handling | Async correctness and performance |
| Drag-and-drop list | Move items across two lists | Data modeling and immutable updates |
| Toast notification | Add toast placement and max queue size | State 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?"
If you have one week before a frontend LLD or React machine coding round, use this sequence:
Day 1: Basic state and events Build counter, accordion, tabs, todo list, and star rating.
Day 2: Lists and derived state Build transfer list, selectable cells, data table, and drag-and-drop reorder.
Day 3: Async UI Build autocomplete, job board, infinite scroll, and a search results page.
Day 4: Timers and queues Build progress bars, stopwatch, traffic light, and toast notifications.
Day 5: Accessibility pass Revisit tabs, accordion, modal dialog, autocomplete, and star rating.
Day 6: Timed mocks Pick two problems and solve each in 45 to 60 minutes.
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.
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:
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.
Practice 50 React coding interview questions with solutions. Essential for front end developers aiming to excel in their 2025 job interviews
Practice these user interface questions if you are short on time to prepare for your front end interviews.