Frontend System Design: Build an Autocomplete
A structured walkthrough of the autocomplete design round: requirements, architecture, race-free fetching, caching, rendering, the ARIA combobox and metrics.
System design rounds with a frontend lens: component architecture, data fetching, caching, performance and trade-offs.
A structured walkthrough of the autocomplete design round: requirements, architecture, race-free fetching, caching, rendering, the ARIA combobox and metrics.
I use a repeatable framework and talk through trade-offs as I go:
I timebox each step, check with the interviewer which area to go deep on, and always state the trade-off behind a choice instead of just naming a library.
Likely follow-up: Which non-functional requirements matter most for a mobile-first product?
Requirements: an endless feed of posts (text, images, video), like/comment/share, a composer, and new posts arriving while the user reads. Non-functional: fast first load, smooth scrolling on low-end phones, accessible.
FeedList, PostCard, a code-split Composer and a MediaViewer.GET /feed?cursor=&limit= returning { items, nextCursor }; cursor pagination avoids duplicates when new posts land at the top.IntersectionObserver sentinel prefetches the next page early, virtualization bounds the DOM, and images are responsive (srcset) and lazy-loaded below the fold.feed pattern with article items, aria-setsize and aria-posinset, and full keyboard support.interface Post {
id: string;
authorId: string; // users live in their own normalized map
text: string;
media: { url: string; width: number; height: number }[];
likeCount: number;
likedByMe: boolean;
createdAt: string; // ISO 8601
}
interface FeedPage { items: Post[]; nextCursor: string | null }Likely follow-up: How would you handle new posts arriving in real time? · How do you restore scroll position when the user navigates back?
Requirements: suggestions appear as the user types, keyboard and mouse selection, highlighted matches, recent searches; it should feel instant and be reusable.
Input, SuggestionList and SuggestionItem, configured with props like fetchSuggestions, minChars, debounceMs and renderItem.GET /suggest?q=ne&limit=8 returning ranked items; the server owns the prefix index.AbortController (or ignore responses that don't match the latest query) so results never arrive out of order.role="combobox" with aria-expanded, aria-controls and aria-activedescendant, a listbox of option elements, arrow keys, Enter and Escape, and a polite live region announcing the result count.Likely follow-up: How would you merge suggestions from several sources, such as recent searches and products?
Choose per route: SSG or ISR for marketing and docs, SSR or streaming for personalized SEO pages, CSR for app screens behind a login.
Likely follow-up: What problems does hydration introduce, and how do islands or RSC help?
Core Web Vitals are Google's user-centric metrics, assessed at the 75th percentile of real page loads:
fetchpriority="high", never lazy-load it, and remove render-blocking CSS and JS.width/height or aspect-ratio on media, reserve space for ads and embeds, avoid inserting content above existing content, and match fallback font metrics.Measure in the field (RUM, CrUX) and debug in the lab (Lighthouse, DevTools).
Likely follow-up: How does INP differ from First Input Delay, the metric it replaced?
Requirements: load more items as the user nears the end, stay smooth with thousands of rows, and handle loading, error and end-of-list states.
IntersectionObserver on a sentinel element near the bottom, with a rootMargin so the next page is fetched before the user gets there; no scroll-event polling.GET /items?cursor=abc&limit=30 returning { items, nextCursor }; cursors stay stable when items are inserted, unlike offsets.Likely follow-up: How would you restore the exact scroll position when the user returns from a detail page?
Requirements: 1:1 and group conversations, real-time send and receive, history, typing indicators, read receipts, attachments; it must survive flaky networks.
SharedWorker or leader election.sending, sent, delivered, read, failed).role="log" (an implicitly polite live region) for incoming messages, a labelled composer, keyboard shortcuts.type MessageStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
interface Message {
clientId: string; // generated on the client, used to dedupe acks
id?: string; // assigned by the server
seq?: number; // server ordering within the conversation
conversationId: string;
senderId: string;
body: string;
status: MessageStatus;
createdAt: string;
}Likely follow-up: How would you keep several open tabs of the same account in sync? · How would end-to-end encryption change the design?
Requirements: show one or more slides, previous/next controls, indicators, touch swipe, optional autoplay and looping, fast loading.
Carousel taking items, autoplay, interval, loop and onChange; the current index is state, optionally controlled by the parent.scroll-snap (native swipe and momentum, little JavaScript) or a transform: translateX() track for full control; animate transforms, not left.srcset, and give slides a fixed aspect-ratio to avoid layout shift.aria-roledescription="carousel", slides labelled like "3 of 8", real buttons for controls, and a visible pause button, because WCAG requires a way to pause auto-moving content. Pause on hover and focus, and respect prefers-reduced-motion.Likely follow-up: How would you implement infinite looping without a visible jump?
I classify state by who owns it and how long it lives:
Derive instead of duplicating, keep one source of truth, and lift state only when siblings need it.
Likely follow-up: When does React context become a performance problem, and what do you do about it?
EventSource keeps one HTTP response open while the server streams text/event-stream messages. Server-to-client only, UTF-8 text, automatic reconnection with Last-Event-ID. Over HTTP/1.1 browsers cap it at 6 connections per domain, so prefer HTTP/2.Default to SSE when only the server pushes (notifications, live feeds, dashboards); use WebSockets when the client also sends frequent messages.
Likely follow-up: How would you detect and recover from a silently dropped WebSocket connection?
From closest to the user outward:
Cache-Control (max-age, no-cache = store but revalidate before use, no-store = never store, private, immutable) plus validators (ETag with If-None-Match, Last-Modified) that allow cheap 304 Not Modified responses.s-maxage and stale-while-revalidate and offers purge APIs.Typical setup: fingerprinted assets (app.3f9a1c.js) get public, max-age=31536000, immutable; HTML gets no-cache so a new deploy is picked up immediately; personalized API responses use private with short lifetimes or revalidation. Invalidate by changing URLs rather than waiting for caches to expire.
Likely follow-up: What is the difference between no-cache and no-store?
Measure before changing anything:
Likely follow-up: How would you find out which third-party script is hurting INP?
Requirements: product grid, faceted filters (category, brand, price, rating), sorting, pagination and shareable URLs; SEO and a fast LCP matter because this page drives revenue.
?brand=nike&price=50-100&sort=price_asc&page=2 is the source of truth, so links are shareable and Back works; canonical tags stop filter combinations from creating duplicate SEO pages.GET /products?... returning { items, facets, total }, where facets carry counts so options that would yield nothing can be disabled.fieldset/legend groups, the result count announced in a live region, the mobile filter drawer built as a real dialog.Likely follow-up: Would you choose offset or cursor pagination here, and why?
Requirements: unread badge, paginated list, mark as read, real-time delivery while the app is open, push while it is closed, user preferences.
{ id, type, actorIds, targetUrl, createdAt, readAt }; the server aggregates similar events ("Ana and 3 others liked your post").GET /notifications?cursor=, GET /notifications/unread-count, and POST /notifications/read taking ids or "all".BroadcastChannel.Likely follow-up: How would you avoid showing the same notification in several open tabs?
Both limit how often a function runs in response to rapid events.
Options matter: a leading call fires immediately (useful against double-clicks), a trailing call captures the last event. For visual updates tied to scrolling or pointer movement, requestAnimationFrame is often better than a time-based throttle because it syncs with frames. Cancel pending timers on unmount, and remember that debouncing doesn't fix out-of-order responses; you still need to cancel stale requests.
Likely follow-up: Implement a debounce function that also has a cancel method.
Offset pagination (?page=3&limit=20, SQL OFFSET 40) lets users jump to any page and see totals, which suits admin tables and search results with page numbers. The downsides: if items are inserted or deleted while the user paginates, they see duplicates or miss items, and large offsets get slow because the database still walks past the skipped rows.
Cursor pagination (?after=eyJpZCI6MTIzfQ&limit=20) returns an opaque cursor pointing at the last item seen, usually encoding the sort key plus a unique tiebreaker such as createdAt and id. It stays stable while data changes and uses an index efficiently, but you can't jump to page 7 and totals are harder.
For a feed or infinite scroll I use cursors; for a table where users need page numbers I use offsets.
Likely follow-up: What should a cursor encode if many items share the same timestamp?
Requirements: replies of arbitrary depth, collapse/expand, inline reply, edit and delete, sorting (top, new), votes; threads can hold thousands of comments.
{ id, parentId, authorId, body, score, childIds, replyCount, deleted }, instead of a deeply nested tree, so updating one comment doesn't mean rebuilding the tree.GET /posts/:id/comments?sort=top&cursor= for top-level comments with their first few replies, and GET /comments/:id/replies?cursor= behind "View 12 more replies".Comment component with a maximum indentation depth, beyond which a "Continue thread" link opens that subtree. For huge threads, flatten the visible nodes into a list with depth and virtualize it.aria-expanded, clear author and time labels, keyboard access to reply.Likely follow-up: How would you show new replies from other users in real time without disrupting the reader?
Code splitting breaks one large bundle into chunks loaded on demand, so the initial page downloads, parses and executes less JavaScript. Bundlers create a separate chunk at every dynamic import().
React.lazy with Suspense).Don't lazy-load anything needed for the first paint or the LCP element, and avoid many tiny chunks that create request waterfalls. Hide the latency by prefetching likely next chunks on hover, on idle, or when a link enters the viewport, and verify the result with a bundle analyzer.
Likely follow-up: How would you handle a chunk that fails to load after a new deploy?
innerHTML and dangerouslySetInnerHTML (sanitize with DOMPurify when unavoidable), block javascript: URLs, and add a strict Content Security Policy based on nonces or hashes, e.g. script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'. Trusted Types can lock down DOM sinks.SameSite=Lax or Strict cookies, anti-CSRF tokens on state-changing requests, and Origin or Sec-Fetch-Site checks; never change state on GET.frame-ancestors 'none' (or 'self'), plus X-Frame-Options: DENY for older browsers.localStorage is readable by any injected script, so prefer HttpOnly, Secure, SameSite cookies.Also enforce HTTPS with HSTS, use Subresource Integrity for third-party scripts, and audit dependencies.
Likely follow-up: Where would you store an access token in a single-page app, and why?
Requirements: play/pause, seeking with previews, volume, captions, quality selection, fullscreen and picture-in-picture, keyboard shortcuts, resume where you left off; smooth playback on poor networks.
video element with custom controls, driven by a state machine (idle, loading, playing, paused, buffering, ended, error).preload="metadata", and prefetch the first segments when playback is likely.track, labelled buttons, keyboard controls (Space, arrows, F, M), visible focus. Browsers block autoplay with sound, so autoplay only when muted.playsinline for inline playback on iOS, saving progress on visibilitychange.Likely follow-up: How does the player decide when to switch to a lower quality?
An optimistic update changes the UI immediately as if the request had already succeeded, then reconciles when the response arrives. It makes likes, toggles, reordering and sending messages feel instant.
A typical implementation (for example with TanStack Query):
Watch for concurrent mutations on the same entity (queue them or compare versions) and make retries safe with idempotency keys. Avoid optimistic UI when failure is likely or costly, such as payments, irreversible deletes or anything that needs server validation; show a pending state instead.
Likely follow-up: How do you handle two quick optimistic updates to the same item when the first one fails?
The browser parses HTML into the DOM and CSS into the CSSOM. CSS is render-blocking: nothing paints until the stylesheets the page needs have loaded. A classic synchronous script is parser-blocking: parsing pauses while it downloads and runs. DOM and CSSOM combine into a render tree of visible nodes, then the browser runs layout (geometry), paint (pixels) and compositing (layers onto the screen).
Optimizations:
defer (run in order after parsing) or async (run as soon as downloaded); type="module" scripts are deferred by default.preload and preconnect, and prioritize the LCP image.Likely follow-up: What is the difference between async and defer?
Requirements: several users edit one document at once, see each other's cursors, edits appear with low latency, offline edits merge later, and undo reverts only your own changes.
Likely follow-up: How would you implement undo so it only reverts your own changes?
Virtualization renders only the rows visible in the viewport plus a small overscan buffer, instead of thousands of DOM nodes. A scroll container holds a spacer with the full scrollable height; on scroll you compute the visible index range from scrollTop and row heights, then position those rows with transform or absolute offsets.
ResizeObserver).Trade-offs: browser find-in-page can't see unrendered rows; screen readers lose the list size (add aria-setsize and aria-posinset); focus is lost if a focused row unmounts; very fast scrolling can flash blank areas; scroll restoration and sticky elements get harder. For moderately long lists, CSS content-visibility: auto with contain-intrinsic-size is a cheaper win that keeps everything in the DOM.
Likely follow-up: How would you virtualize a list whose row heights are unknown until images load?
Requirements: columns and cards, drag to reorder within and across columns, reorder columns, a keyboard alternative, filters, and live updates from teammates.
columns and cards by id; store order as a fractional index (a rank string between its neighbours) so a move updates only the moved card.PATCH /cards/:id with { columnId, rank, version }; the server rejects stale versions.interface Card { id: string; columnId: string; rank: string; title: string; version: number }
interface Column { id: string; title: string }
interface BoardState {
columnOrder: string[];
columns: Record<string, Column>;
cards: Record<string, Card>; // a column's cards = cards with its id, sorted by rank
}Likely follow-up: How does fractional indexing avoid rewriting every card position on a move?
Hydration is when client-side JavaScript takes over server-rendered HTML: the framework re-runs components, rebuilds its internal tree against the existing DOM and attaches event listeners. The HTML appears fast, but the page isn't interactive until hydration finishes.
Costs:
Ways to reduce it: islands or partial hydration (hydrate only interactive components), lazy or progressive hydration (on visibility, idle or interaction), selective hydration with streaming and Suspense in React 18+, Server Components so static parts ship no JS, and resumability (Qwik), which avoids re-executing components on load.
Likely follow-up: What causes a hydration mismatch, and how would you fix one caused by dates?
Requirements: pick or drag-and-drop many images, see previews and per-file progress, cancel and retry, handle large files; browse uploads in a fast grid with a full-screen viewer.
input type="file" multiple accept="image/*" with a drop zone as an enhancement; validate type and size on the client, and again on the server.URL.createObjectURL(file), revoked when no longer needed; optionally resize or compress with a canvas or createImageBitmap.XMLHttpRequest with upload.onprogress for progress, since fetch has no upload progress events. Large files use chunked, resumable uploads (S3 multipart or tus).queued, uploading, processing, done or error.srcset, lazy loading, virtualization for huge libraries, a keyboard-friendly lightbox.progress elements, completion announced in a live region, alt text fields.Likely follow-up: How would you resume a 2 GB upload after the connection drops?
They combine: a BFF can expose REST or GraphQL. I decide based on client diversity, team ownership and caching needs.
Likely follow-up: How would you cache GraphQL responses on the client?
Requirements: a configurable grid of widgets, live time series, time-range selection, drill-down, and thousands of points per chart without jank.
IntersectionObserver.visibilitychange).Likely follow-up: How would you let users share a dashboard view with a specific time range?
Micro-frontends split one web app into independently developed and deployed pieces, usually one per business domain and team, composed into a single experience.
Composition options: build-time packages (simple, but you lose independent deploys), runtime integration such as Module Federation or Web Components, iframes (strong isolation, poor integration), or server/edge composition.
Use them when many teams are blocked on one deploy pipeline, or during a gradual migration. For a single team, a well-modularized monolith or monorepo is usually the better choice.
Likely follow-up: How would micro-frontends share state such as the logged-in user?
Images are usually the heaviest bytes on a page and often the LCP element, so:
picture, SVG for icons and logos, and video instead of large animated GIFs.srcset with sizes, so each device downloads a suitably sized file; never ship a 2400px image into a 400px slot.loading="lazy" for below-the-fold images, but never for the LCP image, which should load eagerly with fetchpriority="high" (plus a preload if it is discovered late, such as a CSS background).width and height (or aspect-ratio) so the browser reserves space.Accept header, with long-lived caching.decoding="async".Likely follow-up: How would you find and speed up the LCP image on a page?
A service worker is a script that runs on its own thread, outside any page, and acts as a programmable network proxy for its scope. It requires HTTPS (localhost is allowed) and has no DOM access.
register() → install (precache the app shell) → waiting (a new version waits until no tab uses the old one, unless it calls skipWaiting()) → activate (delete old caches; clients.claim() takes control of open pages) → handles fetch, push and sync events.start_url, display) served over HTTPS makes the app installable, offline-capable and able to receive push.Pitfall: a buggy service worker can keep serving a stale app, so version caches and prompt users to reload when an update is waiting.
Likely follow-up: How would you tell users that a new version of the app is available?
Requirements: cart review, shipping address and method, payment, order review and confirmation, guest checkout; non-functional: security, reliability (never double-charge) and conversion.
autocomplete values (shipping street-address, cc-number), an error summary that receives focus, announced step changes.Likely follow-up: What happens if the network drops right after the user clicks "Place order"?
APIs often return nested data: a post embeds its author and comments, and the same user appears in dozens of places. Storing that as-is duplicates entities, so updating a user's avatar means finding every copy, and copies drift out of sync.
Normalizing means storing each entity type in a lookup table keyed by id and referencing other entities by id:
users: { byId: { u1: {...} }, allIds: ['u1'] }posts: { byId: { p1: { authorId: 'u1', commentIds: [...] } } }Benefits: one source of truth, O(1) lookups and updates, and narrower re-renders because components subscribe to a single entity. Tools: Redux Toolkit's createEntityAdapter, normalizr, and Apollo Client's cache, which normalizes automatically by __typename and id.
Costs: you denormalize in memoized selectors and must clean up references on delete. For simple screens, a query cache keyed by request (TanStack Query) without normalization is often enough.
Likely follow-up: How does Apollo Client decide that two query results refer to the same object?
Goals: consistent UI, accessibility by default, faster delivery, and painless upgrades across many apps.
--color-bg-danger enable theming, dark mode and multiple brands at runtime.variant, size, controlled and uncontrolled modes, slots or compound components) that pass through native attributes and forward refs.Likely follow-up: How would you roll out a breaking change to a component used by 40 apps?
HttpOnly; Secure; SameSite=Lax cookie that JavaScript can't read, so XSS can't steal it. Add CSRF protection for state-changing requests.HttpOnly cookie; avoid localStorage, which any injected script can read.BroadcastChannel and clear cached user data.Likely follow-up: Why is the OAuth implicit flow no longer recommended?
Requirements: open dialogs from anywhere (including non-UI code), stack them, support confirm dialogs that return a result, animate, and be fully accessible.
dialog element with showModal() provides the top layer (no z-index wars), an inert background, Escape handling via the cancel event, and ::backdrop. Otherwise, a portal with manual focus trapping and the inert attribute on the rest of the page.Dialog with open and onClose, plus an imperative manager for one-offs, e.g. const ok = await confirm({ title, message }), backed by a provider that keeps a stack.aria-labelledby (custom dialogs also need role="dialog" and aria-modal="true"); move focus inside on open, keep Tab inside, return focus to the trigger on close, close on Escape, and lock background scroll.Likely follow-up: How would you make a modal deep-linkable so a URL opens it directly?
Requirements: show success, error and info messages from anywhere, auto-dismiss, stack several, and support an optional action such as Undo.
toast.success('Saved', { action, duration }) callable outside components, backed by a small store or event emitter holding a queue; a Toaster component renders it in a portal.role="status" for polite messages, role="alert" sparingly for errors) and insert text into them, because regions added dynamically are often not announced. Don't move focus to toasts, make actions keyboard-reachable, give people enough time to read, and never make a disappearing toast the only way to reach an important action.window), long messages.Likely follow-up: How would you implement an Undo action for a delete shown in a toast?
AbortController, ignore responses that aren't the latest, or use switchMap in RxJS.ignore flag.ETag in If-Match, and the server replies 412 Precondition Failed if the record changed.Likely follow-up: How would you resolve a race between an optimistic update and a WebSocket event for the same item?
eslint-plugin-jsx-a11y in the editor and axe-core in component and end-to-end tests in CI, remembering that automated tools catch only part of the problems.In SPAs, also manage focus and announcements on route changes.
Likely follow-up: How do you manage focus and announcements on route changes in a single-page app?
Requirements: results for a query with highlighted matches, filters and sorting, pagination, spelling suggestions, fast first results, and shareable URLs.
/search?q=running+shoes&sort=relevance&page=2; the search box, filters and page read from and write to the URL, so Back, refresh and sharing work.GET /search?q&filters&page returning { results, facets, total, correctedQuery }, with highlight ranges computed by the search engine.navigator.sendBeacon to improve ranking.role="search" or the search element), the result count announced in a live region, one heading per result, sensible focus after submitting.Likely follow-up: How would you measure whether search quality is improving?
A performance budget is a set of limits the team agrees not to exceed, turning "keep it fast" into a measurable, enforceable rule.
Set them from your real users' devices and networks, current baselines and competitors, and derive byte budgets from the timing goals. Enforce them in CI with tools like size-limit or bundler size warnings and Lighthouse CI assertions, failing or flagging pull requests that exceed them, and watch RUM dashboards with alerts for production regressions. When a feature needs more budget, make the trade-off explicit: remove something else or justify raising the limit.
Likely follow-up: How would you handle a product request that blows the JavaScript budget?
Requirements: month, week and day views, creating and editing events by click or drag, recurring events, invitations and availability, time zones.
Europe/Berlin), all-day events as plain dates, and recurrence as an RRULE (RFC 5545) with exceptions; occurrences are expanded only for the visible range.GET /events?start=&end= for the visible range, prefetching adjacent ranges; mutations are optimistic.Intl.DateTimeFormat; handle DST days with 23 or 25 hours and locale settings such as week start and 12/24-hour clocks.Likely follow-up: How would you implement editing "this and all following" occurrences of a recurring event?
{count, plural, one {# file} other {# files}}; languages have different plural categories (Arabic has six).Intl APIs (DateTimeFormat, NumberFormat, PluralRules, RelativeTimeFormat, Collator) for dates, currencies, numbers and sorting.Accept-Language; put the locale in the URL (/de/...) for SEO, with hreflang links.lang and dir="rtl", use CSS logical properties (margin-inline-start), mirror directional icons, and leave room for longer text.Common libraries: FormatJS (react-intl), i18next, Angular's built-in i18n.
Likely follow-up: How would you test that a layout survives long German strings and right-to-left languages?
error and unhandledrejection listeners.web-vitals library and custom timings with PerformanceObserver, sent with navigator.sendBeacon when the page is hidden; sample to control cost.traceparent header on API calls so frontend events link to backend traces.Segment dashboards by release to catch bad deploys fast.
Likely follow-up: What would you capture in breadcrumbs to make a bug reproducible?
Requirements: create, edit and search notes with no connection, sync across devices when back online, and never lose edits.
navigator.storage.persist() to reduce the risk of eviction.online event, or Background Sync where supported), push the outbox, then pull changes since the last server cursor.BroadcastChannel and the Web Locks API so only one tab syncs.interface Note {
id: string; // UUID generated on the device
title: string;
body: string;
updatedAt: number; // for display only, not for ordering
version: number; // last version confirmed by the server
deleted: boolean; // tombstone so deletes sync
}
interface OutboxEntry { noteId: string; baseVersion: number; patch: Partial<Note> }Likely follow-up: How would you resolve two devices editing the same note while offline?
Requirements: show a question and options, let each user vote once, show live results with percentages, and embed on any site without breaking it.
postMessage.GET /polls/:id and POST /polls/:id/votes with { optionId }.localStorage flag only remembers the vote for UX.fieldset with a legend, a real submit button, results announced after voting.Likely follow-up: How would you stop a script from casting thousands of fake votes?
HttpOnly, Secure and SameSite.Request/Response pairs, mainly from a service worker for offline assets and API responses.All are scoped per origin and can be evicted under storage pressure unless navigator.storage.persist() is granted.
Likely follow-up: Why is localStorage a poor place for an authentication token?
Requirements: expand and collapse folders, lazy-load children, select, rename, create, delete, drag to move, search, and full keyboard support for trees with thousands of nodes.
{ id, name, type, parentId, childIds, hasChildren } so any node can be updated or moved cheaply; UI state separately holds expanded ids, the selected id, and per-node loading and error.GET /nodes/:id/children on first expand (then cached) and PATCH /nodes/:id for rename or move, with optimistic updates and rollback.role="tree", treeitem, group, aria-expanded, aria-level, aria-selected) with a roving tabindex. Up/Down move, Right expands or enters a folder, Left collapses or goes to the parent, plus Home/End and type-ahead.Intl.Collator.Likely follow-up: How would you implement search that reveals matches inside collapsed folders?
unicode-range), and consider one variable font instead of many weights, or simply a system font stack.link rel="preload" as="font" type="font/woff2" crossorigin for the one or two critical fonts; crossorigin is required even for same-origin fonts.swap shows fallback text immediately and swaps when the font arrives (good for brand fonts, but it can shift layout); optional gives the font a very short window and otherwise keeps the fallback, avoiding layout shift; block hides text and is rarely appropriate.size-adjust, ascent-override and descent-override so it matches the web font's metrics.Cache-Control on versioned font files.Likely follow-up: When would you choose font-display: optional over swap?
Feature flags decouple deploying code from releasing it.
Likely follow-up: How would you run an experiment on a statically generated page?
I follow the test pyramid, adjusted for UI work:
Everything runs in CI on every pull request, and flaky tests are fixed or quarantined quickly, because a suite nobody trusts gets ignored.
Likely follow-up: How do you keep end-to-end tests from becoming flaky?
Requirements: formatting (bold, headings, lists, links), images and embeds, mentions, Markdown shortcuts, undo/redo, pasting from other apps, and optional collaboration.
contenteditable: browsers produce inconsistent HTML, so modern editors (ProseMirror, Lexical, Slate) keep their own document model: a schema of nodes and marks, immutable state, and transactions. The model renders to the DOM, and user input (beforeinput events) is translated back into transactions.## makes a heading), mentions with an autocomplete popup, and a history plugin that groups steps for undo.role="toolbar" with aria-pressed toggle buttons, keyboard shortcuts for every action, a labelled editing region.Likely follow-up: How would you implement @mentions inside the editor?
Likely follow-up: How would you roll back a bad frontend deploy within a minute?
A monorepo holds many apps and packages (web app, design system, shared utilities) in one repository. It is not a monolith: each package can still build and deploy independently.
CODEOWNERS for review ownership.Choose it when teams share a lot of code and want to evolve it together; separate repos suit truly independent products.
Likely follow-up: How do monorepos relate to micro-frontends?
Requirements: pan and zoom a map, show many markers, search places, show the user's location, and keep a results list in sync with the map.
navigator.geolocation requires a secure context and user permission, so ask only after a user action and fall back to manual search if denied.Likely follow-up: How would you smoothly animate a moving car from sparse location updates?
No questions match that filter.