You Might Also Like
CSS Selector Challenge Game — Free HTML CSS JS Snippet
CSS Selector Challenge Game · Games · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
CSS Selector Challenge Game — Live querySelectorAll Evaluation, Set-Equality Grading & Progressive Levels

Most "learn CSS selectors" widgets check the player's answer against a stored string, which means a correct selector written a slightly different way is marked wrong and the whole exercise teaches memorisation rather than understanding. This snippet does the opposite: it runs the player's typed selector through the browser's own querySelectorAll against a small live DOM stage and compares the resulting element set against the set the target selector produces. Any selector that matches exactly the right elements is accepted, so a player who reaches the answer by a different valid route is rewarded rather than punished — which is the behaviour that actually builds selector intuition.
Evaluating the player's selector safely against a scoped stage
Every lookup runs as stage.querySelectorAll(selector) rather than document.querySelectorAll(selector), so a broad answer like * or div can only ever match elements inside the puzzle stage and never reaches the surrounding game chrome, the input, or the score display. The call sits inside a try/catch because a partially typed selector such as li:nth-child( throws a SyntaxError — the catch returns null, which the caller treats as "not valid yet" rather than "matches nothing", letting the input show a red border while the player is mid-keystroke without ever printing a console error.
Set equality instead of string comparison
sameSet(a, b) compares two arrays of DOM element references by length and membership: same count, and every element in the player's result also present in the target result. Because the comparison is on element identity rather than on selector text, .sold.seasonal, li.seasonal.sold and any other syntactically different selector that resolves to the same elements all grade as correct. This is the single design decision that separates a genuine selector trainer from a spelling test.
Feedback that names the failure mode
When the sets differ, the game does not simply say "wrong". It compares the two lengths and reports whether the answer was too broad (matched more elements than the target) or too narrow (matched fewer), then highlights both sets simultaneously — the player's matches with a solid .hit glow, the intended targets with a dashed .want outline. Seeing an over-matching selector light up two extra rows is far more instructive than a binary verdict, because it makes the specific over-reach visible in the same place the player is looking.
Live preview on every keystroke
An input listener re-runs the match and re-applies the .hit class as the player types, so the selector is evaluated continuously rather than only on submit. Highlights are cleared first by removing the classes from all previously marked elements, which keeps the highlight state derived purely from the current input rather than accumulating stale marks. Because querySelectorAll on a stage of a dozen elements is effectively instant, no debounce is needed — the feedback loop is tight enough that players discover how a selector behaves before they commit to it.
Level data as a single array
Each level is one object holding a plain-English goal, the markup string injected into the stage, the reference answer selector, and a hint. Adding a level is one array entry and no code change, and the same markup string is used twice — once as live DOM via innerHTML and once as readable source in the <pre> code panel — so what the player reads and what the selector runs against can never drift apart. The seven levels ramp from a bare type selector through classes, ids, chained classes, attribute selectors, :nth-child() and :not(), which is the practical core of day-to-day selector work.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet into an AI assistant like Claude and ask it to add a timed speedrun mode that scores each level on how few characters the accepted selector used, which turns the game into a lesson about selector economy rather than just correctness. Other natural extensions: add combinator levels (descendant, child, adjacent sibling and general sibling) with markup nested deeply enough that the difference actually matters, add a "specificity score" readout next to each accepted answer so players see the cost of the route they chose, or invert the game so it shows a selector and asks the player to click the elements it matches. Each builds directly on the existing set-equality grading rather than replacing it.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a playable CSS selector challenge game in plain HTML, CSS, and JavaScript — no frameworks or libraries.
Requirements:
- A levels array where each level is a data object with a plain-English goal, an HTML markup string for the puzzle stage, a reference answer selector, and a one-line hint. Adding a level must require no code changes.
- Render each level's markup into a stage container AND show the same markup string as readable source in a code panel, so the player can read exactly what they are selecting against.
- Evaluate the player's typed selector with stage.querySelectorAll() scoped to the stage element only — never document-wide — so a broad selector like * cannot touch the surrounding game UI.
- Wrap the evaluation in try/catch: an incomplete selector such as li:nth-child( throws a SyntaxError, which must be surfaced as an invalid-input state (red border) rather than an uncaught error.
- Grade by set equality, not string comparison: collect the elements the player's selector matches and the elements the reference answer matches, and accept the answer when the two sets contain exactly the same elements. Any differently-written but equivalent selector must pass.
- Highlight matched elements live on every keystroke, and on a wrong submission report whether the answer was too broad or too narrow with both counts, glowing the player's matches while outlining the intended targets in a distinct style.
- Include at least seven levels ramping from a bare type selector through class, id, chained classes, attribute selectors, :nth-child() and :not(), plus a hint button, a skip button, and a solved counter.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Read the goal and the markupThe indigo-bordered line states the target in plain English, and the dark code panel below the stage shows the exact HTML your selector will run against — the same string that was injected into the live preview above it.
- 2Type a selector and watch it match liveAs you type, every element the selector currently matches lights up with an indigo glow. A partially typed selector like li:nth-child( turns the input border red instead of throwing — the syntax error is caught and reported as "not valid yet".
- 3Press Enter or click CheckThe game compares the elements your selector matched with the elements the reference answer matches. Any selector resolving to exactly the right set is accepted, so there is no single "official" spelling you have to guess.
- 4Read the miss diagnosisA wrong answer reports whether you were too broad or too narrow with the counts, glows your matches, and outlines the intended targets with a dashed amber border so you can see exactly which elements you over- or under-reached.
- 5Use a hint if you are stuckShow hint writes the level's one-line explanation of the technique into the status row — for example that :nth-child(n) counts position among siblings starting at 1 — without revealing the answer itself.
- 6Clear all seven levelsLevels ramp from a bare type selector through class, id, chained-class, attribute, :nth-child() and :not() selectors. Solving the last one shows a completion message and restarts the game from level 1 with a fresh score.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
No. It runs your selector with stage.querySelectorAll() and compares the resulting element set with the set the reference answer produces, using sameSet() to check identical length and membership. Any valid selector matching exactly those elements is accepted — on the chained-class level, .sold.seasonal and li.seasonal.sold both pass because they resolve to the same element.
Every lookup is scoped to the puzzle stage element rather than the document, so querySelectorAll can only return descendants of that container. Typing * highlights every element inside the stage and nothing else — the score display, the input, and the code panel all sit outside it and are unreachable.
A partially typed selector such as li:nth-child( is invalid CSS and makes querySelectorAll throw a SyntaxError. The match() helper wraps the call in try/catch and returns null on failure, which the preview treats as "not valid yet" — showing a red border rather than clearing your highlights or logging an error.
Add an object to the LEVELS array with four keys: goal (plain-English instruction), markup (an HTML string, with newlines escaped), answer (a reference selector that matches the intended elements), and hint (a one-line explanation). No other code changes are needed — the same markup string is used for both the live stage and the readable code panel.
Yes. Move the LEVELS array and the match/sameSet helpers into a module, hold index, solved and the input value in component state, and keep a ref to the stage element so querySelectorAll stays scoped to it. In React set the stage content with dangerouslySetInnerHTML from the level markup and run highlighting inside a useEffect keyed on the input value; in Vue use v-html with a watcher; in Angular use [innerHTML] with an ElementRef and ngAfterViewInit for the initial render.