For supporters
Source Code
Want the source code?
Play the game for free. Become a WebDevPuneet supporter to unlock the complete HTML, CSS and JavaScript source code, the exports (HTML, React, Vue, Angular, Tailwind) and Fork.
Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Flexbox Alignment Game — Ghost-Layer Targets, Position-Based Win Detection & Live Generated CSS

Flexbox is learned by watching things move, not by reading property tables, and the fastest way to build that intuition is a tight loop: change one value, see the layout react immediately, and know instantly whether the result is right. This snippet is a playable flexbox trainer built around exactly that loop — a stage with dashed target slots, a row of chips for flex-direction, justify-content and align-items, and balls that animate to their new positions the moment a value changes, with the equivalent CSS rule printed live underneath.
Targets drawn by flexbox itself, not by hardcoded coordinates
The stage holds two absolutely positioned layers of identical size. The lower one, the ghost layer, is a flex container filled with dashed .fx-slot circles and given the level's solution styles; the upper one holds the real .fx-ball elements and receives whatever the player has selected. Because the target positions are produced by applying real flex properties to a real container rather than by storing pixel coordinates, they stay correct at every stage width, on every device, and after any resize — there is no coordinate table to fall out of sync with the CSS. Adding a level means writing one solution object, not measuring anything.
Winning is positional, so alternative routes are accepted
isSolved() never compares the player's chosen property values against the level's solution object. It measures both layers with getBoundingClientRect(), reduces each element to its centre point, and requires every ball centre to sit within a six-pixel tolerance of some slot centre. That means any combination of properties that genuinely lands the balls on their targets counts as a win — which is the honest definition of "did you solve the layout" and prevents the game from teaching one memorised answer per level. The tolerance exists because flex distribution produces sub-pixel fractional positions that will not compare exactly equal, and because a strict equality check would fail intermittently at certain container widths.
Measuring at the right moment
Setting justifyContent on the live layer changes layout synchronously, but the check runs inside a requestAnimationFrame callback so measurement happens after the browser has settled the new layout rather than in the middle of the same task that mutated it. The balls animate to their new positions with a springy cubic-bezier transition, but the check deliberately does not wait for that animation: getBoundingClientRect() on a transitioning element returns its live interpolated box, so waiting for the transition would only delay the verdict, while measuring the settled layout box immediately is both correct and instant.
Generated CSS as the real reward
Every change re-renders a code panel containing the exact rule the player has constructed — display: flex plus the three chosen properties, formatted as copy-ready CSS. This is what converts play into transferable knowledge: the player ends each level looking at the declaration block that produced the layout they just built, in the same syntax they will type into a real stylesheet.
State kept in one object, chips derived from it
A single state object holds the three current values, and syncChips() re-derives which chip in each group is active by comparing chip.dataset.value against state[prop]. Nothing tracks selection independently in the DOM, so the visual state of the controls cannot drift from the styles actually applied to the layer — the same one-way data-flow discipline a framework would enforce, implemented in a dozen lines of vanilla JavaScript. A move counter increments only when a chip changes the value rather than on every click, so re-clicking the already-active option costs nothing.
Step by step
How to Use
- 1Read the goal and find the dashed slotsEach level states its objective in plain English and draws dashed circles where the balls need to end up. Those slots are positioned by real flex properties on a hidden ghost layer, so they sit correctly at any stage size.
- 2Pick a flex-directionSwitching between row and column changes which axis is the main axis — and therefore what justify-content and align-items each control. Levels 4 through 6 only solve in column mode, which is the fastest way to internalise the axis swap.
- 3Choose justify-content and align-itemsEvery chip applies its value to the live layer immediately and the balls animate to their new positions with a spring transition, so you see the effect of a single property change in isolation.
- 4Read the generated CSSThe dark code panel always shows the exact rule you have built — display: flex plus your three current values — formatted as a copy-ready declaration block, so the layout and the syntax stay connected.
- 5Land every ball to advanceThe win check measures both layers with getBoundingClientRect() and requires each ball centre within six pixels of a slot centre. Any property combination that genuinely lands them counts, and the balls turn green when the level is solved.
- 6Watch your move countThe counter increments only when a chip actually changes a value, not on every click. Clearing all six levels in as few moves as possible is the natural replay goal once you know the properties.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Where they end up. isSolved() measures both layers with getBoundingClientRect(), reduces every element to its centre, and requires each ball centre within six pixels of a slot centre. The level solution object is used only to style the ghost targets, never compared against your selections — so any combination that lands the balls correctly is accepted.
Flex distribution routinely produces fractional pixel positions (a container width that does not divide evenly across items, for example), so ball and slot centres would rarely be exactly equal even when the layout is visually identical. A small tolerance absorbs that sub-pixel noise while still being far tighter than the gap between any two distinct alignment values.
Setting a style property and reading geometry in the same task can measure a layout the browser has not finished settling. Deferring the read to the next animation frame guarantees the new layout is in place. Note the check does not wait for the CSS transition to finish — getBoundingClientRect() returns the interpolated box mid-transition, so waiting would only delay the verdict without changing it.
Push an object onto the LEVELS array with three keys: balls (how many items), goal (the plain-English instruction), and solution (an object with flexDirection, justifyContent and alignItems). The ghost layer is styled with that solution object, so the dashed targets position themselves — you never need to compute or store coordinates.
Yes. Keep the LEVELS array in a module, hold the three property values in component state, and bind them straight to the live layer's style object so the framework handles the style updates. Put the getBoundingClientRect() comparison in an effect that runs after the style change commits — useEffect in React, watch plus nextTick in Vue, ngAfterViewChecked or a signal effect in Angular — using refs to both layers rather than getElementById, and remember to remove the resize listener on unmount.