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.
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.
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:
- 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.
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.
Do not start with polish. Start with the smallest demo that proves the requirement:
- Render static UI
- Add state
- Add the main interaction
- Add edge cases
- Clean up names and structure
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.
- 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.
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>
);
}
- 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."
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.
- 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.
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>
);
}
- 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.
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.
- 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.
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>
);
}
- 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.
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.
- 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.
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>
);
}
- 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.
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.
- 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.
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>
);
}
- 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.
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:
- 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.