Pokémon Browser Game: Filter the Pokédex with JavaScript
Outcome
Section titled “Outcome”Add a type filter to the completed Pokédex. A visitor will choose one type from a native select control, and JavaScript will show the matching entries, hide the other entries, and report the visible count without reloading the page.
The eight entries will remain in pokedex.html. If JavaScript is unavailable or fails before initialization, the filter controls will stay hidden and the complete static catalog will remain available.
What you will practice
- Connect pokedex.html to one deferred external JavaScript file.
- Select one control, one status region, and a collection of DOM elements.
- Read a selected value after a change event.
- Store filter metadata in data-* attributes without changing the visible catalog facts.
- Loop through the entries and update each hidden property from one comparison.
- Derive and report the visible result count instead of storing a second count.
- Keep the complete catalog available when JavaScript is disabled.
Why this interaction comes before the game
Section titled “Why this interaction comes before the game”This filter uses the same browser relationship that the game will use later:
- A visitor triggers an event.
- JavaScript reads the current interface value.
- A function decides which result matches.
- JavaScript updates the DOM.
- The page reports the visible result.
The catalog has no map, battle, random encounter, or changing game record. That smaller scope lets you inspect the event and DOM update directly.
What is new and what is reused
Section titled “What is new and what is reused”- New: A deferred
pokedex.jsfile,querySelectorAll, aNodeList, afor...ofloop,data-*attributes,dataset,split,includes, thechangeevent, thehiddenproperty, and a derived visible count. - Reused: The complete static catalog, the shared page shell, native labels and form controls, strings, variables, conditions, functions,
querySelector, missing-element guards,addEventListener,textContent, the Console, keyboard checks, and Git checkpoints.
Before you begin, complete Write JavaScript for a web page. Keep Respond to user input with DOM events open for the DOM selection, native select, event-listener, and status-update patterns.
Starting point
Before you start
- The static Pokédex article passes its complete self-check.
- pokedex.html contains exactly eight complete list items with the class pokedex-entry.
- Every entry remains available at narrow and wide widths and with JavaScript disabled.
- You completed the JavaScript foundations lesson and can connect an external script, read strings, use const and let, call a function, and trace a condition.
- The repository contains the verified static-catalog checkpoint, and the working tree has no unexplained changes.
- Current state
- The Pokédex presents all eight entries on every load. It has no filter control or JavaScript file.
- First action
- Open pokedex.html and add the hidden filter region from the first step immediately before the existing catalog list.
- First checkpoint
- pokedex.html loads pokedex.js with defer, and the Console shows one temporary connection message without an error.
- Help trigger
- Use the nearest recovery note or ask for help if the filter appears while JavaScript is disabled, a selector returns null, the NodeList is empty, one entry has no data-types value, a filter hides the wrong entries, the visible count disagrees with the page, or keyboard focus leaves the select after a change.
Required result
Section titled “Required result”Complete this article when:
pokedex.htmlloadspokedex.jswithdefer;- the filter uses a connected label and native
selectcontrol; - the first option shows all types, and the remaining options match the types in the student’s catalog;
- the filter region starts with the HTML
hiddenattribute and JavaScript reveals it only after successful initialization; - every
.pokedex-entryhas onedata-typesattribute with normalized lowercase type values separated by one space; pokedex.jsselects the filter region, select control, status region, and all Pokédex entries;- a missing-element guard stops the script with a specific Console error when the required DOM contract is incomplete;
- one named function reads the selected value, checks every entry, updates each
hiddenproperty, and derives the visible count; - one
changelistener calls that function without reloading the page; - initialization restores the known All types starting value before the controls appear;
- the status region reports Showing X of 8 Pokémon. after the initial render and every filter change;
- the filter works with pointer and keyboard input without moving focus away from the native select;
- selecting All types restores all eight entries;
- disabling JavaScript hides the inactive controls and leaves all eight static entries available;
- no evolution data, API request, persistence, search field, sorting control, or game state is added; and
- the page passes HTML, CSS, JavaScript, keyboard, narrow-width, zoom, and Console checks.
1. Add a filter region that starts hidden
Section titled “1. Add a filter region that starts hidden”Open pokedex.html. Immediately before the existing catalog list, add this filter region:
<div id="pokedex-filter" hidden> <label for="type-filter">Filter by type</label> <select id="type-filter" name="typeFilter"> <option value="all">All types</option> <option value="grass">Grass</option> <option value="fire">Fire</option> <option value="water">Water</option> <option value="electric">Electric</option> <option value="ghost">Ghost</option> </select> <p id="filter-status" role="status">Showing all 8 Pokémon.</p></div>The visible option text uses normal capitalization. Each value uses a normalized lowercase string that JavaScript can compare with entry metadata.
If you selected different Pokémon, keep All types and replace the other options with the types that appear in your catalog. Each option value must be lowercase and must match the later data-types value exactly.
The hidden attribute is deliberate. Before JavaScript runs, the select cannot change the page. Hiding that inactive control prevents the interface from promising behavior that is unavailable. The eight catalog entries remain visible because this attribute applies only to the filter region.
The status paragraph uses role="status". Supporting assistive technologies can announce later text changes without JavaScript moving keyboard focus.
Save and reload with JavaScript still absent. All eight entries must appear, and the filter controls must not appear.
Inspect the DOM in DevTools. Confirm that the filter region exists immediately before the list and still has its hidden attribute.
If it does not work
Section titled “If it does not work”If the entries disappear, confirm that hidden belongs to the filter div, not the catalog list or a shared parent. If the filter appears, check the exact spelling of the hidden attribute and inspect whether a CSS rule overrides [hidden].
2. Give every entry filter metadata
Section titled “2. Give every entry filter metadata”Add one data-types attribute to each list item that has the pokedex-entry class. Use one lowercase token for each visible type and separate multiple tokens with one space.
The guided Snivy entry begins like this:
<li class="pokedex-entry" data-types="grass"> <!-- Keep the complete Snivy entry from the previous article here. --></li>A project entry with two types would use this pattern:
<li class="pokedex-entry" data-types="water ground"> <!-- Keep the complete entry content here. --></li>data-types is machine-readable metadata attached to the same DOM element that the script will hide. It does not replace the visible type text. A visitor must still be able to read the type without JavaScript or developer tools.
For the guided set, use this contract:
| Entry | data-types value |
Expected matching filter |
|---|---|---|
| Snivy | grass |
Grass |
| Fuecoco | fire |
Fire |
| Totodile | water |
Water |
| Bounsweet | grass |
Grass |
| Growlithe | fire |
Fire |
| Poliwag | water |
Water |
| Mareep | electric |
Electric |
| Misdreavus | ghost |
Ghost |
Select each .pokedex-entry in DevTools. Confirm that:
- exactly eight elements match the class;
- every matching element has
data-types; - every value uses lowercase tokens;
- every metadata value agrees with the visible type text; and
- the attribute is on the list item, not only on a nested heading or label.
If it does not work
Section titled “If it does not work”If DevTools reports fewer than eight matching elements, find the entry with the missing or misspelled class before you write JavaScript. If a type uses inconsistent values such as Fire, fire-type, and fire, normalize them all to fire.
Checkpoint: The filter has a stable HTML contract
- What now works
- The inactive filter region is hidden, all eight entries remain visible, and every .pokedex-entry has lowercase type metadata that agrees with its visible content.
- Files changed
pokedex.html- What remains
- Connect a deferred JavaScript file and confirm that the browser runs it.
- Next action
- Create pokedex.js beside the HTML files and add one temporary Console message.
- If it does not work
- Disable the filter markup and compare each list item with the first passing entry. Restore eight visible entries before connecting the script.
3. Connect pokedex.js
Section titled “3. Connect pokedex.js”Create pokedex.js in the project root beside pokedex.html.
Add this initial code:
"use strict";
console.log("Pokédex JavaScript is connected.");In the head of pokedex.html, add this script element after the shared stylesheet link:
<script src="pokedex.js" defer></script>src points to the file beside pokedex.html. defer lets the browser parse the HTML before it runs the script, so the Pokédex elements exist when JavaScript selects them.
Use Connect JavaScript to the page for the complete loading model.
Open the Console and reload pokedex.html. Confirm that it shows Pokédex JavaScript is connected. once and shows no red error.
The filter controls must remain hidden. The script is connected, but it has not proved that the complete filter can initialize yet.
If it does not work
Section titled “If it does not work”- If the browser cannot find
pokedex.js, compare the scriptsrc, file name, letter case, and project location. - If the code appears as page text, confirm that the JavaScript is inside
pokedex.jsand the HTML contains ascriptelement rather than pasted JavaScript. - If the message appears twice after one reload, inspect
pokedex.htmlfor a duplicate script element.
Checkpoint: The external script runs
- What now works
- pokedex.html loads one deferred pokedex.js file, and one reload produces one exact connection message without a Console error.
- Files changed
pokedex.html, pokedex.js- What remains
- Select the stable DOM elements and stop with a useful error when the contract is incomplete.
- Next action
- Remove the temporary console.log call and add the four DOM selections after the strict-mode directive.
- If it does not work
- Restore only the strict-mode directive and temporary connection message. Correct the file connection before adding filter logic.
4. Select the filter and entries
Section titled “4. Select the filter and entries”Remove the temporary console.log call. Keep the strict-mode directive.
Add these selections:
"use strict";
const filterRegion = document.querySelector("#pokedex-filter");const typeFilter = document.querySelector("#type-filter");const filterStatus = document.querySelector("#filter-status");const pokemonEntries = document.querySelectorAll(".pokedex-entry");The first three calls use querySelector because each ID must identify one element. querySelectorAll returns a NodeList that contains every element matching .pokedex-entry in source order.
Add this guard after the selections:
if ( !filterRegion || !typeFilter || !filterStatus || pokemonEntries.length === 0) { throw new Error("Required Pokédex filter elements are missing.");}The guard checks both kinds of failure:
- a required single element is
null; or - the entry selector returns an empty
NodeList.
Read Connect HTML to JavaScript through the DOM for the relationship between the saved HTML, current DOM, selector strings, and selected objects.
Test the guard deliberately
Section titled “Test the guard deliberately”Temporarily change "#type-filter" to "#type-filter-test". Save and reload. The Console must show Required Pokédex filter elements are missing.
Restore "#type-filter", save, and reload. Confirm that the error disappears. The controls must remain hidden because initialization is not complete.
If it does not work
Section titled “If it does not work”If the Console names querySelector or querySelectorAll as undefined, confirm that both calls begin with document.. If the guard still throws after you restore the selector, compare every selector string with the matching HTML ID or class.
5. Read one filter value and update every entry
Section titled “5. Read one filter value and update every entry”Add this named function after the missing-elements guard:
function updatePokedex() { const selectedType = typeFilter.value; let visibleCount = 0;
for (const entry of pokemonEntries) { const entryTypes = entry.dataset.types.split(" "); const matchesType = selectedType === "all" || entryTypes.includes(selectedType);
entry.hidden = !matchesType;
if (matchesType) { visibleCount += 1; } }
filterStatus.textContent = `Showing ${visibleCount} of ${pokemonEntries.length} Pokémon.`;}Read the function in phases:
typeFilter.valuereads the stable lowercase value of the current option.visibleCountstarts at0for this update.for...ofis a loop. It visits each selected list item once and runs the same block for that item.entry.dataset.typesreads the element’sdata-typesstring.split(" ")creates an array containing each type token.- The comparison matches All types or one included type token.
- The
hiddenproperty becomes the opposite of the match result. - A matching entry increases the visible count by one.
textContentreports the derived count after the loop.
The function does not store a second list of visible Pokémon. It derives the current result from the selected value and the eight existing entries each time it runs.
The HTML hidden attribute and JavaScript hidden property represent the same browser state. When entry.hidden is true, the entry is not rendered and is removed from the accessibility tree. When it is false, the entry returns to its original source position.
Inspect one manual call
Section titled “Inspect one manual call”The function exists but does not run by itself. Temporarily add these lines after the function:
typeFilter.value = "grass";updatePokedex();Save and reload. The guided set should show only Snivy and Bounsweet. The filter region remains hidden, but DevTools shows hidden on the six nonmatching list items and the hidden status text contains Showing 2 of 8 Pokémon.
Remove both temporary lines and reload before continuing. All eight saved HTML entries must return.
If it does not work
Section titled “If it does not work”- If the Console says that
typesis undefined, one selected.pokedex-entrydoes not have adata-typesattribute. - If every entry becomes hidden, compare the selected option value with the lowercase metadata tokens.
- If empty spaces create unexpected tokens, keep one normal space between type values and remove leading or trailing spaces.
- If the page shows blank Grid positions, confirm that
.pokedex-entryselects and hides the list item that participates in the Grid.
Assistance 2 — Trace one Grass comparison
Set the selected value to grass. For Snivy, entry.dataset.types is grass, split(" ") produces an array containing grass, and includes("grass") returns true. entry.hidden therefore becomes false. Repeat the same trace for Fuecoco and confirm that its result becomes true for hidden.
Assistance 3 — Separate the filter into five subgoals
Read one selected value. Start one count. Visit one entry at a time. Decide and apply that entry’s hidden state. After the loop, write one count message. Confirm each subgoal with a temporary breakpoint or Console inspection before combining another change.
Checkpoint: One function controls the filtered DOM result
- What now works
- A temporary Grass update reads the selected type, checks all eight entries, shows the two guided matches, and derives the visible count without changing the saved HTML content.
- Files changed
pokedex.html, pokedex.js- What remains
- Connect the native change event, run the initial update, and reveal the proven filter controls.
- Next action
- Remove the temporary manual call, then register updatePokedex as the select's change listener.
- If it does not work
- Set the select back to All types and call the function once. Restore eight visible entries before connecting the event.
6. Connect the change event and reveal the filter
Section titled “6. Connect the change event and reveal the filter”Remove the temporary manual call from the previous step. Add these lines once at the end of pokedex.js:
typeFilter.value = "all";typeFilter.addEventListener("change", updatePokedex);updatePokedex();filterRegion.hidden = false;The order matters:
- The first line restores one known starting value, even if the browser remembers an earlier form selection.
- The listener connects future select changes to the update function.
- The first function call makes the DOM and count agree with All types.
- The final line reveals the controls only after the required selections and initial update succeed.
Pass updatePokedex without parentheses to addEventListener. The browser will call that function after a change event. The separate updatePokedex() line is an intentional immediate call during initialization.
Use Read one selected option for the native select value pattern. The DOM-events lesson uses the value during form submission; this filter reads it after the select’s own change event.
Check the guided reference results
Section titled “Check the guided reference results”Reload the page. The filter must appear and the status must say Showing 8 of 8 Pokémon.
Test every option:
| Selected option | Visible guided entries | Expected status |
|---|---|---|
| All types | All eight entries | Showing 8 of 8 Pokémon. |
| Grass | Snivy and Bounsweet | Showing 2 of 8 Pokémon. |
| Fire | Fuecoco and Growlithe | Showing 2 of 8 Pokémon. |
| Water | Totodile and Poliwag | Showing 2 of 8 Pokémon. |
| Electric | Mareep | Showing 1 of 8 Pokémon. |
| Ghost | Misdreavus | Showing 1 of 8 Pokémon. |
If you chose different Pokémon, create your own expected-results table before testing. The visible entries, metadata, selected value, and status count must agree.

Style the proven native control
Section titled “Style the proven native control”After the filter behavior passes, add focused rules in styles.css:
- Keep the label visible beside or above the select.
- Stack the label, select, and status in the narrow base layout.
- Let the select inherit the page font and give it a comfortable interactive size.
- Preserve the browser’s native select indicator and keyboard behavior.
- Keep the existing visible focus indicator.
- Let the status text wrap without changing the page width.
- Arrange parts in a row only when their content has enough room.
Do not add a custom dropdown, animated entry transition, or type-color-only label. The current checkpoint concerns the event and DOM result.
Check keyboard behavior
Section titled “Check keyboard behavior”- Reload the page and press Tab until the type select receives focus.
- Use the operating system’s normal select keys to choose Grass.
- Confirm that the two matching entries appear and the status changes.
- Confirm that keyboard focus remains on the select.
- Choose All types and confirm that all eight entries return in source order.
Do not add custom arrow-key listeners. The native select already supplies its keyboard interaction.
If it does not work
Section titled “If it does not work”- If the filter stays hidden, inspect the first Console error before changing CSS. The final reveal line does not run after an earlier error.
- If pointer selection works but the keyboard does not, confirm that the control is a native labeled
selectand that no custom key handler cancels its behavior. - If the count changes but the wrong cards remain, compare each entry’s visible type,
data-typesvalue, and selected option value. - If one selection produces several updates, inspect
pokedex.jsfor duplicateaddEventListenercalls or duplicate script elements.

Checkpoint: The type filter works through one event path
- What now works
- The initialized controls appear, every type selection produces the expected visible entries and count, All types restores the catalog, and the native select keeps keyboard focus.
- Files changed
pokedex.html, pokedex.js, styles.css- What remains
- Prove that the progressive enhancement, responsive catalog, and existing website still work together.
- Next action
- Select All types, save both files, and begin the final verification route with JavaScript enabled.
- If it does not work
- Comment out the event-listener line, call updatePokedex once with All types selected, and restore the eight-entry baseline before reconnecting the event.
7. Verify progressive enhancement and record the checkpoint
Section titled “7. Verify progressive enhancement and record the checkpoint”Run the final route in this order:
- Enable JavaScript, reload
pokedex.html, and confirm the initialized eight-entry result. - Test every filter option against your expected-results table.
- Use only the keyboard to select one multi-entry result, one single-entry result, and All types.
- Confirm after each change that focus remains on the select and the status count agrees with the visible list.
- Test the filtered and complete results near
320px, at a wide viewport, and at200%zoom. - Follow the shared navigation to the landing and game pages, then return to the Pokédex.
- Disable JavaScript and reload. Confirm that the filter region is hidden and all eight entries remain visible.
- Re-enable JavaScript and reload. Confirm that the filter returns and no Console error appears.
- Validate
pokedex.htmland reviewpokedex.jsandstyles.cssin the VS Code Problems panel. - Inspect the Git diff. It should contain one focused filter interface, metadata for existing entries, and one new JavaScript file. It must not contain game behavior or evolution work.
Use the test report structure to record the enabled and disabled JavaScript routes. Then use the solo-development loop to record the verified filter checkpoint.
Self-check
Complete these checks against the required result.
- Confirm that pokedex.html loads one pokedex.js file with defer and the Console shows no connection error.
- Disable JavaScript and confirm that the filter controls disappear while all eight entries remain available.
- Confirm that every .pokedex-entry has lowercase data-types metadata that agrees with its visible type text.
- Deliberately break one selector, confirm the specific missing-elements error, then restore the selector and clear the error.
- Confirm that querySelectorAll returns exactly eight entries in source order.
- Test every filter option and compare the visible species and Showing X of 8 Pokémon status with the expected-results table.
- Confirm that a multi-type entry, when present, matches each type token recorded in data-types.
- Select All types and confirm that all eight entries return in their original source order.
- Operate the select with the keyboard and confirm that focus remains on the control after each DOM update.
- Confirm that no custom keyboard handler, API request, storage, search, sorting, evolution, or game-state code was added.
- Test the complete and filtered layouts near 320px, at a wide viewport, and at 200% zoom without horizontal page scrolling.
- Confirm that HTML validation passes, VS Code reports no unresolved author error, and the Console is clear.
- Confirm that Git records the JavaScript filter separately from the static-catalog checkpoint.
Explanation: from event to visible result
Section titled “Explanation: from event to visible result”The completed filter follows one trace:
Visitor changes the native select→ the browser creates a change event→ updatePokedex runs→ the function reads typeFilter.value→ the loop compares that value with each data-types value→ each list item's hidden property is updated→ the function writes the derived count with textContentThe event does not rewrite pokedex.html. It changes the browser’s current DOM. Reloading creates a new DOM from the saved static catalog and runs pokedex.js again.
This article does not need a separate application-state object because the selected control value and static entry metadata already contain the complete filter input. The later game will need explicit state because trainer, position, encounter, battle, and collection values can all change and affect several interface regions.
Optional investigation: trace one entry in DevTools
Section titled “Optional investigation: trace one entry in DevTools”After the required result passes, place a breakpoint on entry.hidden = !matchesType;. Select Fire and step through the loop eight times. For each pause, record the entry name, entryTypes, matchesType, and resulting hidden value.
Remove the breakpoint when the trace is complete. This investigation changes no required behavior.
Next step or safe stopping point
Section titled “Next step or safe stopping point”This is a safe stopping point. The Pokédex contains the complete static reference and one progressively enhanced JavaScript filter. The filter demonstrates the event → read → compare → DOM update → status path without introducing game state.
Before you stop, leave the page with All types selected and record that both the JavaScript-enabled and JavaScript-disabled routes pass. Continue to Start the game from trainer setup when your teacher confirms that the next article is ready.