Build a Pokedex App with React

Build a React Pokedex that fetches live Pokemon data from the PokéAPI.

Introduction

30 Second Summary

Remember flipping through a Pokedex in the games, looking up Pokemon stats and types? That same experience of browsing, searching, and discovering information is exactly what modern web apps do every day.

In this project, you will build a fully interactive Pokedex web application using React. Your app will fetch real Pokemon data from a live API, display it in a responsive grid, and let users search and view detailed stats for each Pokemon.

What You'll Build

A React Pokedex application that fetches Pokemon data from the PokéAPI and displays it with an interactive, searchable interface.

By the end of this project, you'll have:

  • A React app scaffolded with Vite featuring multiple reusable components that fetch and display live data.
  • A responsive CSS Grid layout of Pokemon cards with type-based coloring, hover effects, and real-time search filtering.
  • A modal-style detail view that shows full Pokemon stats with visual stat bars when you click a card.
  • Secret Mission: Add type-based filtering with color-coded type badges so users can browse Pokemon by type.

Are there any prerequisites?

You should have a basic understanding of HTML, CSS, and programming concepts like variables, functions, loops, and conditionals. No prior React experience is needed.

Set Up Your React Project with Vite

Every React app needs a foundation. Before you can build a Pokedex, you need a project structure that handles compiling your code, running a local development server, and refreshing the browser as you make changes.

In this step, you will scaffold a brand-new React project using Vite. Vite is a modern build tool that makes React development fast and simple.

In this step, get ready to:
  • Scaffold a new React project with Vite.
  • Clean up the default demo content.
  • Create component files for the Pokedex.
Scaffold the project

You will use a single terminal command to generate a full React project with all the configuration you need.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your React project by running this command:
npm create vite@latest pokedex-app -- --template react

What does this command do?

  • npm create vite@latest runs the latest version of Vite's project generator.
  • pokedex-app is the name of the folder it creates for your project.
  • -- --template react tells Vite to use the React template with JavaScript, skipping any interactive prompts.
  • Move into your new project folder and install its dependencies by running these commands:
cd pokedex-app
npm install

What does npm install do?

Your project has a package.json file that lists libraries it depends on (like React). npm install downloads all those libraries into a node_modules folder so your code can use them.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the pokedex-app folder, and click Open.
  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Start your development server by running:
npm run dev

Your terminal shows a local URL. Open http://localhost:5173 in your browser. You should see the default Vite + React demo page with a spinning logo and a counter button.

Not seeing localhost:5173?

  • Make sure you ran npm install before npm run dev. Without installed dependencies, the server cannot start.
  • Check that your Node.js version is 20.19+ or 22.12+. Run node --version to check. Vite requires one of these versions.
  • If port 5173 is already in use, Vite will try the next available port. Check the URL shown in your terminal output.

Help me troubleshoot my Vite dev server not starting

Clean up the scaffold

Vite generates demo content to show that the setup works. Now that you have confirmed it runs, you will replace that demo with a clean starting point for your Pokedex.

  • In the VS Code Explorer sidebar (left panel), expand the src folder.
  • Click on App.css to open it.
  • Select all the content with Cmd+A (macOS) or Ctrl+A (Windows) and delete it.
  • Save the empty file with Cmd+S (macOS) or Ctrl+S (Windows).
  • Click on index.css in the src folder.
  • Select all and delete the content, then save the file.
  • Click on App.jsx in the src folder.
  • Select all the content and replace it with this code:
function App() {
  return <h1>Pokedex</h1>
}

export default App

What does this code do?

  • function App() defines a React component. Components are reusable building blocks for your UI.
  • return <h1>Pokedex</h1> tells React to display a heading on the page. This HTML-like syntax inside JavaScript is called JSX.
  • export default App makes this component available to other files in your project.
  • Save App.jsx with Cmd+S (macOS) or Ctrl+S (Windows).
  • Switch back to your browser. You should see just the word "Pokedex" displayed as a heading with no other styling or content.

Still seeing the old Vite demo page?

  • Make sure you saved all three files (App.css, index.css, App.jsx). Unsaved changes won't appear in the browser.
  • Check that npm run dev is still running in your terminal. If you accidentally closed it, run the command again.
  • Try a hard refresh in your browser with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).

Help me fix my Vite React page not updating after I edited App.jsx

✔️ Awesome, I've got everything!

Great. Make sure you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Here is what src/App.jsx should look like:

function App() {
  return <h1>Pokedex</h1>
}

export default App

Both src/App.css and src/index.css should be completely empty (no content at all).

The src/main.jsx file was generated by Vite and should remain untouched:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
Set up the project file structure

Your Pokedex will be made of multiple React components. Each component lives in its own file inside a dedicated folder.

  • In VS Code's integrated terminal, open a second terminal tab by clicking the + icon in the terminal panel (your dev server is still running in the first tab).
  • Create the components folder and empty component files by running these commands:
mkdir src/components
touch src/components/PokemonCard.jsx src/components/PokemonGrid.jsx src/components/SearchBar.jsx

What are these component files for?

  • PokemonCard.jsx will display a single Pokemon with its image and type.
  • PokemonGrid.jsx will arrange all the cards in a responsive grid layout.
  • SearchBar.jsx will let users filter Pokemon by name.
  • Confirm the files exist by running:
ls src/components

You should see PokemonCard.jsx, PokemonGrid.jsx, and SearchBar.jsx listed in the output.

Don't see the files listed?

  • Make sure you are in the pokedex-app folder. Run pwd to check your current directory.
  • On Windows, touch may not be available. Instead, create the files manually by right-clicking the src/components folder in VS Code's Explorer and selecting New File.

Help me create my component files in the right location

Your project is scaffolded and ready to go. Next up, you will connect to the PokéAPI and fetch real Pokemon data to display in your app.

Fetch Pokemon Data from the API

Your React project is scaffolded and running. Now it needs real data to display.

In this step, you will fetch live Pokemon data from the PokéAPI using React's built-in hooks. The PokéAPI is a free, open API that serves data about every Pokemon ever created.

In this step, get ready to:
  • Fetch the Pokemon list from the PokéAPI.
  • Get detailed data (images, types, stats) for each Pokemon.
  • Display a loading state and a temporary list of Pokemon names.
Import React hooks and fetch the Pokemon list

React provides two hooks that work together to fetch data. useState stores data that can change over time. useEffect runs code after your component first appears on screen.

What is a hook?

A hook is a special React function that lets your component "hook into" React features like state and lifecycle events. Hooks always start with the word use.

  • In VS Code, open src/App.jsx from the Explorer sidebar.
  • Select all the existing code and replace it with the following:
import { useState, useEffect } from 'react'
import './App.css'

function App() {
  // Array to hold our Pokemon data
  const [pokemon, setPokemon] = useState([])
  // Track whether data is still loading
  const [loading, setLoading] = useState(true)
  // For future search functionality
  const [searchTerm, setSearchTerm] = useState('')
  // For future detail view
  const [selectedPokemon, setSelectedPokemon] = useState(null)

  useEffect(() => {
    async function fetchPokemon() {
      try {
        // Fetch the first 20 Pokemon from PokéAPI
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()
        console.log(data.results)
        setLoading(false)
      } catch (error) {
        console.error('Failed to fetch Pokemon:', error)
        setLoading(false)
      }
    }

    fetchPokemon()
  }, [])

  return <h1>Pokedex</h1>
}

export default App

What does this code do?

useState([]) creates a pokemon variable that starts as an empty array. useState(true) creates a loading flag that starts as true (because we haven't loaded anything yet).

useEffect with an empty [] dependency array means "run this function once when the component first appears." Inside it, fetchPokemon() calls the PokéAPI and logs the results.

The async/await pattern is required because useEffect cannot be async directly. That is why we define a separate async function fetchPokemon() inside it and call it immediately.

  • Save src/App.jsx.
  • Open your browser's DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
  • Click the Console tab. You should see an array of 20 objects, each with a name and url property.

Not seeing anything in the console?

Make sure you saved src/App.jsx. Vite hot-reloads on save, so unsaved changes will not appear.

If you see a network error, check that your internet connection is active. The PokéAPI requires no authentication but does need network access.

help me debug why my useEffect fetch is not logging data

Fetch detailed data for each Pokemon

The list endpoint only returns names and URLs. To get images, types, and stats, you need to fetch each Pokemon's detail URL individually.

Why two rounds of fetching?

The PokéAPI splits data across endpoints to keep responses small. The list endpoint gives you 20 names quickly. Each name comes with a URL pointing to that Pokemon's full data (sprites, types, stats, abilities).

Promise.all() lets you fire all 20 detail requests at the same time instead of waiting for each one to finish before starting the next. This keeps load times short.

  • In src/App.jsx, find the try block inside your useEffect. Find this code:
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()
        console.log(data.results)
        setLoading(false)
  • Replace those lines with the following:
        // Fetch the first 20 Pokemon from PokéAPI
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()

        // Fetch detailed data for each Pokemon in parallel
        const detailedPokemon = await Promise.all(
          data.results.map(async (p) => {
            const detailResponse = await fetch(p.url)
            return detailResponse.json()
          })
        )

        console.log(detailedPokemon)
        setPokemon(detailedPokemon)
        setLoading(false)

What does this code do?

data.results.map() loops through each Pokemon in the list. For each one, it fetches the full detail URL (which the list endpoint provided as p.url).

Promise.all() wraps all 20 fetch requests and waits for every one to complete. The result is an array of 20 detailed Pokemon objects containing names, images, types, and stats.

setPokemon(detailedPokemon) stores all that data in your component's state so React can use it to render the UI.

  • Save src/App.jsx.
  • Check your browser's Console tab again. You should now see 20 objects that each contain name, sprites, types, and stats properties.

Seeing 'Failed to fetch' errors?

If you see network errors in the console, the PokéAPI might be temporarily unreachable. Wait a moment and refresh the page to try again.

If you see a syntax error, double-check that your async keyword is before the arrow function inside .map(). Every await must be inside an async function.

help me fix my Promise.all fetch for Pokemon detail data

Display a loading state and Pokemon names

Right now the page still just shows "Pokedex" regardless of whether data has loaded. Conditional rendering lets you show different content depending on component state.

  • In src/App.jsx, find the console.log(detailedPokemon) line you added in the previous substep and delete it.
  • Then find the return statement at the bottom of the App function:
  return <h1>Pokedex</h1>
  • Replace it with the following conditional rendering and temporary Pokemon list:
  // Show loading message while data is being fetched
  if (loading) {
    return <p className="loading">Loading Pokemon...</p>
  }

  // Temporary list to confirm data loaded correctly
  return (
    <div className="app">
      <h1 className="title">Pokedex</h1>
      <ul>
        {pokemon.map((p) => <li key={p.name}>{p.name}</li>)}
      </ul>
    </div>
  )

What does this code do?

The if (loading) check returns early with a loading message. React renders this paragraph instead of the main content while data is still being fetched.

Once setLoading(false) runs (after the fetch completes), React re-renders the component. This time it skips the loading check and displays the main return with a <ul> list of Pokemon names.

The key={p.name} prop helps React efficiently update the list when items change. Every list item in React needs a unique key.

  • Save src/App.jsx.
  • Switch to your browser. You should briefly see "Loading Pokemon..." followed by a plain list of 20 Pokemon names (bulbasaur, ivysaur, venusaur, and so on).

Page is stuck on Loading Pokemon?

If the loading message never goes away, your fetch might be failing silently. Open the Console tab in DevTools and look for error messages.

Make sure setLoading(false) appears both inside the try block (after success) and inside the catch block (after an error). If it only appears in try, a failed fetch leaves loading stuck at true.

help me figure out why my Pokemon list is not rendering after the loading state

✔️ Awesome, I've got everything!

Great. Double check you have saved src/App.jsx and your browser shows 20 Pokemon names.

ⓧ I'd like to double check the full code

Here is the complete src/App.jsx at the end of this step:

import { useState, useEffect } from 'react'
import './App.css'

function App() {
  const [pokemon, setPokemon] = useState([])
  const [loading, setLoading] = useState(true)
  const [searchTerm, setSearchTerm] = useState('')
  const [selectedPokemon, setSelectedPokemon] = useState(null)

  useEffect(() => {
    async function fetchPokemon() {
      try {
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()

        const detailedPokemon = await Promise.all(
          data.results.map(async (p) => {
            const detailResponse = await fetch(p.url)
            return detailResponse.json()
          })
        )

        setPokemon(detailedPokemon)
        setLoading(false)
      } catch (error) {
        console.error('Failed to fetch Pokemon:', error)
        setLoading(false)
      }
    }

    fetchPokemon()
  }, [])

  if (loading) {
    return <p className="loading">Loading Pokemon...</p>
  }

  return (
    <div className="app">
      <h1 className="title">Pokedex</h1>
      <ul>
        {pokemon.map((p) => <li key={p.name}>{p.name}</li>)}
      </ul>
    </div>
  )
}

export default App

Your app is now pulling live data from the internet and displaying it. Next up, you will turn those plain names into visual Pokemon cards with images and type colors.

Build the Pokemon Card Component

You have 20 Pokemon flowing into your app from the PokéAPI. Right now they appear as a plain list of names, which does not showcase the rich data you fetched (images, types, and stats).

In this step, you will build a reusable React component that turns each Pokemon into a visual card. Each card displays an image, a capitalized name, a type badge, and a color tint based on the Pokemon's type.

In this step, get ready to:
  • Create a PokemonCard component that accepts props and renders Pokemon data.
  • Style the card with CSS for a polished, interactive look.
  • Add type-based background colors so each Pokemon's card reflects its type.
Create the PokemonCard component

A React component is a reusable function that returns a piece of UI. You will create a PokemonCard component that receives a single Pokemon object as a prop and renders its image, name, and primary type.

  • In VS Code, open src/components/PokemonCard.jsx and paste the following code:
import './PokemonCard.css'

function PokemonCard({ pokemon, onClick }) {
  // Get the Pokemon's primary type from the types array
  const primaryType = pokemon.types[0].type.name

  return (
    <div
      className="pokemon-card"
      onClick={() => onClick(pokemon)}
    >
      <img src={pokemon.sprites.front_default} alt={pokemon.name} />
      <h3>{pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</h3>
      <span className="type-badge">
        {primaryType}
      </span>
    </div>
  )
}

export default PokemonCard

What does this code do?

This component uses destructuring in its function parameters to pull out pokemon and onClick from the props object. Props are how parent components pass data down to child components.

  • pokemon.types[0].type.name digs into the API's nested data structure to get the first type (e.g. "fire", "water").
  • pokemon.sprites.front_default is the URL for the Pokemon's official sprite image.
  • charAt(0).toUpperCase() + slice(1) capitalizes the first letter of the Pokemon's name since the API returns lowercase.

Now you need to use this component in your app. The PokemonCard component is ready, but App.jsx does not know about it yet.

  • Switch back to src/App.jsx.
  • Find this import line near the top of the file:
import './App.css'
  • Add this new import line directly above it:
import PokemonCard from './components/PokemonCard.jsx'
  • Now find the <ul> block inside the return statement:
      <ul>
        {pokemon.map((p) => <li key={p.name}>{p.name}</li>)}
      </ul>
  • Replace that entire <ul>...</ul> block with this line:
      {pokemon[0] && <PokemonCard pokemon={pokemon[0]} onClick={() => {}} />}

Why render just one card?

You are rendering only the first Pokemon (pokemon[0]) to test that your component works before building out the full grid. The && operator ensures nothing renders if the array is empty.

The onClick={() => {}} passes an empty function for now. You will wire up real click behavior in a later step.

  • Save both PokemonCard.jsx and App.jsx.
  • Check your browser at http://localhost:5173. You should see a single Pokemon with its sprite image, capitalized name, and type text displayed (unstyled for now).

Don't see the Pokemon card?

  • Check that your import path in App.jsx matches exactly: ./components/PokemonCard.jsx. A missing ./ prefix or wrong filename will cause an import error.
  • Open DevTools (press F12) and check the Console tab for error messages. A red error about "Cannot read properties of undefined" means the Pokemon data has not loaded yet.
  • Make sure you saved both files. Vite only refreshes when files are saved to disk.

Why isn't my PokemonCard component rendering?

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on to styling.

ⓧ I'd like to double check the full code

Here is what src/App.jsx should look like at this point:

import { useState, useEffect } from 'react'
import PokemonCard from './components/PokemonCard.jsx'
import './App.css'

function App() {
  const [pokemon, setPokemon] = useState([])
  const [loading, setLoading] = useState(true)
  const [searchTerm, setSearchTerm] = useState('')
  const [selectedPokemon, setSelectedPokemon] = useState(null)

  useEffect(() => {
    async function fetchPokemon() {
      try {
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()

        const detailedPokemon = await Promise.all(
          data.results.map(async (p) => {
            const detailResponse = await fetch(p.url)
            return detailResponse.json()
          })
        )

        setPokemon(detailedPokemon)
        setLoading(false)
      } catch (error) {
        console.error('Failed to fetch Pokemon:', error)
        setLoading(false)
      }
    }

    fetchPokemon()
  }, [])

  if (loading) {
    return <p className="loading">Loading Pokemon...</p>
  }

  return (
    <div className="app">
      <h1 className="title">Pokedex</h1>
      {pokemon[0] && <PokemonCard pokemon={pokemon[0]} onClick={() => {}} />}
    </div>
  )
}

export default App
Style the card with CSS

Your card renders data but has no visual styling. A good card component needs borders, spacing, and interactivity cues like a hover effect so users know it is clickable.

  • Open src/components/PokemonCard.css and paste the following styles:
.pokemon-card {
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  padding: 20px;
  text-align: center;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  background-color: white;
}

.pokemon-card:hover {
  transform: scale(1.05);
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}

.pokemon-card img {
  width: 96px;
  height: 96px;
}

.pokemon-card h3 {
  margin: 10px 0 8px;
  font-size: 1.1rem;
  color: #333;
}

.type-badge {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 20px;
  color: white;
  font-size: 0.8rem;
  font-weight: bold;
  text-transform: capitalize;
}

What do these styles do?

  • .pokemon-card gives the card a rounded border, padding, and a pointer cursor to signal clickability.
  • The transition property makes the hover effect animate smoothly instead of snapping.
  • .pokemon-card:hover scales the card up by 5% and adds a box shadow, making it feel interactive.
  • .type-badge styles the type text as a rounded pill shape with white text.
  • Save PokemonCard.css and check your browser.
  • You should see the Pokemon card as a white rounded box with centered content. Hover over it and the card scales up slightly with a shadow appearing behind it.

Card not styled?

  • Confirm that PokemonCard.jsx has import './PokemonCard.css' at the top. Without this import, React does not load the CSS file.
  • Check that the class name in your JSX (className="pokemon-card") matches the class name in your CSS (.pokemon-card). Class names are case-sensitive.

Help me debug why my PokemonCard CSS styles are not applying.

✔️ Awesome, I've got everything!

Nice. Your card has a polished look with hover interactivity.

ⓧ I'd like to double check the full code

Here is the full src/components/PokemonCard.css:

.pokemon-card {
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  padding: 20px;
  text-align: center;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  background-color: white;
}

.pokemon-card:hover {
  transform: scale(1.05);
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}

.pokemon-card img {
  width: 96px;
  height: 96px;
}

.pokemon-card h3 {
  margin: 10px 0 8px;
  font-size: 1.1rem;
  color: #333;
}

.type-badge {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 20px;
  color: white;
  font-size: 0.8rem;
  font-weight: bold;
  text-transform: capitalize;
}
Add type-based background color

Each Pokemon has a type (fire, water, grass, etc.). You can use the type to tint the card's background, making it easy to visually distinguish Pokemon at a glance.

  • Return to src/components/PokemonCard.jsx and replace the entire file contents with the following updated code:
import './PokemonCard.css'

// Map each Pokemon type to a hex color
const typeColors = {
  fire: '#F08030',
  water: '#6890F0',
  grass: '#78C850',
  electric: '#F8D030',
  normal: '#A8A878',
  ice: '#98D8D8',
  fighting: '#C03028',
  poison: '#A040A0',
  ground: '#E0C068',
  flying: '#A890F0',
  psychic: '#F85888',
  bug: '#A8B820',
  rock: '#B8A038',
  ghost: '#705898',
  dragon: '#7038F8',
  dark: '#705848',
  steel: '#B8B8D0',
  fairy: '#EE99AC',
}

function PokemonCard({ pokemon, onClick }) {
  // Get the primary type and its matching color
  const primaryType = pokemon.types[0].type.name
  const bgColor = typeColors[primaryType] || '#A8A878'

  return (
    <div
      className="pokemon-card"
      style={{ backgroundColor: `${bgColor}33` }}
      onClick={() => onClick(pokemon)}
    >
      <img src={pokemon.sprites.front_default} alt={pokemon.name} />
      <h3>{pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</h3>
      <span className="type-badge" style={{ backgroundColor: bgColor }}>
        {primaryType}
      </span>
    </div>
  )
}

export default PokemonCard

What does the typeColors object do?

The typeColors object maps every Pokemon type to an official-style hex color. When the component renders, it looks up the Pokemon's primary type in this object.

  • ${bgColor}33 appends a hex opacity value (33 = 20% opacity) to the color. This gives the card a subtle tint without overwhelming the content.
  • The || '#A8A878' fallback ensures the card still gets a color if the type is not in the mapping.
  • The type badge span uses the full-strength color as its background, making the badge stand out.
  • Save PokemonCard.jsx and check your browser.
  • Your Pokemon card now has a tinted background matching its type. If the first Pokemon is Bulbasaur, you should see a green tint. The type badge also displays in the full type color.

Card background still white?

  • Make sure the inline style prop uses double curly braces: style={{ backgroundColor: ... }}. The outer braces enter JavaScript mode. The inner braces define the style object.
  • Check that the backtick template string wraps correctly: `${bgColor}33`. Regular quotes will not interpolate the variable.

Why is my inline style not applying a background color to the Pokemon card?

✔️ Awesome, I've got everything!

Excellent. Your PokemonCard component is complete with type-based coloring.

ⓧ I'd like to double check the full code

Here is the final src/components/PokemonCard.jsx:

import './PokemonCard.css'

const typeColors = {
  fire: '#F08030',
  water: '#6890F0',
  grass: '#78C850',
  electric: '#F8D030',
  normal: '#A8A878',
  ice: '#98D8D8',
  fighting: '#C03028',
  poison: '#A040A0',
  ground: '#E0C068',
  flying: '#A890F0',
  psychic: '#F85888',
  bug: '#A8B820',
  rock: '#B8A038',
  ghost: '#705898',
  dragon: '#7038F8',
  dark: '#705848',
  steel: '#B8B8D0',
  fairy: '#EE99AC',
}

function PokemonCard({ pokemon, onClick }) {
  const primaryType = pokemon.types[0].type.name
  const bgColor = typeColors[primaryType] || '#A8A878'

  return (
    <div
      className="pokemon-card"
      style={{ backgroundColor: `${bgColor}33` }}
      onClick={() => onClick(pokemon)}
    >
      <img src={pokemon.sprites.front_default} alt={pokemon.name} />
      <h3>{pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</h3>
      <span className="type-badge" style={{ backgroundColor: bgColor }}>
        {primaryType}
      </span>
    </div>
  )
}

export default PokemonCard

Your PokemonCard component is complete and rendering one colorful card. Next up, you will build a grid layout that renders all 20 Pokemon at once, plus a search bar to filter them in real time.

Build the Grid Layout and Search

In the last step, you built a single Pokemon card that looks great on its own. Now it's time to display all 20 Pokemon at once in a responsive grid layout.

You'll create two new components: a grid that arranges all cards into neat columns, and a search bar that filters Pokemon in real-time as the user types. These two components work together to give your Pokedex a professional, interactive feel.

In this step, get ready to:
  • Build a PokemonGrid component that renders all Pokemon cards in a responsive CSS Grid layout.
  • Build a SearchBar component that filters Pokemon by name as you type.
  • Style the overall page layout with a centered container and modern input styling.
Create the PokemonGrid component

Right now your app only renders one card. The PokemonGrid component takes an array of Pokemon and maps over it to render a PokemonCard for each one.

  • Open src/components/PokemonGrid.jsx in VS Code and add the following code:
import PokemonCard from './PokemonCard.jsx'
import './PokemonGrid.css'

function PokemonGrid({ pokemon, onPokemonClick }) {
  return (
    <div className="pokemon-grid">
      {pokemon.map((p) => (
        <PokemonCard key={p.name} pokemon={p} onClick={onPokemonClick} />
      ))}
    </div>
  )
}

export default PokemonGrid

What does this code do?

  • The component accepts two props: pokemon (an array of Pokemon objects) and onPokemonClick (a callback function for when a card is clicked).
  • pokemon.map() loops through the array and renders a PokemonCard for each entry.
  • key={p.name} gives React a unique identifier for each card so it can efficiently update the list when data changes.

You won't see a change yet. The grid needs its CSS styles before we wire it into the app.

  • Create a new file src/components/PokemonGrid.css and add the grid layout styles:
.pokemon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
}

What is CSS Grid doing here?

CSS Grid arranges items into rows and columns automatically. The auto-fill keyword tells the browser to fit as many 200px-wide columns as possible into the available space.

When the window shrinks, columns drop away. When it grows, new columns appear. This makes the grid fully responsive without any media queries.

  • Save both PokemonGrid.jsx and PokemonGrid.css.

You won't see a change in the browser yet. The next substep creates the SearchBar, then you'll wire both components into your app.

Create the SearchBar component

A search bar lets users filter the grid by typing a Pokemon name. In React, this is called a controlled input because React state controls the input's value at all times.

  • Open src/components/SearchBar.jsx and add the following code:
import './SearchBar.css'

function SearchBar({ searchTerm, onSearchChange }) {
  return (
    <div className="search-bar">
      <input
        type="text"
        placeholder="Search Pokemon..."
        value={searchTerm}
        onChange={(e) => onSearchChange(e.target.value)}
      />
    </div>
  )
}

export default SearchBar

What does this code do?

  • searchTerm is the current text value, passed in from the parent component.
  • onSearchChange is a callback that fires every time the user types a character. It sends the new value up to the parent so it can filter the Pokemon list.
  • value={searchTerm} makes this a controlled input. React always knows what's in the text field.
  • Create a new file src/components/SearchBar.css and add the following styles:
.search-bar {
  margin-bottom: 30px;
  display: flex;
  justify-content: center;
}

.search-bar input {
  width: 100%;
  max-width: 500px;
  padding: 14px 20px;
  font-size: 1.1rem;
  border: 2px solid #ddd;
  border-radius: 30px;
  outline: none;
  transition: border-color 0.3s;
}

.search-bar input:focus {
  border-color: #6890f0;
}

What does this CSS do?

The input gets a pill shape with border-radius: 30px and generous padding for a modern look. The :focus rule changes the border to a blue accent color when the user clicks into the field, giving clear visual feedback.

  • Save both SearchBar.jsx and SearchBar.css.

Now it's time to wire both new components into your main app. This is where everything comes together.

  • Replace the entire contents of src/App.jsx with the following updated code:
import { useState, useEffect } from 'react'
import PokemonGrid from './components/PokemonGrid.jsx'
import SearchBar from './components/SearchBar.jsx'
import './App.css'

function App() {
  const [pokemon, setPokemon] = useState([])
  const [loading, setLoading] = useState(true)
  const [searchTerm, setSearchTerm] = useState('')
  const [selectedPokemon, setSelectedPokemon] = useState(null)

  useEffect(() => {
    async function fetchPokemon() {
      try {
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()

        const detailedPokemon = await Promise.all(
          data.results.map(async (p) => {
            const detailResponse = await fetch(p.url)
            return detailResponse.json()
          })
        )

        setPokemon(detailedPokemon)
        setLoading(false)
      } catch (error) {
        console.error('Failed to fetch Pokemon:', error)
        setLoading(false)
      }
    }

    fetchPokemon()
  }, [])

  // Filter pokemon based on search input
  const filteredPokemon = pokemon.filter((p) =>
    p.name.includes(searchTerm.toLowerCase())
  )

  if (loading) {
    return <p className="loading">Loading Pokemon...</p>
  }

  return (
    <div className="app">
      <h1 className="title">Pokedex</h1>
      <SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
      <PokemonGrid pokemon={filteredPokemon} onPokemonClick={setSelectedPokemon} />
    </div>
  )
}

export default App

What changed in App.jsx?

  • The imports now bring in PokemonGrid and SearchBar instead of the single PokemonCard.
  • filteredPokemon runs .filter() on the full array, keeping only Pokemon whose name includes whatever the user has typed. This runs on every render, so the grid updates instantly as you type.
  • The return now renders SearchBar (passing the current search text and setter) above PokemonGrid (passing the filtered list and click handler).
  • Save src/App.jsx.
  • Switch to your browser. You should see all 20 Pokemon displayed as cards in a grid layout with a search input above them.
  • Type char into the search bar. The grid should filter down to show only Charmander and Charmeleon.

Not seeing the grid or search?

  • Check that your imports in App.jsx match the file paths exactly: ./components/PokemonGrid.jsx and ./components/SearchBar.jsx.
  • Make sure you removed the old import PokemonCard line from App.jsx. PokemonGrid handles rendering cards now.
  • If the search doesn't filter, confirm that the filteredPokemon variable is placed BEFORE the if (loading) check.

Help me debug why my PokemonGrid or SearchBar isn't rendering.

Style the search bar and overall layout

The grid and search work, but the page still looks rough. A few global styles and app-level tweaks will make it feel polished.

  • Open src/index.css and replace any existing content with the following global reset styles:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  min-height: 100vh;
}

What does this CSS do?

The * selector resets margins and padding on every element so browsers render things consistently. box-sizing: border-box makes padding count inside an element's width rather than adding to it.

The body gets a light gray background and a system font stack for clean, native-looking text across all platforms.

  • Save src/index.css.
  • Refresh your browser. The page background should now be a light gray, and any default browser spacing disappears.
  • Open src/App.css and replace its contents with the following app-level styles:
.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.title {
  text-align: center;
  font-size: 2.5rem;
  margin-bottom: 20px;
  color: #333;
}

.loading {
  text-align: center;
  font-size: 1.5rem;
  margin-top: 100px;
  color: #666;
}

What does this CSS do?

  • .app constrains the content to 1200px wide and centers it horizontally with margin: 0 auto.
  • .title centers the "Pokedex" heading and sizes it prominently.
  • .loading styles the loading message that appears while data fetches from the API.
  • Save src/App.css and refresh your browser.
  • Your app should now have a clean centered layout. The "Pokedex" heading is large and centered, the search bar sits below it with rounded styling, and the Pokemon grid fills the page with responsive columns.
  • Try resizing your browser window. The grid should automatically adjust the number of columns to fit the available space.

Layout not centered or looking off?

  • Make sure your index.css has the * { margin: 0; padding: 0; } reset. Without it, browser defaults add unwanted spacing.
  • Confirm that the className in your App.jsx return matches: className="app" on the wrapper div and className="title" on the h1.
  • Check that import './App.css' is present at the top of App.jsx.

Help me figure out why my Pokedex layout styles aren't applying.

✔️ Awesome, I've got everything!

Great. Make sure all files are saved before moving on.

ⓧ I'd like to double check the full code

Here are all the files you created or modified in this step.

import PokemonCard from './PokemonCard.jsx'
import './PokemonGrid.css'

function PokemonGrid({ pokemon, onPokemonClick }) {
  return (
    <div className="pokemon-grid">
      {pokemon.map((p) => (
        <PokemonCard key={p.name} pokemon={p} onClick={onPokemonClick} />
      ))}
    </div>
  )
}

export default PokemonGrid
.pokemon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
}
import './SearchBar.css'

function SearchBar({ searchTerm, onSearchChange }) {
  return (
    <div className="search-bar">
      <input
        type="text"
        placeholder="Search Pokemon..."
        value={searchTerm}
        onChange={(e) => onSearchChange(e.target.value)}
      />
    </div>
  )
}

export default SearchBar
.search-bar {
  margin-bottom: 30px;
  display: flex;
  justify-content: center;
}

.search-bar input {
  width: 100%;
  max-width: 500px;
  padding: 14px 20px;
  font-size: 1.1rem;
  border: 2px solid #ddd;
  border-radius: 30px;
  outline: none;
  transition: border-color 0.3s;
}

.search-bar input:focus {
  border-color: #6890f0;
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  min-height: 100vh;
}
.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.title {
  text-align: center;
  font-size: 2.5rem;
  margin-bottom: 20px;
  color: #333;
}

.loading {
  text-align: center;
  font-size: 1.5rem;
  margin-top: 100px;
  color: #666;
}
import { useState, useEffect } from 'react'
import PokemonGrid from './components/PokemonGrid.jsx'
import SearchBar from './components/SearchBar.jsx'
import './App.css'

function App() {
  const [pokemon, setPokemon] = useState([])
  const [loading, setLoading] = useState(true)
  const [searchTerm, setSearchTerm] = useState('')
  const [selectedPokemon, setSelectedPokemon] = useState(null)

  useEffect(() => {
    async function fetchPokemon() {
      try {
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()

        const detailedPokemon = await Promise.all(
          data.results.map(async (p) => {
            const detailResponse = await fetch(p.url)
            return detailResponse.json()
          })
        )

        setPokemon(detailedPokemon)
        setLoading(false)
      } catch (error) {
        console.error('Failed to fetch Pokemon:', error)
        setLoading(false)
      }
    }

    fetchPokemon()
  }, [])

  const filteredPokemon = pokemon.filter((p) =>
    p.name.includes(searchTerm.toLowerCase())
  )

  if (loading) {
    return <p className="loading">Loading Pokemon...</p>
  }

  return (
    <div className="app">
      <h1 className="title">Pokedex</h1>
      <SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
      <PokemonGrid pokemon={filteredPokemon} onPokemonClick={setSelectedPokemon} />
    </div>
  )
}

export default App

Your Pokedex now shows a full grid of Pokemon with real-time search filtering. Next up, you'll add a detail view that appears when you click a card, showing each Pokemon's full stats.

Add Pokemon Detail View

Your Pokedex grid shows 20 Pokemon with their images and types. Clicking a card already updates the selectedPokemon state behind the scenes, but nothing visible happens yet.

In this step, you'll build a detail panel that pops up as a modal overlay when you click a card. It displays the Pokemon's full stat bars (HP, Attack, Defense, Speed) so you can compare strengths at a glance.

In this step, get ready to:
  • Verify the click handler connects cards to the selected state.
  • Build a PokemonDetail component with stat bars and type badges.
  • Style and wire up the modal overlay in App.jsx.
Verify your click handler works

The click handling is already wired from the previous step. Your PokemonGrid passes onPokemonClick down to each PokemonCard, which calls it when clicked. Before building the detail view, let's confirm the state updates correctly.

  • Open your browser at http://localhost:5173 where your Vite dev server is running.
  • Open DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
  • Switch to the Console tab.
  • Click any Pokemon card in the grid.

Nothing visible changes yet because we haven't built the detail view. If you install the React Developer Tools browser extension, you can inspect the App component's state and confirm selectedPokemon now holds the clicked Pokemon's data.

What is conditional rendering?

In React, you can show or hide parts of your UI based on state. The pattern {selectedPokemon && <Component />} only renders the component when selectedPokemon is not null.

This is how you'll display the detail panel only after a click, and hide it when the user presses Back.

Build the PokemonDetail component

This component receives a full Pokemon object and displays it in a modal-style panel. It shows a large image, the Pokemon's name, all its type badges, and stat bars that visually represent each stat's strength.

  • Create a new file called PokemonDetail.jsx inside your src/components/ folder.
  • Add the following code to PokemonDetail.jsx:
import './PokemonDetail.css'

const typeColors = {
  fire: '#F08030',
  water: '#6890F0',
  grass: '#78C850',
  electric: '#F8D030',
  normal: '#A8A878',
  ice: '#98D8D8',
  fighting: '#C03028',
  poison: '#A040A0',
  ground: '#E0C068',
  flying: '#A890F0',
  psychic: '#F85888',
  bug: '#A8B820',
  rock: '#B8A038',
  ghost: '#705898',
  dragon: '#7038F8',
  dark: '#705848',
  steel: '#B8B8D0',
  fairy: '#EE99AC',
}

function PokemonDetail({ pokemon, onClose }) {
  return (
    // Clicking the overlay background closes the detail view
    <div className="detail-overlay" onClick={onClose}>
      {/* stopPropagation prevents clicks inside the panel from closing it */}
      <div className="detail-panel" onClick={(e) => e.stopPropagation()}>
        <button className="close-button" onClick={onClose}>Back</button>
        <img src={pokemon.sprites.front_default} alt={pokemon.name} />
        <h2>{pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</h2>
        <div className="types">
          {pokemon.types.map((t) => (
            <span
              key={t.type.name}
              className="type-badge"
              style={{ backgroundColor: typeColors[t.type.name] || '#A8A878' }}
            >
              {t.type.name}
            </span>
          ))}
        </div>
        <div className="stats">
          {pokemon.stats.map((s) => (
            <div key={s.stat.name} className="stat-row">
              <span className="stat-name">{s.stat.name}</span>
              <div className="stat-bar-bg">
                <div
                  className="stat-bar-fill"
                  style={{ width: `${(s.base_stat / 255) * 100}%` }}
                ></div>
              </div>
              <span className="stat-value">{s.base_stat}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

export default PokemonDetail

What does this code do?

  • The outer detail-overlay div covers the entire screen. Clicking it calls onClose, which dismisses the modal.
  • The inner detail-panel uses e.stopPropagation() so clicks inside the panel don't bubble up and accidentally close it.
  • Each stat bar calculates its width as a percentage of 255 (the maximum base stat in Pokemon). A stat of 100 renders as about 39% width.
  • The typeColors object maps each Pokemon type to a color for the type badges, just like in PokemonCard.
  • Save PokemonDetail.jsx.

The component won't render yet because we haven't imported it in App.jsx or created its CSS file. You'll do that next.

Seeing a red error in your browser?

If you see an error about PokemonDetail.css not being found, that's expected. The import at the top references a CSS file you haven't created yet. You'll create it in the next substep.

help me fix an import error in my PokemonDetail component

Style the overlay and connect the detail view

The detail panel needs CSS to create the modal effect. A fixed overlay dims the background while the white panel floats in the center.

  • Create a new file called PokemonDetail.css inside src/components/.
  • Add the following styles to PokemonDetail.css:
/* Full-screen dark overlay behind the detail panel */
.detail-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

/* The white card that holds Pokemon details */
.detail-panel {
  background: white;
  border-radius: 16px;
  padding: 30px;
  max-width: 400px;
  width: 90%;
  text-align: center;
}

.detail-panel img {
  width: 150px;
  height: 150px;
}

.detail-panel h2 {
  margin: 10px 0;
  font-size: 1.8rem;
  color: #333;
}

.close-button {
  display: block;
  margin-bottom: 10px;
  padding: 8px 16px;
  border: none;
  background-color: #666;
  color: white;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.9rem;
}

.close-button:hover {
  background-color: #444;
}

.types {
  margin: 10px 0 20px;
  display: flex;
  gap: 8px;
  justify-content: center;
}

.type-badge {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 20px;
  color: white;
  font-size: 0.8rem;
  font-weight: bold;
  text-transform: capitalize;
}

/* Stat bar layout and styling */
.stats {
  text-align: left;
}

.stat-row {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
  gap: 10px;
}

.stat-name {
  width: 110px;
  font-size: 0.8rem;
  text-transform: capitalize;
  color: #555;
}

.stat-bar-bg {
  flex: 1;
  height: 10px;
  background-color: #e0e0e0;
  border-radius: 5px;
  overflow: hidden;
}

.stat-bar-fill {
  height: 100%;
  background-color: #6890f0;
  border-radius: 5px;
  transition: width 0.3s;
}

.stat-value {
  width: 30px;
  font-size: 0.8rem;
  text-align: right;
  color: #333;
  font-weight: bold;
}

How does the overlay effect work?

  • The position: fixed overlay covers the entire viewport regardless of scroll position.
  • The rgba(0, 0, 0, 0.6) background creates a semi-transparent dark layer, dimming the grid behind it.
  • The stat-bar-fill div uses a dynamic width set by the component's inline style. The transition: width 0.3s makes the bars animate smoothly when the panel appears.
  • Save PokemonDetail.css.

Now connect the PokemonDetail component to your app. You need to import it and conditionally render it when a Pokemon is selected.

  • In src/App.jsx, find this line near the top:
import SearchBar from './components/SearchBar.jsx'
  • Add this import directly below it:
import PokemonDetail from './components/PokemonDetail.jsx'
  • Next, find the closing </div> at the bottom of the return statement (the one after <PokemonGrid ... />).
  • Add this block between the PokemonGrid line and the closing </div>:
      {selectedPokemon && (
        <PokemonDetail
          pokemon={selectedPokemon}
          onClose={() => setSelectedPokemon(null)}
        />
      )}

What does this code do?

  • The && operator means PokemonDetail only renders when selectedPokemon is not null (a card has been clicked).
  • The onClose prop passes a function that resets selectedPokemon to null, which hides the detail view and shows the grid again.
  • Save App.jsx.
  • Return to your browser. Click any Pokemon card in the grid.

You should see a dark overlay appear with a white panel showing the Pokemon's large image, name, type badges, and stat bars. Clicking the Back button or clicking the dark area outside the panel returns you to the grid.

Clicking a card but nothing appears?

  • Check that your import path matches exactly: ./components/PokemonDetail.jsx. A typo in the path causes a module-not-found error.
  • Make sure the conditional render block is inside the <div className="app"> wrapper, not outside it.
  • Confirm PokemonDetail.css is saved in src/components/ (not in src/). The import in PokemonDetail.jsx expects the CSS in the same folder.

help me debug why my PokemonDetail modal isn't showing up

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files: PokemonDetail.jsx, PokemonDetail.css, and App.jsx.

ⓧ I'd like to double check the full code

Here is the complete PokemonDetail.jsx:

import './PokemonDetail.css'

const typeColors = {
  fire: '#F08030',
  water: '#6890F0',
  grass: '#78C850',
  electric: '#F8D030',
  normal: '#A8A878',
  ice: '#98D8D8',
  fighting: '#C03028',
  poison: '#A040A0',
  ground: '#E0C068',
  flying: '#A890F0',
  psychic: '#F85888',
  bug: '#A8B820',
  rock: '#B8A038',
  ghost: '#705898',
  dragon: '#7038F8',
  dark: '#705848',
  steel: '#B8B8D0',
  fairy: '#EE99AC',
}

function PokemonDetail({ pokemon, onClose }) {
  return (
    <div className="detail-overlay" onClick={onClose}>
      <div className="detail-panel" onClick={(e) => e.stopPropagation()}>
        <button className="close-button" onClick={onClose}>Back</button>
        <img src={pokemon.sprites.front_default} alt={pokemon.name} />
        <h2>{pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</h2>
        <div className="types">
          {pokemon.types.map((t) => (
            <span
              key={t.type.name}
              className="type-badge"
              style={{ backgroundColor: typeColors[t.type.name] || '#A8A878' }}
            >
              {t.type.name}
            </span>
          ))}
        </div>
        <div className="stats">
          {pokemon.stats.map((s) => (
            <div key={s.stat.name} className="stat-row">
              <span className="stat-name">{s.stat.name}</span>
              <div className="stat-bar-bg">
                <div
                  className="stat-bar-fill"
                  style={{ width: `${(s.base_stat / 255) * 100}%` }}
                ></div>
              </div>
              <span className="stat-value">{s.base_stat}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

export default PokemonDetail

Here is the complete PokemonDetail.css:

.detail-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.detail-panel {
  background: white;
  border-radius: 16px;
  padding: 30px;
  max-width: 400px;
  width: 90%;
  text-align: center;
}

.detail-panel img {
  width: 150px;
  height: 150px;
}

.detail-panel h2 {
  margin: 10px 0;
  font-size: 1.8rem;
  color: #333;
}

.close-button {
  display: block;
  margin-bottom: 10px;
  padding: 8px 16px;
  border: none;
  background-color: #666;
  color: white;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.9rem;
}

.close-button:hover {
  background-color: #444;
}

.types {
  margin: 10px 0 20px;
  display: flex;
  gap: 8px;
  justify-content: center;
}

.type-badge {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 20px;
  color: white;
  font-size: 0.8rem;
  font-weight: bold;
  text-transform: capitalize;
}

.stats {
  text-align: left;
}

.stat-row {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
  gap: 10px;
}

.stat-name {
  width: 110px;
  font-size: 0.8rem;
  text-transform: capitalize;
  color: #555;
}

.stat-bar-bg {
  flex: 1;
  height: 10px;
  background-color: #e0e0e0;
  border-radius: 5px;
  overflow: hidden;
}

.stat-bar-fill {
  height: 100%;
  background-color: #6890f0;
  border-radius: 5px;
  transition: width 0.3s;
}

.stat-value {
  width: 30px;
  font-size: 0.8rem;
  text-align: right;
  color: #333;
  font-weight: bold;
}

Here is the complete App.jsx:

import { useState, useEffect } from 'react'
import PokemonGrid from './components/PokemonGrid.jsx'
import SearchBar from './components/SearchBar.jsx'
import PokemonDetail from './components/PokemonDetail.jsx'
import './App.css'

function App() {
  const [pokemon, setPokemon] = useState([])
  const [loading, setLoading] = useState(true)
  const [searchTerm, setSearchTerm] = useState('')
  const [selectedPokemon, setSelectedPokemon] = useState(null)

  useEffect(() => {
    async function fetchPokemon() {
      try {
        const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=20')
        const data = await response.json()

        const detailedPokemon = await Promise.all(
          data.results.map(async (p) => {
            const detailResponse = await fetch(p.url)
            return detailResponse.json()
          })
        )

        setPokemon(detailedPokemon)
        setLoading(false)
      } catch (error) {
        console.error('Failed to fetch Pokemon:', error)
        setLoading(false)
      }
    }

    fetchPokemon()
  }, [])

  const filteredPokemon = pokemon.filter((p) =>
    p.name.includes(searchTerm.toLowerCase())
  )

  if (loading) {
    return <p className="loading">Loading Pokemon...</p>
  }

  return (
    <div className="app">
      <h1 className="title">Pokedex</h1>
      <SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
      <PokemonGrid pokemon={filteredPokemon} onPokemonClick={setSelectedPokemon} />
      {selectedPokemon && (
        <PokemonDetail
          pokemon={selectedPokemon}
          onClose={() => setSelectedPokemon(null)}
        />
      )}
    </div>
  )
}

export default App

Your Pokedex is now fully functional. You can browse Pokemon in the grid, search by name, and click any card to see detailed stats in a slick modal overlay. This is the kind of interactive UI that makes frontend portfolios stand out.

Secret mission

Add Type-Based Filtering

Your Pokedex search works great for finding Pokemon by name. But what if you want to browse by type? In this secret mission, you'll build a dynamic filter bar that lets users click type buttons to narrow results. The real challenge is combining two filters at once.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The pokedex-app project folder on your Desktop.
  • Node.js packages installed in node_modules (inside the project folder).
  • The Vite dev server running in your terminal.

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your Pokedex app.

  • Your Vite dev server stays running at http://localhost:5173.
  • Your pokedex-app folder remains on your Desktop with all source files intact.
  • You can return anytime and run npm run dev to restart the app if it stops.

Pause - I'll come back to this later

Shut down the running dev server to free up the terminal and port, but keep your project files for later.

  • Switch to the terminal where Vite is running.
  • Press Ctrl+C to stop the dev server.
  • Your pokedex-app folder stays on your Desktop. When you're ready to resume, open a terminal in that folder and run npm run dev.

Delete - I don't want to use this again

Remove all project files and start fresh. This is permanent.

  • Switch to the terminal where Vite is running.
  • Press Ctrl+C to stop the dev server.
  • Navigate to your Desktop by running:
cd ~/Desktop
  • Delete the entire project folder by running:
rm -rf pokedex-app

On Windows, use this command instead:

rmdir /s /q pokedex-app
  • Confirm the folder is gone by running:
ls

You should no longer see pokedex-app in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully interactive Pokedex application from scratch using React and Vite.

You've learned how to:

  • Built a complete React application with reusable components, props, and state management using useState and useEffect.
  • Integrated a live REST API (PokéAPI) using async/await, Promise.all, and real-time search filtering.
  • Created an interactive UI with a responsive CSS Grid layout, type-based color coding, and a modal detail view with stat visualization.
  • Secret Mission: Combined multiple filter criteria (text search and type selection) with a dynamic TypeFilter component.

Ready to quiz yourself?