Skip to content

Pokémon Browser Game: Filter the Pokédex with JavaScript

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:

  1. A visitor triggers an event.
  2. JavaScript reads the current interface value.
  3. A function decides which result matches.
  4. JavaScript updates the DOM.
  5. 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.

  • New: A deferred pokedex.js file, querySelectorAll, a NodeList, a for...of loop, data-* attributes, dataset, split, includes, the change event, the hidden property, 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.

Complete this article when:

  • pokedex.html loads pokedex.js with defer;
  • the filter uses a connected label and native select control;
  • 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 hidden attribute and JavaScript reveals it only after successful initialization;
  • every .pokedex-entry has one data-types attribute with normalized lowercase type values separated by one space;
  • pokedex.js selects 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 hidden property, and derives the visible count;
  • one change listener 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.

Open pokedex.html. Immediately before the existing catalog list, add this filter region:

pokedex.html — filter region for the guided set
<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 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].

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:

pokedex.html — one single-type entry
<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:

pokedex.html — multiple type tokens
<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 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.

Create pokedex.js in the project root beside pokedex.html.

Add this initial code:

pokedex.js — connection check
"use strict";
console.log("Pokédex JavaScript is connected.");

In the head of pokedex.html, add this script element after the shared stylesheet link:

pokedex.html — deferred script
<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 the browser cannot find pokedex.js, compare the script src, file name, letter case, and project location.
  • If the code appears as page text, confirm that the JavaScript is inside pokedex.js and the HTML contains a script element rather than pasted JavaScript.
  • If the message appears twice after one reload, inspect pokedex.html for 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.

Remove the temporary console.log call. Keep the strict-mode directive.

Add these selections:

pokedex.js — select the filter interface
"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:

pokedex.js — check the DOM contract
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.

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 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:

pokedex.js — update the visible entries
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:

  1. typeFilter.value reads the stable lowercase value of the current option.
  2. visibleCount starts at 0 for this update.
  3. for...of is a loop. It visits each selected list item once and runs the same block for that item.
  4. entry.dataset.types reads the element’s data-types string.
  5. split(" ") creates an array containing each type token.
  6. The comparison matches All types or one included type token.
  7. The hidden property becomes the opposite of the match result.
  8. A matching entry increases the visible count by one.
  9. textContent reports 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.

The function exists but does not run by itself. Temporarily add these lines after the function:

pokedex.js — temporary Grass test
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 the Console says that types is undefined, one selected .pokedex-entry does not have a data-types attribute.
  • 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-entry selects 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:

pokedex.js — initialize the filter
typeFilter.value = "all";
typeFilter.addEventListener("change", updatePokedex);
updatePokedex();
filterRegion.hidden = false;

The order matters:

  1. The first line restores one known starting value, even if the browser remembers an earlier form selection.
  2. The listener connects future select changes to the update function.
  3. The first function call makes the DOM and count agree with All types.
  4. 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.

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.

Initialized Pokémon Trail Pokédex filter with All types selected, a status showing eight of eight Pokémon, and the first catalog entries visible.
JavaScript reveals the filter only after the page's DOM contract is ready and the initial count matches the catalog.
Initialized Pokémon Trail Pokédex filter with All types selected, a status showing eight of eight Pokémon, and the first catalog entries visible.

After the filter behavior passes, add focused rules in styles.css:

  1. Keep the label visible beside or above the select.
  2. Stack the label, select, and status in the narrow base layout.
  3. Let the select inherit the page font and give it a comfortable interactive size.
  4. Preserve the browser’s native select indicator and keyboard behavior.
  5. Keep the existing visible focus indicator.
  6. Let the status text wrap without changing the page width.
  7. 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.

  1. Reload the page and press Tab until the type select receives focus.
  2. Use the operating system’s normal select keys to choose Grass.
  3. Confirm that the two matching entries appear and the status changes.
  4. Confirm that keyboard focus remains on the select.
  5. 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 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 select and that no custom key handler cancels its behavior.
  • If the count changes but the wrong cards remain, compare each entry’s visible type, data-types value, and selected option value.
  • If one selection produces several updates, inspect pokedex.js for duplicate addEventListener calls or duplicate script elements.
Pokémon Trail Pokédex after selecting Grass. Snivy and Bounsweet remain in source order, and the status shows two of eight Pokémon.
One change event updates each existing entry's hidden state and then reports the count derived from the visible result.
Pokémon Trail Pokédex after selecting Grass. Snivy and Bounsweet remain in source order, and the status shows two of eight Pokémon.

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:

  1. Enable JavaScript, reload pokedex.html, and confirm the initialized eight-entry result.
  2. Test every filter option against your expected-results table.
  3. Use only the keyboard to select one multi-entry result, one single-entry result, and All types.
  4. Confirm after each change that focus remains on the select and the status count agrees with the visible list.
  5. Test the filtered and complete results near 320px, at a wide viewport, and at 200% zoom.
  6. Follow the shared navigation to the landing and game pages, then return to the Pokédex.
  7. Disable JavaScript and reload. Confirm that the filter region is hidden and all eight entries remain visible.
  8. Re-enable JavaScript and reload. Confirm that the filter returns and no Console error appears.
  9. Validate pokedex.html and review pokedex.js and styles.css in the VS Code Problems panel.
  10. 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.

  1. Confirm that pokedex.html loads one pokedex.js file with defer and the Console shows no connection error.
  2. Disable JavaScript and confirm that the filter controls disappear while all eight entries remain available.
  3. Confirm that every .pokedex-entry has lowercase data-types metadata that agrees with its visible type text.
  4. Deliberately break one selector, confirm the specific missing-elements error, then restore the selector and clear the error.
  5. Confirm that querySelectorAll returns exactly eight entries in source order.
  6. Test every filter option and compare the visible species and Showing X of 8 Pokémon status with the expected-results table.
  7. Confirm that a multi-type entry, when present, matches each type token recorded in data-types.
  8. Select All types and confirm that all eight entries return in their original source order.
  9. Operate the select with the keyboard and confirm that focus remains on the control after each DOM update.
  10. Confirm that no custom keyboard handler, API request, storage, search, sorting, evolution, or game-state code was added.
  11. Test the complete and filtered layouts near 320px, at a wide viewport, and at 200% zoom without horizontal page scrolling.
  12. Confirm that HTML validation passes, VS Code reports no unresolved author error, and the Console is clear.
  13. Confirm that Git records the JavaScript filter separately from the static-catalog checkpoint.

The completed filter follows one trace:

Pokédex filter 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 textContent

The 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.

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.