Overview
This page is the active-recall companion to the Frontend Essentials learning track: four fixed
drills that force retrieval instead of passive re-reading. Work through them in order --
short-answer recall first, then scenario judgment, then hands-on implementation, then a checklist to
confirm real automaticity. Every answer is hidden in a <details> block; try each item yourself
before opening it. Every kata below is fully self-contained HTML/CSS/JS -- no live third-party host,
no bundler, no dev server -- so recognizing an answer isn't enough; you have to actually reconstruct
the reasoning by opening the file yourself.
Recall Q&A
Twenty-eight short-answer questions, one per concept (co-01 through co-28). Answer from memory,
then check.
Q1 (co-01 -- html-document-structure). Name the three things a valid HTML5 page needs at minimum,
and what the viewport meta tag actually controls.
Answer
<!doctype html>, a head with charset and title, and a body. The viewport meta tag tells a
mobile browser how to scale the page to the device's screen width, instead of rendering it at a
fixed desktop-width default and shrinking everything down.
Q2 (co-02 -- html-semantics). Why does <nav> communicate more to a screen reader than a <div class="nav"> styled identically?
Answer
<nav> is a real landmark element -- it's exposed in the accessibility tree with the navigation
role automatically, letting assistive tech jump straight to it. A <div class="nav"> carries no
semantic meaning at all; a screen reader sees a generic, unnamed container regardless of its class
name or visual styling.
Q3 (co-03 -- text-and-links). An <img> fails to load. What must be present on that element for
a user to still know what it was, and why can't a filename alone serve that purpose?
Answer
A descriptive alt attribute. Filenames are often auto-generated, cryptic, or purely technical
(IMG_4821.jpg) and are never read aloud by assistive tech in place of alt text -- only alt
is guaranteed to convey the image's meaning when the image itself is unavailable.
Q4 (co-04 -- css-selectors). Write a single selector that targets every input of type email
while it has keyboard focus, and name the two selector features it combines.
Answer
input[type=email]:focus -- it combines an attribute selector ([type=email]) with a pseudo-class
selector (:focus). Both narrow the same base element type down to a more specific condition.
Q5 (co-05 -- css-specificity-cascade). An element has both a class rule and an id rule setting different colors. Which wins, and would the answer change if the class rule appeared later in the stylesheet?
Answer
The id rule wins regardless of source order -- an id selector carries higher specificity than a class selector, and specificity is compared before source order ever becomes a tiebreaker. Source order only decides the winner when two rules have EQUAL specificity.
Q6 (co-06 -- css-custom-properties). Two unrelated CSS rules both use var(--brand). If
--brand is redefined inside a .dark scope, what happens to elements inside that scope versus
elements outside it?
Answer
Elements inside .dark recompute var(--brand) to the new, scoped value; elements outside .dark
keep resolving to whatever --brand is defined as in their own ancestor chain (typically :root's
value). Custom properties cascade and are resolved per-element, not once globally.
Q7 (co-07 -- box-model). An element has width: 100px, padding: 10px, and border: 2px solid.
Under the default box model, what is its actual rendered (offsetWidth) width, and how does
box-sizing: border-box change that?
Answer
Under the default content-box model, offsetWidth is 124px (100 content + 20 padding + 4 border).
Switching to box-sizing: border-box folds padding and border INTO the declared width, so
offsetWidth becomes exactly 100px -- the declared width now means the full rendered box, not just
the content area.
Q8 (co-08 -- normal-flow-display). An element is set to display: none. What does
element.offsetParent report, and how does that differ from an element merely styled
visibility: hidden?
Answer
offsetParent reports null -- the element is removed from layout entirely, occupying no space and
participating in no flow calculations. visibility: hidden is different: the element still occupies
its layout space and still has a non-null offsetParent; it's only invisible, not removed.
Q9 (co-09 -- flexbox). In a flex row, one child has flex-grow: 1 and its two siblings have
flex-grow: 0 (the default). What happens to the container's leftover free space?
Answer
The one child with flex-grow: 1 absorbs ALL of the leftover free space along the main axis; the
flex-grow: 0 siblings stay at their natural (or explicitly set) size and grow no further.
Q10 (co-10 -- grid). What's the difference between grid-template-columns and
grid-template-areas as ways of placing children into a grid?
Answer
grid-template-columns (paired with grid-template-rows) defines the SIZE of each track (e.g.
1fr 1fr for two equal columns); grid-template-areas names REGIONS of the grid as a text map, and
children opt into a named region via grid-area. The two are usually combined -- tracks define the
geometry, areas give each region a readable name.
Q11 (co-11 -- responsive-media-queries). A layout uses @media (max-width: 600px) to stack two
columns into one. What triggers that rule to apply, and does it require any JavaScript?
Answer
The rule applies purely from CSS, whenever the browser's viewport width is at most 600px -- re-evaluated live as the viewport is resized. No JavaScript is involved; media queries are a declarative CSS mechanism the rendering engine itself evaluates.
Q12 (co-12 -- dom-selection). document.querySelectorAll('li') is called once, then two more
li elements are appended to the page. Does the returned collection grow to include them?
Answer
No -- querySelectorAll returns a static NodeList, a snapshot taken at call time. Elements added
to the DOM afterward are not reflected in that already-returned collection; a fresh
querySelectorAll call would be needed to see them.
Q13 (co-13 -- dom-manipulation). Name three distinct DOM APIs used to mutate rendered content, and what each one changes.
Answer
textContent replaces an element's text; createElement + append builds and inserts a brand-new
node; classList (add/remove/toggle) changes which CSS classes -- and therefore which styles
-- apply, without touching the element's structure or text at all.
Q14 (co-14 -- event-handling). What object does an event handler receive as its argument, and name one property on it that identifies which element the event actually happened on.
Answer
The handler receives an Event object (or a more specific subtype, like MouseEvent); its
target property identifies the exact element the event originated from -- distinct from this /
currentTarget, which identifies the element the LISTENER is attached to.
Q15 (co-15 -- event-propagation). A click listener sits on a ul; the user clicks one of its
li children. Does the ul's listener fire, and what's this pattern called?
Answer
Yes -- the click event bubbles up the ancestor chain from the li to the ul (and beyond), so a
single listener on the parent fires for a click on any descendant. This pattern is called event
delegation, and it means one listener can handle many current AND future children without
individually binding each one.
Q16 (co-16 -- default-action-control). What's the difference between calling preventDefault()
and calling stopPropagation() inside the same click handler?
Answer
preventDefault() suppresses the browser's own built-in behavior for that event (like following a
link's href or submitting a form) but does NOT stop the event from continuing to bubble.
stopPropagation() does the opposite -- it halts the event from reaching any ancestor listeners, but
does nothing to the browser's default action. They control two independent things.
Q17 (co-17 -- event-loop). console.log("A"), then Promise.resolve().then(() => console.log("B")),
then setTimeout(() => console.log("C"), 0), then console.log("D") all run in that order. In what
order do "A", "B", "C", "D" actually print?
Answer
A, D, B, C. The synchronous code (A, D) runs first, in order. Once the call stack is empty, all
queued MICROtasks (the Promise.then callback, B) run before the event loop even looks at the
macrotask queue that setTimeout schedules into (C) -- microtasks always drain fully before the
next macrotask, even a setTimeout(fn, 0).
Q18 (co-18 -- ui-as-function-of-state). In the render-from-state pattern this topic teaches, what is the ONE thing allowed to directly change the DOM, and what is everything else allowed to change instead?
Answer
Only the render function is allowed to directly touch the DOM. Everything else (event handlers, data loaders, business logic) is only allowed to change STATE and then call render again -- the DOM is always a pure, derived reflection of the current state, never mutated ad hoc from scattered places.
Q19 (co-19 -- component-props). A Greeting(props) function is called twice with different
props.name values. What determines the two calls' output, and can the component itself change
what props it received?
Answer
The output is entirely determined by the props argument each call receives -- same shape of
function, different input, different output, exactly like any pure function. The component cannot
change the props object it was handed; props flow one way, parent to child, and reassigning a
local copy inside the child never reaches back up to the parent's own data.
Q20 (co-20 -- component-state). What two things does a minimal interactive component need beyond a plain render function, to behave like a counter that increments on click?
Answer
Some mutable state living OUTSIDE the render function (so it survives across re-renders, e.g. a closed-over variable) and an event handler that updates that state and then calls render again -- render alone, with no state and no handler, can only ever produce the same static output every time.
Q21 (co-21 -- list-rendering). An array of five items is mapped to five li elements. What does
"keying" a rendered list mean, and what problem does it solve on a re-render?
Answer
Keying means giving each rendered item a stable identifier (e.g. an id-derived DOM id, or in
framework contexts a key prop) tied to the underlying data item, not its array index. On a
re-render, that stable identity lets the renderer recognize "this is the same logical item, just
possibly reordered or updated" instead of treating every re-render as a full tear-down and rebuild.
Q22 (co-22 -- forms-controlled-input). What makes a text input "controlled," and what are the two halves of the round trip that keep it that way?
Answer
A controlled input's displayed value comes FROM state (state -> input, on every render), and its
input event WRITES that new value back into state (input -> state, on every keystroke) --
state and the on-screen value never drift apart, because both directions of the round trip are
wired explicitly.
Q23 (co-23 -- form-validation). Name three attributes/methods from the Constraint Validation API covered in this topic, and what each one is responsible for.
Answer
required marks a field as mandatory; pattern supplies a regex the value must match;
checkValidity() runs all of an input's active constraints and returns whether it currently passes;
setCustomValidity(message) lets code report a custom failure reason beyond what the built-in
constraints alone can express.
Q24 (co-24 -- accessible-forms). An input has no visible label text next to it, only a placeholder. Why does that fail accessibility, and what's the fix?
Answer
A placeholder is not an accessible name -- it disappears the moment the user starts typing, and many
assistive technologies don't reliably announce it as a label at all. The fix is a real <label for="..."> (or aria-label/aria-labelledby) tied to the input's id, giving it a persistent,
programmatically-associated accessible name.
Q25 (co-25 -- aria-roles-semantics). A <div> is given role="button" and a click handler. What
does the role attribute alone accomplish, and what does it NOT accomplish?
Answer
role="button" exposes the button semantic to the accessibility tree, so assistive tech announces
it as a button. It does NOT make the element keyboard-focusable or keyboard-operable on its own --
those require an explicit tabindex="0" and manual Enter/Space key handling, none of which a real
<button> element would need in the first place.
Q26 (co-26 -- keyboard-navigation). A dialog opens and traps focus inside it. What should happen when the user presses Tab on the last focusable element inside the dialog?
Answer
Focus should cycle back to the FIRST focusable element inside the dialog, not escape to whatever comes next in the underlying page's tab order. A focus trap deliberately makes the dialog's focusable elements the entire tab cycle for as long as it's open.
Q27 (co-27 -- discriminated-union-states). A TaskListState type has four tagged variants:
loading, error, empty, loaded. What does "exhaustive" mean when a switch handles this type,
and what tool catches a missed case?
Answer
Exhaustive means every possible tag/variant has its own explicit case, with no variant left
unhandled. A switch's default branch typed as never is what catches a missed case: if a new
variant is later added to the union but no new case is written for it, TypeScript's tsc reports
a type error at the never-typed default, since that branch is no longer provably unreachable.
Q28 (co-28 -- typing-props-state). A component's props interface declares label: string, but a
caller passes label={42}. At what point is this mismatch caught, and what would catch it if the
props interface didn't exist?
Answer
tsc catches it at compile time, before the component ever renders -- a type error against the
declared label: string shape. Without a typed props interface, nothing catches it until runtime
(if ever) -- the component would simply receive a number where it expected a string and behave
however that mismatch happens to play out, silently or not.
Applied problems
Ten scenarios spanning beginner through advanced material. Each describes a realistic engineering situation without naming the specific technique -- decide what applies and why, then check your reasoning against the worked solution.
AP1. A teammate's page passes an automated HTML validator but a screen-reader user still reports "I can't tell what's the navigation and what's the main content." What's the most likely root cause?
Worked solution
The page probably uses generic divs styled to LOOK like a nav bar and a main content area, instead
of real semantic landmarks (<nav>, <main>). An HTML validator only checks that markup is
well-formed, not that it's semantically meaningful -- passing validation says nothing about whether
assistive tech can identify the page's structure (co-02).
AP2. Two CSS rules both target the same button: one written as .btn { color: blue; } earlier in
the file, another as .primary-btn { color: red; } later, and the button has both classes. Which
color renders, and why might a teammate be surprised by the answer?
Worked solution
Red renders -- both selectors are single-class selectors of EQUAL specificity, so the tiebreaker is
source order, and .primary-btn appears later. A teammate expecting the more "specific-sounding"
class name (primary-btn) to always win might be surprised that specificity is measured
structurally (selector type/count), not by how semantically specific a class name reads (co-05).
AP3. A design system wants one place to change an entire app's accent color, including inside a dark-mode variant that overrides just that one value. What CSS mechanism fits, and why not just find-and-replace every hardcoded color?
Worked solution
CSS custom properties (--accent at :root, referenced everywhere via var(--accent), then
re-declared inside a .dark scope). Find-and-replace is brittle and fully static -- it can't express
"this value is different depending on which scope/mode is active," while a custom property resolves
per-element through the cascade, live, with zero JavaScript (co-06, co-11).
AP4. A card component's declared width: 200px renders WIDER than expected once padding and a
border are added, breaking a grid layout that assumed exact 200px cards. What's the fix, without
changing the declared width value at all?
Worked solution
Add box-sizing: border-box to the card. Under the default content-box model, padding and border
ADD to the declared width, growing the rendered box beyond 200px. border-box fixes exactly that by
folding padding and border back inside the declared width, so offsetWidth stays exactly 200px
(co-07).
AP5. A three-column layout needs to become a single stacked column on narrow screens, with zero JavaScript and no separate mobile-only markup. What CSS feature combination handles this cleanly?
Worked solution
A flex or grid container for the desktop layout, combined with a @media (max-width: ...) rule that
switches flex-direction/grid-template-columns to a single-column arrangement below the
breakpoint. The SAME markup serves every viewport width; only the CSS layout mechanism responds
(co-09 or co-10, plus co-11).
AP6. A dynamically-rendered list re-renders every 2 seconds from fresh server data, and a developer complains that a text input INSIDE one of the list items keeps losing focus and its typed text on every refresh, even though that item's other data hasn't changed. What's the most likely missing piece?
Worked solution
The list is probably not stably keyed -- without a key tied to each item's own identity, a naive re-render can tear down and rebuild every node from scratch on each pass, destroying focus and in-progress input state even for items whose underlying data is unchanged. Stable keys let the renderer recognize "same logical item" and preserve its live DOM node instead (co-21).
AP7. A form's Add button is a <div role="button" tabindex="0"> with a click listener only.
QA reports it works fine with a mouse but does nothing when a keyboard-only user presses Enter on
it while it's focused. What's missing, and is this a framework bug?
Worked solution
Not a framework bug -- role="button" and tabindex="0" only expose the role and make the element
focusable; they do NOT wire up the actual Enter/Space keyboard activation a real <button> gets for
free. The fix is either to add explicit keydown handling for Enter/Space, or -- far simpler and
more robust -- replace the div with a real <button> element (co-25, co-26).
AP8. A component fetches data on mount and needs to show a spinner, an error message, an empty-state message, or the actual list -- and a bug report says two of those UI states have both rendered on screen simultaneously after a slow request. What class of bug is this, and what pattern prevents it structurally?
Worked solution
This is a classic symptom of tracking loading/error/data as SEPARATE independent boolean/nullable
flags (isLoading, error, data) that can drift into an invalid combination (e.g. both
isLoading: true and data: [...] set at once). A discriminated union with exactly one active tag
at a time ({status: "loading"} | {status: "error", message} | ...) makes that invalid combination
structurally unrepresentable -- there is no state value where two branches could both be true
(co-27).
AP9. A teammate insists a component's props object can be safely mutated inside the component
"since JavaScript passes everything by reference anyway." Is this reasoning sound for a props object
containing only primitive fields (strings, numbers, booleans)?
Worked solution
The premise about reference semantics is correct for the props OBJECT itself, but reassigning one of its primitive-valued fields inside the child only changes the child's own local binding -- it does not write back into the parent's original object, since primitives are copied by value at the moment they're read out of the object. The one-way flow is real regardless of whether the field is a primitive or a nested object; mutating a NESTED object field would actually reach the parent, which is exactly why passing primitives (or making deliberate copies) is the safer default (co-19).
AP10. A switch over a four-variant discriminated union has three cases handled and a default: break; that silently does nothing for the fourth. Six months later, a fifth variant is added to the
union elsewhere in the codebase. What happens, and how would changing that default case to be
typed never have caught the gap immediately?
Worked solution
With a bare default: break;, the fifth variant silently falls into the same do-nothing branch as
the original unhandled fourth -- no error, no warning, just a UI that quietly does the wrong thing
for two variants instead of one. A default branch that assigns the switched value to a
never-typed variable instead makes the compiler prove every variant is exhausted: adding a fifth
variant without a matching new case breaks that proof, and tsc reports a type error at the
never assignment the moment the new variant exists -- turning a silent runtime gap into an
immediate compile-time failure (co-27, co-28).
Code katas
Six hands-on drills, spanning beginner tools through advanced component work. Each is a self-contained, runnable HTML/CSS/JS task with a scenario distinct from the learning track's 80 worked examples -- no live third-party host is ever required for this topic's material, since every worked example and every kata here is fully self-contained HTML/CSS/JS opened directly from disk, per DD-30: attempt the task yourself first, then compare against the reference approach and the genuinely captured output shown.
Kata 1 -- Fix a heading hierarchy that skips a level
Task. A page has <h1>Dashboard</h1> followed directly by <h3>Recent orders</h3> (skipping
h2). Without changing any visible text or styling, fix the heading levels so the outline is
correctly nested.
Reference approach: change the skipped-level heading to <h2>, preserving every other heading's
relative depth beneath it.
<h1>Dashboard</h1>
<h2>Recent orders</h2>
<!-- was <h3>, which skipped a level with nothing at h2 in between -->Key takeaway: A correct document outline requires each heading level to appear in order with no
gaps -- h1 to h3 with no h2 anywhere is an invalid outline even though it renders visually fine,
since visual size can be restyled with CSS independent of the semantic level (co-03).
Kata 2 -- Write a computeSpecificity() sketch for two rules, without a browser
Task. Given the two selectors #nav a.active and nav a.active.current, reason out (without
opening a browser) which one wins if both set a conflicting color, and why.
Reference approach: count each selector's ids, classes/attributes/pseudo-classes, and elements.
#nav a.active -> 1 id, 1 class, 1 element -> (1, 1, 1)
nav a.active.current -> 0 ids, 2 classes, 2 elements -> (0, 2, 2)Key takeaway: #nav a.active wins -- specificity compares id-count first, and one id already
outranks any number of classes or elements; the second selector's extra class and element count
never even enter the comparison (co-05).
Kata 3 -- Build a render(state) function for a traffic light, from scratch
Task. Without looking at Example 43, write a pure render(root: HTMLElement, color: "red" | "yellow" | "green") function that rebuilds a single div inside root with a background color
matching color.
Reference approach:
function render(root, color) {
root.innerHTML = "";
const light = document.createElement("div");
light.style.width = "40px";
light.style.height = "40px";
light.style.borderRadius = "50%";
light.style.background = color;
root.append(light);
}Key takeaway: The entire "UI as a function of state" idea is captured in this one small function's shape -- clear what was there before, derive fresh output purely from the input argument, never read or write any outside state (co-18).
Kata 4 -- Add keyboard support to a custom disclosure toggle
Task. Starting from a <div role="button" tabindex="0"> that already toggles a panel's
visibility on click, add the missing keyboard handling so Enter and Space also activate it.
Reference approach:
toggleButton.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault(); // stop Space from also scrolling the page
toggleButton.click(); // reuse the existing click handler's logic
}
});Key takeaway: role="button" and tabindex="0" get you exposed semantics and focusability for
free, but keyboard ACTIVATION is never automatic for a non-native element -- it must be wired by
hand, one more reason a real <button> (which needs none of this) is usually the simpler choice
(co-25, co-26).
Kata 5 -- Model a three-variant fetch state as a discriminated union
Task. Without reusing Example 65-67's exact shape, design your own FetchState<T> discriminated
union covering idle, pending, and done (holding a T payload), and write the exhaustive
switch with a never-typed default.
Reference approach:
type FetchState<T> = { kind: "idle" } | { kind: "pending" } | { kind: "done"; value: T };
function describe<T>(state: FetchState<T>): string {
switch (state.kind) {
case "idle":
return "not started";
case "pending":
return "in flight";
case "done":
return `got: ${JSON.stringify(state.value)}`;
default: {
const exhaustive: never = state;
return exhaustive;
}
}
}Key takeaway: The exhaustiveness technique (a never-typed default) is completely general --
it applies to any tagged union with any variant names or field shapes, not just the specific
loading/error/empty/loaded union this topic's worked examples happened to use (co-27, co-28).
Kata 6 -- Type a component's props and catch a mismatched caller
Task. Write a Badge props interface with label: string and count: number, then write one
correct call and one call passing count as a string literal, and predict tsc's output for the
bad call before running it.
Reference approach:
interface BadgeProps {
label: string;
count: number;
}
function Badge(props: BadgeProps): string {
return `${props.label}: ${props.count}`;
}
Badge({ label: "Open", count: 3 }); // fine
Badge({ label: "Open", count: "3" }); // predict the tsc error before runningOutput (genuinely captured, via tsc --noEmit --strict --skipLibCheck):
error TS2322: Type 'string' is not assignable to type 'number'.Key takeaway: A typed props interface turns a caller's shape mistake into a compile-time
TS2322 the moment it's written, in an editor or in CI -- with no props interface at all, the same
mistake (a string where a number was expected) would only surface however the component happens to
misbehave at runtime, if it's ever even noticed (co-28).
Self-check checklist
Confirm each item without checking the learning track first. If you hesitate, that concept needs another pass.
- I can name the minimum pieces a valid HTML5 document needs and what the viewport meta tag controls. (co-01)
- I can explain why a semantic landmark element communicates more than a visually identical
div. (co-02) - I can explain why
alttext matters even when a filename looks descriptive. (co-03) - I can combine an attribute selector and a pseudo-class into one selector, from memory. (co-04)
- I can explain why an id selector beats a class selector regardless of source order. (co-05)
- I can explain how a custom property redefined inside a nested scope changes what descendants resolve to. (co-06)
- I can compute an element's rendered width under both the default and
border-boxbox models. (co-07) - I can explain the difference between
display: noneandvisibility: hiddenin terms of layout participation. (co-08) - I can explain what
flex-grow: 1does to a flex child's share of leftover space. (co-09) - I can explain the difference between
grid-template-columnsandgrid-template-areas. (co-10) - I can write a media query that changes a layout below a given viewport width, with zero JavaScript. (co-11)
- I can explain why
querySelectorAll's returned NodeList does not grow when new matching elements are added later. (co-12) - I can name three DOM APIs that mutate rendered content and what each one changes. (co-13)
- I can explain the difference between an event's
targetand the element a listener is attached to. (co-14) - I can explain event bubbling and why it enables delegation. (co-15)
- I can explain the difference between
preventDefault()andstopPropagation(). (co-16) - I can order
console.logcalls, aPromise.then, and asetTimeout(fn, 0)correctly by when each actually runs. (co-17) - I can state, in one sentence, what "UI as a function of state" means and what it forbids. (co-18)
- I can explain why reassigning a prop inside a child component never changes the parent's own data. (co-19)
- I can name the two ingredients a minimal interactive (stateful) component needs beyond a plain render function. (co-20)
- I can explain what "keying" a rendered list means and the problem it solves on re-render. (co-21)
- I can explain both directions of a controlled input's round trip between state and the DOM. (co-22)
- I can name at least three pieces of the Constraint Validation API and what each is responsible for. (co-23)
- I can explain why a placeholder alone is not an accessible label. (co-24)
- I can explain what
role="button"accomplishes and what it does NOT accomplish on its own. (co-25) - I can explain what should happen when Tab is pressed on the last focusable element inside a focus trap. (co-26)
- I can explain what "exhaustive" means for a switch over a discriminated union and name the tool that catches a missed case. (co-27)
- I can explain when a typed-props mismatch is caught with a props interface present, versus without one. (co-28)
← Previous: Capstone
Last updated July 14, 2026