Build a Fish Keeper's Reference Site

Build a filterable fish species catalog and health guide with vanilla JS.

Introduction

30 Second Summary

Aquarium hobbyists need quick access to care parameters, compatibility info, and disease symptoms for dozens of fish species. A dedicated reference website puts all of that in one searchable, always-available place.

In this project, you will build a Fish Keeper's Reference website with a filterable species catalog, detailed care modals, and a health guide section. You will deploy it live on Netlify so anyone can access it.

What You'll Build

A responsive multi-page static website powered by vanilla JavaScript, featuring dynamic card rendering from data arrays, real-time filtering and search, modal interactions, and CSS Grid layouts.

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

  • A filterable species catalog with 16 fish cards that can be searched by name and filtered by care level and temperament.
  • A species detail modal showing water parameters, diet, tank size, lifespan, and compatible tank mates for each fish.
  • A fish health guide with disease and parasite cards displaying symptoms, causes, treatments, and visual severity indicators.
  • Secret Mission: Add a "Request a Fish" form that captures user submissions through Netlify Forms without any backend code.

Are there any prerequisites?

This project assumes working knowledge of HTML, CSS, and JavaScript fundamentals. You should be comfortable creating HTML elements, writing CSS selectors, and using basic JavaScript (variables, functions, arrays, objects).

Set Up the Project Structure

Every multi-page website starts with a clear file structure. You need HTML pages for each section, a stylesheet for the visual design, and JavaScript files for the interactive behaviour.

In this step, you will create all six project files, write the HTML shell with navigation that links your pages together, and define the colour palette using CSS custom properties.

In this step, get ready to:
  • Create the project files and open them in VS Code.
  • Write the HTML boilerplate with a shared navigation bar across both pages.
  • Add a CSS reset and define custom properties for your colour palette and typography.
Create the project files

Your website needs two HTML pages, one CSS file, and three JavaScript files. The JavaScript files will stay empty for now and get filled in over the next few steps.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the fish-keepers-reference folder on your Desktop, then click Open.

VS Code now shows your empty project folder in the sidebar.

  • Open the integrated terminal by pressing Ctrl+` (macOS) or Ctrl+` (Windows). The backtick key is above the Tab key on your keyboard.
  • Create all six project files by running this command:

macOS

touch index.html health.html styles.css data.js app.js health.js

Windows

New-Item -ItemType "File" -Path "index.html", "health.html", "styles.css", "data.js", "app.js", "health.js"
  • Confirm the files exist by running:
ls

You should see index.html, health.html, styles.css, data.js, app.js, and health.js listed in the output.

Don't see all six files?

Make sure the terminal is pointing to your fish-keepers-reference folder. If you see a different path in the terminal prompt, the folder may not have opened correctly. Try File > Open Folder again and re-select the correct folder.

Help me find my files in VS Code

Write the HTML boilerplate with navigation

Both pages share an identical header with navigation links and a footer. The only differences are the page title, which link is marked as active, and which JavaScript files load at the bottom.

  • Open index.html by clicking it in the Explorer sidebar, then paste this code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link active">Species Catalog</a>
                <a href="health.html" class="nav-link">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
    </main>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="app.js"></script>
</body>
</html>

What does this code do?

  • The <header> contains the site title and a <nav> with links to both pages. The active class marks the current page so it can be styled differently.
  • The <main> element is empty for now. You will fill it with species cards in the next step.
  • The two <script> tags load data.js before app.js. Order matters because app.js will reference data defined in data.js.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Now open health.html from the sidebar and paste this code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Health Guide - Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link">Species Catalog</a>
                <a href="health.html" class="nav-link active">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
    </main>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="health.js"></script>
</body>
</html>

What is different about this page?

The active class has moved to the Health Guide link. The scripts load health.js instead of app.js because each page has its own logic.

  • Save health.html.

Seeing an error squiggle in your HTML?

VS Code may show a yellow warning if it thinks a <script> tag points to an empty file. This is normal and disappears once you add content to those JavaScript files in later steps.

Help me understand this warning

Add the CSS reset and custom properties

CSS custom properties let you define your colour palette and spacing values in one place. Every selector in the stylesheet can reference these variables, so updating a colour later is a single-line change.

  • Open styles.css from the sidebar and paste this code:
:root {
    --color-primary: #1a7a6c;
    --color-primary-light: #2db39e;
    --color-primary-dark: #0f5c52;
    --color-bg: #f0f7f6;
    --color-surface: #ffffff;
    --color-text: #1a2e2a;
    --color-text-light: #4a6b65;
    --color-border: #d4e5e2;
    --color-danger: #d94040;
    --color-warning: #e6a817;
    --color-success: #2d8a4e;
    --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --radius: 8px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: var(--font-main);
    background-color: var(--color-bg);
    color: var(--color-text);
    line-height: 1.6;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

What does this code do?

  • The :root block defines 15 custom properties (variables) that control every colour, font, shadow, and border radius across the entire site.
  • The * selector is a CSS reset. It removes default browser margins and padding, and sets box-sizing: border-box so padding never blows out your layouts.
  • The body uses display: flex with flex-direction: column so the footer always sticks to the bottom of the page, even when content is short.
  • The .container class centres content at a max width of 1200px. You will reuse this class throughout both pages.
  • Below the .container block, add the styles for the header, navigation, and footer:
.site-header {
    background-color: var(--color-primary);
    color: white;
    padding: 16px 0;
    position: sticky;
    top: 0;
    z-index: 100;
    box-shadow: var(--shadow);
}

.site-header .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
}

.site-title {
    font-size: 1.4rem;
    font-weight: 700;
}

.main-nav {
    display: flex;
    gap: 8px;
}

.nav-link {
    color: rgba(255, 255, 255, 0.85);
    text-decoration: none;
    padding: 8px 16px;
    border-radius: var(--radius);
    transition: background-color 0.2s, color 0.2s;
}

.nav-link:hover {
    background-color: rgba(255, 255, 255, 0.15);
    color: white;
}

.nav-link.active {
    background-color: rgba(255, 255, 255, 0.2);
    color: white;
    font-weight: 600;
}

main {
    flex: 1;
    padding: 32px 0;
}

.site-footer {
    background-color: var(--color-primary-dark);
    color: rgba(255, 255, 255, 0.7);
    text-align: center;
    padding: 20px;
    margin-top: auto;
}

What does this code do?

  • The .site-header uses position: sticky so it stays visible as the user scrolls through long species lists.
  • The .nav-link.active selector highlights the current page link with a slightly brighter background and bolder text.
  • The .site-footer uses margin-top: auto which pushes it to the bottom of the page thanks to the flexbox column layout on body.
  • Save styles.css.

Now install a VS Code extension that will let you preview the site in your browser with live reloading.

  • Click the Extensions icon in the left sidebar (it looks like four squares), or press Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Live Server in the search bar.
  • Find the extension by Ritwick Dey and click Install.
  • Once installed, right-click index.html in the Explorer sidebar and select Open with Live Server.

Your browser opens with a teal header showing the site title, two navigation links, and a dark footer at the bottom.

  • Click the Health Guide link in the navigation bar.

The page navigates to health.html and the Health Guide link is now highlighted as active. Both pages share the same styled shell.

Browser shows unstyled text?

Check that your styles.css file is saved. If the header appears as plain black text on white, the CSS file name in your HTML might not match. Open index.html and confirm the link reads href="styles.css" exactly.

Help me fix my CSS loading issue

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file at the end of this step.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link active">Species Catalog</a>
                <a href="health.html" class="nav-link">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
    </main>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="app.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Health Guide - Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link">Species Catalog</a>
                <a href="health.html" class="nav-link active">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
    </main>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="health.js"></script>
</body>
</html>
:root {
    --color-primary: #1a7a6c;
    --color-primary-light: #2db39e;
    --color-primary-dark: #0f5c52;
    --color-bg: #f0f7f6;
    --color-surface: #ffffff;
    --color-text: #1a2e2a;
    --color-text-light: #4a6b65;
    --color-border: #d4e5e2;
    --color-danger: #d94040;
    --color-warning: #e6a817;
    --color-success: #2d8a4e;
    --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --radius: 8px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: var(--font-main);
    background-color: var(--color-bg);
    color: var(--color-text);
    line-height: 1.6;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

.site-header {
    background-color: var(--color-primary);
    color: white;
    padding: 16px 0;
    position: sticky;
    top: 0;
    z-index: 100;
    box-shadow: var(--shadow);
}

.site-header .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
}

.site-title {
    font-size: 1.4rem;
    font-weight: 700;
}

.main-nav {
    display: flex;
    gap: 8px;
}

.nav-link {
    color: rgba(255, 255, 255, 0.85);
    text-decoration: none;
    padding: 8px 16px;
    border-radius: var(--radius);
    transition: background-color 0.2s, color 0.2s;
}

.nav-link:hover {
    background-color: rgba(255, 255, 255, 0.15);
    color: white;
}

.nav-link.active {
    background-color: rgba(255, 255, 255, 0.2);
    color: white;
    font-weight: 600;
}

main {
    flex: 1;
    padding: 32px 0;
}

.site-footer {
    background-color: var(--color-primary-dark);
    color: rgba(255, 255, 255, 0.7);
    text-align: center;
    padding: 20px;
    margin-top: auto;
}

Your project structure is in place and both pages render with a consistent header, navigation, and footer. Next up, you will populate the species catalog with real fish data and render interactive cards.

Build the Species Catalog Layout

Your project files are ready and your local server is running. Now it's time to fill this empty shell with actual fish data and render it into a browsable catalog.

Every data-driven website needs two things: a source of structured data and a function that turns that data into visible elements on the page. In this step, you'll build both.

In this step, get ready to:
  • Create a fish species data array with 16 detailed fish objects.
  • Build a render function that dynamically creates species cards from the data.
  • Style the cards with a CSS Grid layout, hover effects, and responsive behaviour.
Create the fish species data array

The species catalog needs a structured dataset that JavaScript can loop through, filter, and display. Each fish object stores everything an aquarist needs: care level, water parameters, diet, and compatible tank mates.

  • In VS Code, open data.js from the Explorer sidebar.
  • Paste the entire fish species array by copying the following code:
const fishSpecies = [
    {
        id: 1,
        name: "Neon Tetra",
        scientificName: "Paracheirodon innesi",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "20-26°C (68-79°F)",
        pH: "5.0-7.5",
        hardness: "1-10 dGH",
        diet: "Omnivore. Micro pellets, crushed flakes, frozen bloodworms, brine shrimp.",
        lifespan: "3-5 years",
        size: "3.5-4 cm",
        tankMates: "Corydoras, Harlequin Rasboras, Otocinclus, Cherry Shrimp, other small tetras",
        description: "One of the most popular freshwater fish. Brilliant blue-green stripe and red tail. Keep in schools of 10 or more for best behaviour and colour."
    },
    {
        id: 2,
        name: "Cardinal Tetra",
        scientificName: "Paracheirodon axelrodi",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "23-29°C (73-84°F)",
        pH: "5.5-7.0",
        hardness: "4-10 dGH",
        diet: "Omnivore. Frozen brine shrimp, daphnia, micro pellets, bloodworms.",
        lifespan: "4-5 years",
        size: "2-3.5 cm",
        tankMates: "Rummynose Tetras, Ember Tetras, Corydoras, Otocinclus, Hatchetfish",
        description: "Similar to Neon Tetras but with a full red stripe along the body. Prefers soft, acidic water. Keep in groups of 8-12 or more."
    },
    {
        id: 3,
        name: "Betta Fish",
        scientificName: "Betta splendens",
        careLevel: "Easy",
        temperament: "Semi-aggressive",
        tankSize: "19 litres (5 gallons)",
        temperature: "24-28°C (75-82°F)",
        pH: "6.5-7.5",
        hardness: "3-12 dGH",
        diet: "Carnivore. Betta pellets, frozen bloodworms, daphnia, brine shrimp.",
        lifespan: "3-5 years",
        size: "6-7 cm",
        tankMates: "Snails, Corydoras (in 40L+ tanks), avoid other Bettas and fin-nippers",
        description: "Stunning flowing fins and vibrant colours. Males are territorial and must be kept alone or with peaceful, non-flashy tank mates."
    },
    {
        id: 4,
        name: "Platy",
        scientificName: "Xiphophorus maculatus",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "60 litres (13 gallons)",
        temperature: "20-28°C (68-82°F)",
        pH: "6.8-8.0",
        hardness: "10-25 dGH",
        diet: "Omnivore. Flakes, micro pellets, blanched vegetables, spirulina, frozen foods.",
        lifespan: "2-3 years",
        size: "5-7 cm",
        tankMates: "Mollies, Swordtails, Corydoras, Tetras, Guppies",
        description: "One of the best beginner fish. Peaceful, active, and available in dozens of colour forms. Breeds readily in community tanks."
    },
    {
        id: 5,
        name: "Corydoras Catfish",
        scientificName: "Corydoras paleatus",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-26°C (72-79°F)",
        pH: "6.0-7.5",
        hardness: "2-12 dGH",
        diet: "Omnivore. Sinking pellets, wafers, frozen bloodworms, blanched vegetables.",
        lifespan: "5-8 years",
        size: "5-7 cm",
        tankMates: "Tetras, Rasboras, Guppies, Platies, other peaceful community fish",
        description: "Armoured bottom-dwellers that constantly forage the substrate. Social fish that must be kept in groups of 6 or more. Great for cleaning up leftover food."
    },
    {
        id: 6,
        name: "Angelfish",
        scientificName: "Pterophyllum scalare",
        careLevel: "Moderate",
        temperament: "Semi-aggressive",
        tankSize: "120 litres (30 gallons)",
        temperature: "24-30°C (75-86°F)",
        pH: "6.0-7.5",
        hardness: "3-8 dGH",
        diet: "Omnivore. Quality flakes/pellets, frozen bloodworms, daphnia, brine shrimp.",
        lifespan: "8-10 years",
        size: "15 cm body, 20+ cm fin tip to fin tip",
        tankMates: "Larger tetras, Corydoras, peaceful Gouramis, avoid small fish like Neons",
        description: "Elegant cichlid with tall, compressed body. Needs tall tanks (45cm+ height). Territorial during breeding. Not a beginner species."
    },
    {
        id: 7,
        name: "Pearl Gourami",
        scientificName: "Trichopodus leerii",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "115 litres (30 gallons)",
        temperature: "23-28°C (74-82°F)",
        pH: "6.0-7.5",
        hardness: "5-15 dGH",
        diet: "Omnivore. Tropical flakes/pellets, frozen brine shrimp, bloodworms, daphnia.",
        lifespan: "5-8 years",
        size: "10 cm (4 inches)",
        tankMates: "Small tetras, Rasboras, Corydoras, peaceful loaches",
        description: "Beautiful pearl-like iridescent patterning. Labyrinth fish that breathes air at the surface. Shy and peaceful. Keep 1 male with 1-2 females."
    },
    {
        id: 8,
        name: "Cherry Barb",
        scientificName: "Puntius titteya",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "50 litres (13 gallons)",
        temperature: "23-27°C (73-81°F)",
        pH: "6.0-7.0",
        hardness: "4-10 dGH",
        diet: "Omnivore. Flakes, micro pellets, frozen or live brine shrimp, daphnia.",
        lifespan: "4-6 years",
        size: "4-5 cm",
        tankMates: "Tetras, Rasboras, Corydoras, Gouramis, shrimp",
        description: "Hardy, peaceful barb with vibrant cherry-red colouring in males. Unlike many barbs, they are not fin-nippers. Keep in groups of 6+."
    },
    {
        id: 9,
        name: "Guppy",
        scientificName: "Poecilia reticulata",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-28°C (72-82°F)",
        pH: "6.8-7.8",
        hardness: "8-12 dGH",
        diet: "Omnivore. Flakes, micro pellets, frozen bloodworms, brine shrimp, blanched vegetables.",
        lifespan: "2-3 years",
        size: "3-6 cm",
        tankMates: "Platies, Mollies, Corydoras, Tetras, Rasboras",
        description: "Incredibly colourful livebearers with elaborate tail fins. Males are smaller and more colourful. Breeds readily. Keep 1 male per 2-3 females."
    },
    {
        id: 10,
        name: "Dwarf Gourami",
        scientificName: "Trichogaster lalius",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "24-28°C (75-82°F)",
        pH: "6.0-7.5",
        hardness: "4-10 dGH",
        diet: "Omnivore. Tropical flakes, pellets, frozen bloodworms, brine shrimp.",
        lifespan: "4-5 years",
        size: "6-9 cm",
        tankMates: "Tetras, Rasboras, Corydoras, Otocinclus",
        description: "Vibrant colours with striking patterns. Labyrinth fish that gulps air at the surface. Can be shy. Keep only one male per tank to avoid aggression."
    },
    {
        id: 11,
        name: "Otocinclus",
        scientificName: "Otocinclus vittatus",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-26°C (72-79°F)",
        pH: "6.0-7.5",
        hardness: "2-10 dGH",
        diet: "Herbivore. Algae, blanched zucchini, algae wafers, biofilm.",
        lifespan: "3-5 years",
        size: "3-5 cm",
        tankMates: "Tetras, Rasboras, Corydoras, shrimp, peaceful community fish",
        description: "Tiny algae-eating catfish. Excellent for planted tanks. Keep in groups of 6+. Sensitive to poor water quality. Best in established tanks with algae growth."
    },
    {
        id: 12,
        name: "Harlequin Rasbora",
        scientificName: "Trigonostigma heteromorpha",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-27°C (72-80°F)",
        pH: "6.0-7.5",
        hardness: "2-10 dGH",
        diet: "Omnivore. Micro pellets, flakes, frozen daphnia, brine shrimp.",
        lifespan: "5-8 years",
        size: "4-5 cm",
        tankMates: "Tetras, Corydoras, Gouramis, other Rasboras, shrimp",
        description: "Copper-orange body with distinctive black triangular patch. Tight schooling fish. Keep in groups of 8-10+ for impressive shoaling behaviour."
    },
    {
        id: 13,
        name: "Bristlenose Pleco",
        scientificName: "Ancistrus cirrhosus",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "23-27°C (73-81°F)",
        pH: "6.0-7.5",
        hardness: "2-12 dGH",
        diet: "Herbivore. Algae wafers, blanched zucchini, cucumber, driftwood (for fibre).",
        lifespan: "5-8 years",
        size: "12-15 cm",
        tankMates: "Most community fish. Tetras, Rasboras, Barbs, Gouramis",
        description: "Hardy algae eater with distinctive bristle-like growths on males. Nocturnal. Needs driftwood in the tank for digestion. One of the best algae cleaners."
    },
    {
        id: 14,
        name: "Kuhli Loach",
        scientificName: "Pangio kuhlii",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "24-28°C (75-82°F)",
        pH: "5.5-7.0",
        hardness: "1-10 dGH",
        diet: "Omnivore. Sinking pellets, frozen bloodworms, brine shrimp, live foods.",
        lifespan: "10+ years",
        size: "8-12 cm",
        tankMates: "Tetras, Rasboras, Corydoras, Gouramis, other peaceful fish",
        description: "Eel-like striped loach that hides during the day and forages at night. Keep in groups of 5+. Needs sandy substrate and hiding places."
    },
    {
        id: 15,
        name: "Emperor Tetra",
        scientificName: "Nematobrycon palmeri",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "23-27°C (73-81°F)",
        pH: "5.0-7.8",
        hardness: "3-6 dKH",
        diet: "Omnivore. Flakes, pellets, brine shrimp, daphnia, tubifex.",
        lifespan: "6 years",
        size: "5-7.5 cm",
        tankMates: "Corydoras, Rasboras, other peaceful tetras, small peaceful fish",
        description: "Iridescent blue-purple colouration that shifts with the light. Keep in groups of 6+. Males have extended tail fin rays. Stunning in planted tanks."
    },
    {
        id: 16,
        name: "Molly",
        scientificName: "Poecilia sphenops",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "22-28°C (72-82°F)",
        pH: "7.0-8.5",
        hardness: "10-25 dGH",
        diet: "Omnivore. Flakes, pellets, spirulina, blanched vegetables, frozen foods.",
        lifespan: "3-5 years",
        size: "6-10 cm",
        tankMates: "Platies, Guppies, Swordtails, Corydoras, larger tetras",
        description: "Hardy livebearers available in many colour variants (black, dalmatian, sailfin). Prefer harder, more alkaline water. Breeds readily."
    }
];

What does this data do?

Each fish object is a self-contained record with 14 properties. The careLevel and temperament fields will power the filters you build in the next step.

The description field stores the long-form text, but on the card preview you'll only show the first 100 characters. The full details appear later in a modal.

  • Save data.js.

Seeing a syntax error in the console?

Check for missing commas between objects in the array. Every } that closes a fish object (except the last one) needs a comma after it.

Also check that every string value is wrapped in double quotes and that no quote characters appear unescaped inside string values.

Help me fix a syntax error in my data.js fish species array

Build the render function for species cards

The data exists in memory, but nothing appears on the page yet. You need a container in the HTML for the cards to live in, and a JavaScript function that loops through the data and creates DOM elements for each fish.

  • Switch to index.html in VS Code.
  • Find the empty <main class="container"> section.
  • Add the species grid container and a no-results message inside <main> by pasting the following code between the opening and closing <main> tags:
        <section class="species-grid" id="species-grid">
        </section>

        <div class="no-results" id="no-results" hidden>
            <p>No species match your filters. Try adjusting your search.</p>
        </div>

What does this HTML do?

The species-grid section is an empty container. JavaScript will fill it with cards at runtime. The no-results div starts hidden and only appears when no fish match the active filters.

  • Save index.html.

Now you'll write the JavaScript function that reads the fish data and creates a card element for each species.

  • Switch to app.js in VS Code.
  • Add the render function and initial call by pasting the following code:
const speciesGrid = document.getElementById('species-grid');
const noResults = document.getElementById('no-results');

function renderSpeciesCards(species) {
    speciesGrid.innerHTML = '';

    if (species.length === 0) {
        noResults.hidden = false;
        return;
    }

    noResults.hidden = true;

    species.forEach(function(fish) {
        const card = document.createElement('div');
        card.className = 'species-card';
        card.dataset.id = fish.id;

        const badgeClass = fish.careLevel === 'Easy' ? 'badge-easy' :
                          fish.careLevel === 'Moderate' ? 'badge-moderate' : 'badge-advanced';

        card.innerHTML =
            '<div class="species-card-header">' +
                '<h3>' + fish.name + '</h3>' +
                '<span class="badge ' + badgeClass + '">' + fish.careLevel + '</span>' +
            '</div>' +
            '<p class="scientific-name">' + fish.scientificName + '</p>' +
            '<p>' + fish.description.substring(0, 100) + '...</p>' +
            '<div class="species-card-info">' +
                '<span class="info-tag">🌡️ ' + fish.temperature + '</span>' +
                '<span class="info-tag">🐟 ' + fish.temperament + '</span>' +
                '<span class="info-tag">📏 ' + fish.size + '</span>' +
            '</div>';

        speciesGrid.appendChild(card);
    });
}

renderSpeciesCards(fishSpecies);

What does this code do?

  • speciesGrid.innerHTML = '' clears out any existing cards before re-rendering. This becomes important when filters change the visible set.
  • The empty-array check shows the no-results message when no fish match, then exits early so no cards render.
  • badgeClass picks a CSS class based on the fish's care level, giving each badge a different colour.
  • description.substring(0, 100) truncates the description to a short preview so cards stay a consistent height.
  • The final line renderSpeciesCards(fishSpecies) runs the function immediately on page load to populate the grid.
  • Save app.js.
  • Check your browser. You should see 16 white card rectangles inside the main area, each showing a fish name, scientific name, a truncated description, and info tags at the bottom.

Cards not appearing?

Open DevTools with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows) and check the Console tab for errors.

Make sure data.js is loaded before app.js in your HTML. The <script src="data.js"></script> tag must come first.

Also confirm the id="species-grid" attribute on the <section> in your HTML matches the getElementById('species-grid') call in your JS.

Help me debug why my fish species cards aren't rendering on the page

Style the species cards with hover effects and responsive grid

The cards are rendering but they're unstyled. CSS Grid will arrange them into a responsive multi-column layout, and hover effects will make each card feel interactive.

  • Switch to styles.css in VS Code.
  • Find the main { ... } rule (just above .site-footer).
  • Add the grid layout and card base styles below main (before .site-footer) by pasting this code:
.species-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 20px;
}

.species-card {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    padding: 20px;
    box-shadow: var(--shadow);
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
    border: 2px solid transparent;
}

.species-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-hover);
    border-color: var(--color-primary-light);
}

.species-card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 8px;
}

.species-card h3 {
    font-size: 1.1rem;
    color: var(--color-primary-dark);
}

.species-card .scientific-name {
    font-style: italic;
    font-size: 0.85rem;
    color: var(--color-text-light);
    margin-bottom: 12px;
}

What does this CSS do?

  • grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) creates columns that are at least 280px wide and expand to fill available space. The browser automatically adjusts the column count as the window resizes.
  • The transparent border on the card reserves 2px of space so that the hover border doesn't cause a layout shift when it appears.
  • translateY(-2px) lifts the card slightly on hover, creating a subtle "raised" interactive feel.
  • Save styles.css and check your browser.
  • You should see the cards arranged in neat columns. Hovering over a card lifts it slightly with a teal border highlight.

Now add the badge colours, info tags, no-results message, and the responsive breakpoint.

  • Directly below the .species-card .scientific-name rule you just added, paste the following badge and tag styles (still above .site-footer):
.badge {
    display: inline-block;
    padding: 4px 10px;
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.badge-easy {
    background-color: #e0f5ea;
    color: var(--color-success);
}

.badge-moderate {
    background-color: #fff4d6;
    color: #b8860b;
}

.badge-advanced {
    background-color: #fde8e8;
    color: var(--color-danger);
}

.species-card-info {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}

.info-tag {
    font-size: 0.8rem;
    padding: 4px 8px;
    background-color: var(--color-bg);
    border-radius: 4px;
    color: var(--color-text-light);
}

.no-results {
    text-align: center;
    padding: 48px 20px;
    color: var(--color-text-light);
    font-size: 1.1rem;
}

What do the badges show?

Each badge gets a colour-coded background: green for Easy, gold for Moderate, and red for Advanced. This gives users an instant visual signal about a fish's care difficulty without reading the details.

Finally, add a responsive media query at the very end of the file (after the .site-footer rule).

  • Scroll to the bottom of styles.css and paste this media query after the last closing brace:
@media (max-width: 768px) {
    .site-header .container {
        flex-direction: column;
        align-items: flex-start;
    }

    .species-grid {
        grid-template-columns: 1fr;
    }
}

What does the media query do?

On screens narrower than 768px, the grid collapses to a single column so cards stack vertically. The header also switches to a column layout so the navigation wraps beneath the title.

  • Save styles.css and refresh your browser.
  • You should see polished cards in a responsive grid with green, gold, or red care-level badges in the top-right corner of each card.
  • Resize your browser window to a narrow width. The cards should stack into a single column.

Badges not showing colour?

Make sure the class names in your CSS (.badge-easy, .badge-moderate, .badge-advanced) match exactly what your JavaScript generates. These are case-sensitive.

Open DevTools and inspect a badge element. If you see the class applied but no background colour, check for typos in the hex values or variable references.

Help me figure out why my care-level badges have no background colour

✔️ Awesome, I've got everything!

Great. Double check you've saved all four files: data.js, index.html, app.js, and styles.css.

ⓧ I'd like to double check the full code

Here is the full code for each file at this point.

const speciesGrid = document.getElementById('species-grid');
const noResults = document.getElementById('no-results');

function renderSpeciesCards(species) {
    speciesGrid.innerHTML = '';

    if (species.length === 0) {
        noResults.hidden = false;
        return;
    }

    noResults.hidden = true;

    species.forEach(function(fish) {
        const card = document.createElement('div');
        card.className = 'species-card';
        card.dataset.id = fish.id;

        const badgeClass = fish.careLevel === 'Easy' ? 'badge-easy' :
                          fish.careLevel === 'Moderate' ? 'badge-moderate' : 'badge-advanced';

        card.innerHTML =
            '<div class="species-card-header">' +
                '<h3>' + fish.name + '</h3>' +
                '<span class="badge ' + badgeClass + '">' + fish.careLevel + '</span>' +
            '</div>' +
            '<p class="scientific-name">' + fish.scientificName + '</p>' +
            '<p>' + fish.description.substring(0, 100) + '...</p>' +
            '<div class="species-card-info">' +
                '<span class="info-tag">🌡️ ' + fish.temperature + '</span>' +
                '<span class="info-tag">🐟 ' + fish.temperament + '</span>' +
                '<span class="info-tag">📏 ' + fish.size + '</span>' +
            '</div>';

        speciesGrid.appendChild(card);
    });
}

renderSpeciesCards(fishSpecies);
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link active">Species Catalog</a>
                <a href="health.html" class="nav-link">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
        <section class="species-grid" id="species-grid">
        </section>

        <div class="no-results" id="no-results" hidden>
            <p>No species match your filters. Try adjusting your search.</p>
        </div>
    </main>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="app.js"></script>
</body>
</html>
:root {
    --color-primary: #1a7a6c;
    --color-primary-light: #2db39e;
    --color-primary-dark: #0f5c52;
    --color-bg: #f0f7f6;
    --color-surface: #ffffff;
    --color-text: #1a2e2a;
    --color-text-light: #4a6b65;
    --color-border: #d4e5e2;
    --color-danger: #d94040;
    --color-warning: #e6a817;
    --color-success: #2d8a4e;
    --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --radius: 8px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: var(--font-main);
    background-color: var(--color-bg);
    color: var(--color-text);
    line-height: 1.6;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

.site-header {
    background-color: var(--color-primary);
    color: white;
    padding: 16px 0;
    position: sticky;
    top: 0;
    z-index: 100;
    box-shadow: var(--shadow);
}

.site-header .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
}

.site-title {
    font-size: 1.4rem;
    font-weight: 700;
}

.main-nav {
    display: flex;
    gap: 8px;
}

.nav-link {
    color: rgba(255, 255, 255, 0.85);
    text-decoration: none;
    padding: 8px 16px;
    border-radius: var(--radius);
    transition: background-color 0.2s, color 0.2s;
}

.nav-link:hover {
    background-color: rgba(255, 255, 255, 0.15);
    color: white;
}

.nav-link.active {
    background-color: rgba(255, 255, 255, 0.2);
    color: white;
    font-weight: 600;
}

main {
    flex: 1;
    padding: 32px 0;
}

.species-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 20px;
}

.species-card {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    padding: 20px;
    box-shadow: var(--shadow);
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
    border: 2px solid transparent;
}

.species-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-hover);
    border-color: var(--color-primary-light);
}

.species-card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 8px;
}

.species-card h3 {
    font-size: 1.1rem;
    color: var(--color-primary-dark);
}

.species-card .scientific-name {
    font-style: italic;
    font-size: 0.85rem;
    color: var(--color-text-light);
    margin-bottom: 12px;
}

.badge {
    display: inline-block;
    padding: 4px 10px;
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.badge-easy {
    background-color: #e0f5ea;
    color: var(--color-success);
}

.badge-moderate {
    background-color: #fff4d6;
    color: #b8860b;
}

.badge-advanced {
    background-color: #fde8e8;
    color: var(--color-danger);
}

.species-card-info {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}

.info-tag {
    font-size: 0.8rem;
    padding: 4px 8px;
    background-color: var(--color-bg);
    border-radius: 4px;
    color: var(--color-text-light);
}

.no-results {
    text-align: center;
    padding: 48px 20px;
    color: var(--color-text-light);
    font-size: 1.1rem;
}

.site-footer {
    background-color: var(--color-primary-dark);
    color: rgba(255, 255, 255, 0.7);
    text-align: center;
    padding: 20px;
    margin-top: auto;
}

@media (max-width: 768px) {
    .site-header .container {
        flex-direction: column;
        align-items: flex-start;
    }

    .species-grid {
        grid-template-columns: 1fr;
    }
}

Your species catalog is looking sharp. Next up, you'll add filter dropdowns and a search bar so users can narrow down the catalog to find exactly the fish they're looking for.

Add Filtering and Search

Your species catalog now renders 16 fish cards on the page. That looks great, but imagine a user trying to find a specific peaceful fish for their community tank.

Scrolling through every card is tedious. In this step, you'll add dropdown filters for care level and temperament, plus a text search that narrows results in real-time as the user types.

In this step, get ready to:
  • Add filter dropdowns and a search input to the HTML.
  • Write JavaScript filtering logic that updates displayed cards based on active filters.
  • Style the filter controls so they sit neatly above the species grid.
Add the filter and search controls to index.html

The filter controls need to live above the species grid so users see them before the cards. You'll add a search bar for typing a fish name and two dropdown selects for narrowing by care level and temperament.

  • In index.html, find the opening <main class="container"> tag.
  • Add this filters section directly below it, above the existing <section class="species-grid">:
<!-- Filter controls for narrowing species results -->
<section class="filters-section">
    <div class="search-bar">
        <input type="text" id="search-input" placeholder="Search species by name..." aria-label="Search species">
    </div>
    <div class="filter-controls">
        <div class="filter-group">
            <label for="care-filter">Care Level:</label>
            <select id="care-filter">
                <option value="all">All</option>
                <option value="Easy">Easy</option>
                <option value="Moderate">Moderate</option>
                <option value="Advanced">Advanced</option>
            </select>
        </div>
        <div class="filter-group">
            <label for="temperament-filter">Temperament:</label>
            <select id="temperament-filter">
                <option value="all">All</option>
                <option value="Peaceful">Peaceful</option>
                <option value="Semi-aggressive">Semi-aggressive</option>
                <option value="Aggressive">Aggressive</option>
            </select>
        </div>
    </div>
</section>

What does this HTML do?

  • The #search-input is a text field where users type a fish name. The aria-label makes it accessible to screen readers.
  • Each <select> dropdown has an "All" option with value="all" as the default. The other option values match the exact strings in your fish data ("Easy", "Moderate", "Peaceful", etc.).
  • The for attribute on each <label> connects it to its matching <select> by ID. Clicking the label focuses the dropdown.
  • Save index.html.
  • Refresh your browser. You should see an unstyled text input and two dropdown menus above your fish cards.

Don't see the input and dropdowns?

  • Make sure the new <section class="filters-section"> is inside <main> but above the <section class="species-grid"> element.
  • Check you haven't accidentally deleted the closing </section> tag on the filters section.

Help me debug why my filter controls aren't appearing in index.html.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link active">Species Catalog</a>
                <a href="health.html" class="nav-link">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
        <section class="filters-section">
            <div class="search-bar">
                <input type="text" id="search-input" placeholder="Search species by name..." aria-label="Search species">
            </div>
            <div class="filter-controls">
                <div class="filter-group">
                    <label for="care-filter">Care Level:</label>
                    <select id="care-filter">
                        <option value="all">All</option>
                        <option value="Easy">Easy</option>
                        <option value="Moderate">Moderate</option>
                        <option value="Advanced">Advanced</option>
                    </select>
                </div>
                <div class="filter-group">
                    <label for="temperament-filter">Temperament:</label>
                    <select id="temperament-filter">
                        <option value="all">All</option>
                        <option value="Peaceful">Peaceful</option>
                        <option value="Semi-aggressive">Semi-aggressive</option>
                        <option value="Aggressive">Aggressive</option>
                    </select>
                </div>
            </div>
        </section>

        <section class="species-grid" id="species-grid">
        </section>

        <div class="no-results" id="no-results" hidden>
            <p>No species match your filters. Try adjusting your search.</p>
        </div>
    </main>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="app.js"></script>
</body>
</html>
Write the filtering logic in JavaScript

The HTML controls exist but they don't do anything yet. You need JavaScript that reads the current filter values, compares them against each fish object in the array, and re-renders only the matching cards.

  • In app.js, find the line that says const speciesGrid = document.getElementById('species-grid');
  • Add these three new constants directly below it, before the existing noResults line:
const searchInput = document.getElementById('search-input');
const careFilter = document.getElementById('care-filter');
const temperamentFilter = document.getElementById('temperament-filter');

What does this code do?

Each line grabs a reference to one of the HTML elements you just created. Storing them in constants means you only query the DOM once, which is more efficient than calling getElementById every time the user types or selects a filter.

Now you'll add the function that actually performs the filtering. It reads the current search text and dropdown values, then uses the array .filter() method to keep only fish that match all active criteria.

  • In app.js, scroll down to the bottom of the file. Find the line renderSpeciesCards(fishSpecies);
  • Add the following code directly above that line:
// Filter species based on search text and dropdown selections
function filterSpecies() {
    const searchTerm = searchInput.value.toLowerCase().trim();
    const careValue = careFilter.value;
    const temperamentValue = temperamentFilter.value;

    const filtered = fishSpecies.filter(function(fish) {
        const matchesSearch = fish.name.toLowerCase().includes(searchTerm) ||
                            fish.scientificName.toLowerCase().includes(searchTerm);
        const matchesCare = careValue === 'all' || fish.careLevel === careValue;
        const matchesTemperament = temperamentValue === 'all' || fish.temperament === temperamentValue;

        return matchesSearch && matchesCare && matchesTemperament;
    });

    renderSpeciesCards(filtered);
}

// Listen for user input on search and filter controls
searchInput.addEventListener('input', filterSpecies);
careFilter.addEventListener('change', filterSpecies);
temperamentFilter.addEventListener('change', filterSpecies);

What does this code do?

  • filterSpecies() reads the current value of the search input and both dropdowns.
  • .filter() loops through every fish object and keeps only those where ALL three conditions are true: the name or scientific name includes the search term, the care level matches (or is set to "all"), and the temperament matches (or is set to "all").
  • The three addEventListener calls connect each control to the filter function. The search uses the input event (fires on every keystroke) while the dropdowns use change (fires when a new option is selected).
  • Save app.js and refresh your browser.
  • Type "neon" in the search bar. You should see only the Neon Tetra card remain visible.
  • Clear the search and select "Moderate" from the Care Level dropdown. Only fish with a Moderate care level badge should appear.

Filters not working?

  • Open the browser console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). If you see "Cannot read properties of null", it means one of the getElementById calls can't find its element. Double-check that the IDs in your HTML match exactly: search-input, care-filter, temperament-filter.
  • Make sure the option values in your HTML use the exact capitalisation from the data: Easy, Moderate, Peaceful, etc. JavaScript's === comparison is case-sensitive.

Help me debug why my fish species filter isn't working.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

const speciesGrid = document.getElementById('species-grid');
const searchInput = document.getElementById('search-input');
const careFilter = document.getElementById('care-filter');
const temperamentFilter = document.getElementById('temperament-filter');
const noResults = document.getElementById('no-results');

function renderSpeciesCards(species) {
    speciesGrid.innerHTML = '';

    if (species.length === 0) {
        noResults.hidden = false;
        return;
    }

    noResults.hidden = true;

    species.forEach(function(fish) {
        const card = document.createElement('div');
        card.className = 'species-card';
        card.dataset.id = fish.id;

        const badgeClass = fish.careLevel === 'Easy' ? 'badge-easy' :
                          fish.careLevel === 'Moderate' ? 'badge-moderate' : 'badge-advanced';

        card.innerHTML =
            '<div class="species-card-header">' +
                '<h3>' + fish.name + '</h3>' +
                '<span class="badge ' + badgeClass + '">' + fish.careLevel + '</span>' +
            '</div>' +
            '<p class="scientific-name">' + fish.scientificName + '</p>' +
            '<p>' + fish.description.substring(0, 100) + '...</p>' +
            '<div class="species-card-info">' +
                '<span class="info-tag">🌡️ ' + fish.temperature + '</span>' +
                '<span class="info-tag">🐟 ' + fish.temperament + '</span>' +
                '<span class="info-tag">📏 ' + fish.size + '</span>' +
            '</div>';

        speciesGrid.appendChild(card);
    });
}

function filterSpecies() {
    const searchTerm = searchInput.value.toLowerCase().trim();
    const careValue = careFilter.value;
    const temperamentValue = temperamentFilter.value;

    const filtered = fishSpecies.filter(function(fish) {
        const matchesSearch = fish.name.toLowerCase().includes(searchTerm) ||
                            fish.scientificName.toLowerCase().includes(searchTerm);
        const matchesCare = careValue === 'all' || fish.careLevel === careValue;
        const matchesTemperament = temperamentValue === 'all' || fish.temperament === temperamentValue;

        return matchesSearch && matchesCare && matchesTemperament;
    });

    renderSpeciesCards(filtered);
}

searchInput.addEventListener('input', filterSpecies);
careFilter.addEventListener('change', filterSpecies);
temperamentFilter.addEventListener('change', filterSpecies);

renderSpeciesCards(fishSpecies);
Style the filter controls

The filters work, but they look like raw unstyled form elements. You'll use Flexbox to lay out the search bar alongside the dropdowns and give everything consistent spacing and borders.

  • In styles.css, find the rule for main { flex: 1; padding: 32px 0; }
  • Add the following filter styles directly below it:
/* Filter section layout */
.filters-section {
    margin-bottom: 24px;
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    align-items: center;
}

.search-bar {
    flex: 1;
    min-width: 200px;
}

#search-input {
    width: 100%;
    padding: 12px 16px;
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 1rem;
    transition: border-color 0.2s;
}

#search-input:focus {
    outline: none;
    border-color: var(--color-primary);
}

/* Dropdown filter controls */
.filter-controls {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}

.filter-group {
    display: flex;
    align-items: center;
    gap: 8px;
}

.filter-group label {
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--color-text-light);
}

.filter-group select {
    padding: 10px 12px;
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 0.9rem;
    background-color: var(--color-surface);
    cursor: pointer;
}

.filter-group select:focus {
    outline: none;
    border-color: var(--color-primary);
}

What does this CSS do?

  • .filters-section uses display: flex with flex-wrap: wrap so the search bar and dropdowns sit side-by-side on wide screens and stack on narrow ones.
  • flex: 1 on .search-bar makes the search input stretch to fill available space, while min-width: 200px prevents it from getting too narrow.
  • The :focus styles change the border colour to the primary green when a user clicks into the input or dropdown. This provides clear visual feedback.

You also need responsive styles so the filters stack vertically on mobile. These go inside the existing @media (max-width: 768px) block.

  • In styles.css, scroll down to the @media (max-width: 768px) section at the bottom of the file.
  • Add these two rules inside the media query block, below the existing .site-header .container rule:
.filters-section {
    flex-direction: column;
    align-items: stretch;
}

.filter-controls {
    flex-direction: column;
}

What does this code do?

On screens narrower than 768px, the filters switch from a horizontal row to a vertical stack. This prevents the dropdowns from being squished on mobile.

  • Save styles.css and refresh your browser.
  • You should see a polished search bar stretching across the top with the two dropdowns to its right. Clicking into the search field highlights it with a green border.
  • Try selecting "Peaceful" from the Temperament dropdown while typing "cor" in the search bar. Only Corydoras Catfish should remain.

Filters look unstyled or misaligned?

  • Confirm your class name in the HTML is filters-section (with a hyphen) matching the CSS selector .filters-section.
  • Make sure the responsive rules are inside the @media block's curly braces, not below them.

Help me fix my filter section styling in CSS.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

:root {
    --color-primary: #1a7a6c;
    --color-primary-light: #2db39e;
    --color-primary-dark: #0f5c52;
    --color-bg: #f0f7f6;
    --color-surface: #ffffff;
    --color-text: #1a2e2a;
    --color-text-light: #4a6b65;
    --color-border: #d4e5e2;
    --color-danger: #d94040;
    --color-warning: #e6a817;
    --color-success: #2d8a4e;
    --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --radius: 8px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: var(--font-main);
    background-color: var(--color-bg);
    color: var(--color-text);
    line-height: 1.6;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

.site-header {
    background-color: var(--color-primary);
    color: white;
    padding: 16px 0;
    position: sticky;
    top: 0;
    z-index: 100;
    box-shadow: var(--shadow);
}

.site-header .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
}

.site-title {
    font-size: 1.4rem;
    font-weight: 700;
}

.main-nav {
    display: flex;
    gap: 8px;
}

.nav-link {
    color: rgba(255, 255, 255, 0.85);
    text-decoration: none;
    padding: 8px 16px;
    border-radius: var(--radius);
    transition: background-color 0.2s, color 0.2s;
}

.nav-link:hover {
    background-color: rgba(255, 255, 255, 0.15);
    color: white;
}

.nav-link.active {
    background-color: rgba(255, 255, 255, 0.2);
    color: white;
    font-weight: 600;
}

main {
    flex: 1;
    padding: 32px 0;
}

.filters-section {
    margin-bottom: 24px;
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    align-items: center;
}

.search-bar {
    flex: 1;
    min-width: 200px;
}

#search-input {
    width: 100%;
    padding: 12px 16px;
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 1rem;
    transition: border-color 0.2s;
}

#search-input:focus {
    outline: none;
    border-color: var(--color-primary);
}

.filter-controls {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}

.filter-group {
    display: flex;
    align-items: center;
    gap: 8px;
}

.filter-group label {
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--color-text-light);
}

.filter-group select {
    padding: 10px 12px;
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 0.9rem;
    background-color: var(--color-surface);
    cursor: pointer;
}

.filter-group select:focus {
    outline: none;
    border-color: var(--color-primary);
}

.species-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 20px;
}

.species-card {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    padding: 20px;
    box-shadow: var(--shadow);
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
    border: 2px solid transparent;
}

.species-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-hover);
    border-color: var(--color-primary-light);
}

.species-card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 8px;
}

.species-card h3 {
    font-size: 1.1rem;
    color: var(--color-primary-dark);
}

.species-card .scientific-name {
    font-style: italic;
    font-size: 0.85rem;
    color: var(--color-text-light);
    margin-bottom: 12px;
}

.badge {
    display: inline-block;
    padding: 4px 10px;
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.badge-easy {
    background-color: #e0f5ea;
    color: var(--color-success);
}

.badge-moderate {
    background-color: #fff4d6;
    color: #b8860b;
}

.badge-advanced {
    background-color: #fde8e8;
    color: var(--color-danger);
}

.species-card-info {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}

.info-tag {
    font-size: 0.8rem;
    padding: 4px 8px;
    background-color: var(--color-bg);
    border-radius: 4px;
    color: var(--color-text-light);
}

.no-results {
    text-align: center;
    padding: 48px 20px;
    color: var(--color-text-light);
    font-size: 1.1rem;
}

.site-footer {
    background-color: var(--color-primary-dark);
    color: rgba(255, 255, 255, 0.7);
    text-align: center;
    padding: 20px;
    margin-top: auto;
}

@media (max-width: 768px) {
    .site-header .container {
        flex-direction: column;
        align-items: flex-start;
    }

    .filters-section {
        flex-direction: column;
        align-items: stretch;
    }

    .filter-controls {
        flex-direction: column;
    }

    .species-grid {
        grid-template-columns: 1fr;
    }
}

Your species catalog now lets users narrow down results instantly. Next up, you'll make those cards interactive by building a detail modal that shows full care information when a card is clicked.

Create the Species Detail Modal

Your species catalog now shows filterable cards with key info at a glance. But aquarium hobbyists need more than a summary.

In this step, you'll build a modal panel that appears when a card is clicked. It displays the full care profile for each species: water parameters, diet, tank size, and compatible tank mates. You'll also wire up multiple ways to dismiss the modal.

In this step, get ready to:
  • Build a modal HTML structure that overlays the page when a species card is clicked.
  • Populate the detail view with full care information including water parameters, diet, and tank mates.
  • Add a close button, overlay click-to-dismiss, and Escape key functionality.
Build the modal HTML structure

A modal is a panel that appears on top of the page content, blocking interaction with everything behind it until the user dismisses it. You need a hidden overlay container in the HTML that JavaScript can show and populate on demand.

  • In index.html, find the closing </main> tag.
  • Add the following modal structure between </main> and the <footer> element:
    <div class="modal-overlay" id="modal-overlay" hidden>
        <div class="modal" id="species-modal">
            <button class="modal-close" id="modal-close" aria-label="Close modal">&times;</button>
            <div class="modal-content" id="modal-content">
            </div>
        </div>
    </div>

What does this HTML do?

The hidden attribute keeps the entire overlay invisible until JavaScript removes it. The .modal-overlay covers the full screen with a dark background. The .modal is the white content panel sitting in the centre.

The .modal-content div is empty because JavaScript will fill it dynamically with the clicked fish's data.

  • Save index.html.
  • You won't see a visible change yet because the modal starts hidden. The next chunk adds the CSS that styles it.

Now add the CSS that controls how the modal overlay and panel look when they become visible.

  • In styles.css, add the following modal styles above the .site-footer rule:
.modal-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: 200;
    padding: 20px;
}

.modal {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    max-width: 600px;
    width: 100%;
    max-height: 80vh;
    overflow-y: auto;
    padding: 32px;
    position: relative;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}

.modal-close {
    position: absolute;
    top: 12px;
    right: 16px;
    background: none;
    border: none;
    font-size: 2rem;
    cursor: pointer;
    color: var(--color-text-light);
    line-height: 1;
}

.modal-close:hover {
    color: var(--color-text);
}

.modal-content h2 {
    color: var(--color-primary-dark);
    margin-bottom: 4px;
}

.modal-content .scientific-name {
    font-style: italic;
    color: var(--color-text-light);
    margin-bottom: 16px;
}

.modal-content .detail-section {
    margin-bottom: 16px;
}

.modal-content .detail-section h4 {
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--color-primary);
    margin-bottom: 6px;
}

.modal-content .params-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 8px;
}

.param-item {
    background-color: var(--color-bg);
    padding: 10px 12px;
    border-radius: var(--radius);
}

.param-item .param-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    color: var(--color-text-light);
    margin-bottom: 2px;
}

.param-item .param-value {
    font-weight: 600;
    font-size: 0.95rem;
}

What does this CSS do?

The overlay uses position: fixed to cover the entire viewport. Flexbox centres the modal panel vertically and horizontally. The z-index: 200 ensures the modal sits above the sticky header (which has z-index: 100).

The .params-grid uses CSS Grid with auto-fit to wrap parameter cards responsively inside the modal. Each .param-item displays a label and value as a tidy info tile.

  • Also add this rule inside your existing @media (max-width: 768px) block, after the .species-grid rule:
    .modal {
        padding: 20px;
        max-height: 90vh;
    }
  • Save styles.css.
  • Refresh your browser. You won't see a change yet because the modal is hidden until JavaScript shows it.

Seeing a CSS error or broken layout?

Check that every opening brace { has a matching closing brace }. A missing brace will cascade errors through the rest of the stylesheet.

Make sure the new modal responsive rule is inside the @media block, not after its closing brace.

My modal styles aren't working properly.

Populate the detail view with care information

With the HTML skeleton and CSS ready, you need JavaScript to connect card clicks to the modal. Each card needs a click event that passes that fish's data object into a function that builds the detail view.

  • In app.js, add these three constants directly below the existing noResults constant:
const modalOverlay = document.getElementById('modal-overlay');
const modalContent = document.getElementById('modal-content');
const modalClose = document.getElementById('modal-close');

What do these constants do?

These cache references to the modal's DOM elements so you can show, hide, and populate the modal without searching the DOM each time. Caching references in constants at the top of the file is a performance pattern you'll see in most vanilla JS apps.

Next, each species card needs a click event so it opens the modal with that specific fish's data.

  • Inside the renderSpeciesCards function, find this line:
        speciesGrid.appendChild(card);
  • Add this click listener directly above that line, so the card triggers openModal before it's appended to the grid:
        card.addEventListener('click', function() {
            openModal(fish);
        });

Now write the openModal function that builds the detail view HTML from the fish object.

  • Add this function below the renderSpeciesCards function:
function openModal(fish) {
    const badgeClass = fish.careLevel === 'Easy' ? 'badge-easy' :
                      fish.careLevel === 'Moderate' ? 'badge-moderate' : 'badge-advanced';

    modalContent.innerHTML =
        '<h2>' + fish.name + '</h2>' +
        '<p class="scientific-name">' + fish.scientificName + '</p>' +
        '<span class="badge ' + badgeClass + '">' + fish.careLevel + '</span>' +

        '<div class="detail-section">' +
            '<h4>About</h4>' +
            '<p>' + fish.description + '</p>' +
        '</div>' +

        '<div class="detail-section">' +
            '<h4>Water Parameters</h4>' +
            '<div class="params-grid">' +
                '<div class="param-item">' +
                    '<div class="param-label">Temperature</div>' +
                    '<div class="param-value">' + fish.temperature + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">pH</div>' +
                    '<div class="param-value">' + fish.pH + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Hardness</div>' +
                    '<div class="param-value">' + fish.hardness + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Tank Size</div>' +
                    '<div class="param-value">' + fish.tankSize + '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +

        '<div class="detail-section">' +
            '<h4>Care Details</h4>' +
            '<div class="params-grid">' +
                '<div class="param-item">' +
                    '<div class="param-label">Diet</div>' +
                    '<div class="param-value">' + fish.diet + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Size</div>' +
                    '<div class="param-value">' + fish.size + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Lifespan</div>' +
                    '<div class="param-value">' + fish.lifespan + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Temperament</div>' +
                    '<div class="param-value">' + fish.temperament + '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +

        '<div class="detail-section">' +
            '<h4>Compatible Tank Mates</h4>' +
            '<p>' + fish.tankMates + '</p>' +
        '</div>';

    modalOverlay.hidden = false;
    document.body.style.overflow = 'hidden';
}

What does openModal do?

It receives the full fish data object and builds an HTML string with four detail sections: About (the description), Water Parameters (temperature, pH, hardness, tank size), Care Details (diet, size, lifespan, temperament), and Compatible Tank Mates.

Setting modalOverlay.hidden = false reveals the overlay. Setting document.body.style.overflow = 'hidden' prevents the page from scrolling behind the modal while it's open.

  • Save app.js and refresh your browser.
  • Click any species card. You should see a dark overlay appear with a white panel showing the fish's name, scientific name, care badge, and all four detail sections (About, Water Parameters, Care Details, Compatible Tank Mates).

Modal not appearing when you click a card?

Open your browser's DevTools console (F12) and look for red error messages. A common cause is a typo in the element IDs. Confirm that modal-overlay in your HTML matches what getElementById expects in app.js.

Also check that the card.addEventListener('click', ...) line is inside the forEach loop, before speciesGrid.appendChild(card).

Help me debug why my species detail modal isn't opening on card click.

Add close and overlay dismiss functionality

The modal opens, but you can't close it yet. Users expect three ways to dismiss a modal: clicking the X button, clicking the dark overlay area, or pressing the Escape key.

  • In app.js, add the closeModal function directly below your openModal function:
function closeModal() {
    modalOverlay.hidden = true;
    document.body.style.overflow = '';
}
  • Now add these three event listeners below your existing filter event listeners (the searchInput, careFilter, and temperamentFilter listeners):
// Close modal via the X button
modalClose.addEventListener('click', closeModal);

// Close modal by clicking the dark overlay (not the panel itself)
modalOverlay.addEventListener('click', function(event) {
    if (event.target === modalOverlay) {
        closeModal();
    }
});

// Close modal with the Escape key
document.addEventListener('keydown', function(event) {
    if (event.key === 'Escape' && !modalOverlay.hidden) {
        closeModal();
    }
});

Why check event.target for the overlay click?

Click events bubble up through the DOM. Without the event.target === modalOverlay check, clicking anywhere inside the modal panel would also close it (since those clicks bubble to the overlay). The check ensures only direct clicks on the dark background dismiss the modal.

The Escape listener also checks !modalOverlay.hidden so it only fires when the modal is actually open. No point running close logic when there's nothing to close.

  • Save app.js and refresh your browser.
  • Click a species card to open the modal. Then try all three dismiss methods: click the X button in the top-right corner, click the dark area outside the white panel, and press the Escape key. Each one should close the modal and restore page scrolling.

Clicking the overlay closes the modal but clicking X doesn't?

Make sure the modalClose constant at the top of your file references the ID modal-close (with a hyphen), matching the button's ID in your HTML.

If clicking inside the panel accidentally closes the modal, double check that your overlay listener has the if (event.target === modalOverlay) guard.

Help me fix my modal close functionality.

✔️ Awesome, I've got everything!

Great. Double check that you've saved all three files (index.html, app.js, styles.css).

ⓧ I'd like to double check the full code

Here is the full index.html at this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link active">Species Catalog</a>
                <a href="health.html" class="nav-link">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
        <section class="filters-section">
            <div class="search-bar">
                <input type="text" id="search-input" placeholder="Search species by name..." aria-label="Search species">
            </div>
            <div class="filter-controls">
                <div class="filter-group">
                    <label for="care-filter">Care Level:</label>
                    <select id="care-filter">
                        <option value="all">All</option>
                        <option value="Easy">Easy</option>
                        <option value="Moderate">Moderate</option>
                        <option value="Advanced">Advanced</option>
                    </select>
                </div>
                <div class="filter-group">
                    <label for="temperament-filter">Temperament:</label>
                    <select id="temperament-filter">
                        <option value="all">All</option>
                        <option value="Peaceful">Peaceful</option>
                        <option value="Semi-aggressive">Semi-aggressive</option>
                        <option value="Aggressive">Aggressive</option>
                    </select>
                </div>
            </div>
        </section>

        <section class="species-grid" id="species-grid">
        </section>

        <div class="no-results" id="no-results" hidden>
            <p>No species match your filters. Try adjusting your search.</p>
        </div>
    </main>

    <div class="modal-overlay" id="modal-overlay" hidden>
        <div class="modal" id="species-modal">
            <button class="modal-close" id="modal-close" aria-label="Close modal">&times;</button>
            <div class="modal-content" id="modal-content">
            </div>
        </div>
    </div>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="app.js"></script>
</body>
</html>

Here is the full app.js at this point:

const speciesGrid = document.getElementById('species-grid');
const searchInput = document.getElementById('search-input');
const careFilter = document.getElementById('care-filter');
const temperamentFilter = document.getElementById('temperament-filter');
const noResults = document.getElementById('no-results');
const modalOverlay = document.getElementById('modal-overlay');
const modalContent = document.getElementById('modal-content');
const modalClose = document.getElementById('modal-close');

function renderSpeciesCards(species) {
    speciesGrid.innerHTML = '';

    if (species.length === 0) {
        noResults.hidden = false;
        return;
    }

    noResults.hidden = true;

    species.forEach(function(fish) {
        const card = document.createElement('div');
        card.className = 'species-card';
        card.dataset.id = fish.id;

        const badgeClass = fish.careLevel === 'Easy' ? 'badge-easy' :
                          fish.careLevel === 'Moderate' ? 'badge-moderate' : 'badge-advanced';

        card.innerHTML =
            '<div class="species-card-header">' +
                '<h3>' + fish.name + '</h3>' +
                '<span class="badge ' + badgeClass + '">' + fish.careLevel + '</span>' +
            '</div>' +
            '<p class="scientific-name">' + fish.scientificName + '</p>' +
            '<p>' + fish.description.substring(0, 100) + '...</p>' +
            '<div class="species-card-info">' +
                '<span class="info-tag">🌡️ ' + fish.temperature + '</span>' +
                '<span class="info-tag">🐟 ' + fish.temperament + '</span>' +
                '<span class="info-tag">📏 ' + fish.size + '</span>' +
            '</div>';

        card.addEventListener('click', function() {
            openModal(fish);
        });

        speciesGrid.appendChild(card);
    });
}

function openModal(fish) {
    const badgeClass = fish.careLevel === 'Easy' ? 'badge-easy' :
                      fish.careLevel === 'Moderate' ? 'badge-moderate' : 'badge-advanced';

    modalContent.innerHTML =
        '<h2>' + fish.name + '</h2>' +
        '<p class="scientific-name">' + fish.scientificName + '</p>' +
        '<span class="badge ' + badgeClass + '">' + fish.careLevel + '</span>' +

        '<div class="detail-section">' +
            '<h4>About</h4>' +
            '<p>' + fish.description + '</p>' +
        '</div>' +

        '<div class="detail-section">' +
            '<h4>Water Parameters</h4>' +
            '<div class="params-grid">' +
                '<div class="param-item">' +
                    '<div class="param-label">Temperature</div>' +
                    '<div class="param-value">' + fish.temperature + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">pH</div>' +
                    '<div class="param-value">' + fish.pH + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Hardness</div>' +
                    '<div class="param-value">' + fish.hardness + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Tank Size</div>' +
                    '<div class="param-value">' + fish.tankSize + '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +

        '<div class="detail-section">' +
            '<h4>Care Details</h4>' +
            '<div class="params-grid">' +
                '<div class="param-item">' +
                    '<div class="param-label">Diet</div>' +
                    '<div class="param-value">' + fish.diet + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Size</div>' +
                    '<div class="param-value">' + fish.size + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Lifespan</div>' +
                    '<div class="param-value">' + fish.lifespan + '</div>' +
                '</div>' +
                '<div class="param-item">' +
                    '<div class="param-label">Temperament</div>' +
                    '<div class="param-value">' + fish.temperament + '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +

        '<div class="detail-section">' +
            '<h4>Compatible Tank Mates</h4>' +
            '<p>' + fish.tankMates + '</p>' +
        '</div>';

    modalOverlay.hidden = false;
    document.body.style.overflow = 'hidden';
}

function closeModal() {
    modalOverlay.hidden = true;
    document.body.style.overflow = '';
}

function filterSpecies() {
    const searchTerm = searchInput.value.toLowerCase().trim();
    const careValue = careFilter.value;
    const temperamentValue = temperamentFilter.value;

    const filtered = fishSpecies.filter(function(fish) {
        const matchesSearch = fish.name.toLowerCase().includes(searchTerm) ||
                            fish.scientificName.toLowerCase().includes(searchTerm);
        const matchesCare = careValue === 'all' || fish.careLevel === careValue;
        const matchesTemperament = temperamentValue === 'all' || fish.temperament === temperamentValue;

        return matchesSearch && matchesCare && matchesTemperament;
    });

    renderSpeciesCards(filtered);
}

searchInput.addEventListener('input', filterSpecies);
careFilter.addEventListener('change', filterSpecies);
temperamentFilter.addEventListener('change', filterSpecies);

modalClose.addEventListener('click', closeModal);
modalOverlay.addEventListener('click', function(event) {
    if (event.target === modalOverlay) {
        closeModal();
    }
});

document.addEventListener('keydown', function(event) {
    if (event.key === 'Escape' && !modalOverlay.hidden) {
        closeModal();
    }
});

renderSpeciesCards(fishSpecies);

Here is the full styles.css at this point:

:root {
    --color-primary: #1a7a6c;
    --color-primary-light: #2db39e;
    --color-primary-dark: #0f5c52;
    --color-bg: #f0f7f6;
    --color-surface: #ffffff;
    --color-text: #1a2e2a;
    --color-text-light: #4a6b65;
    --color-border: #d4e5e2;
    --color-danger: #d94040;
    --color-warning: #e6a817;
    --color-success: #2d8a4e;
    --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --radius: 8px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: var(--font-main);
    background-color: var(--color-bg);
    color: var(--color-text);
    line-height: 1.6;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

.site-header {
    background-color: var(--color-primary);
    color: white;
    padding: 16px 0;
    position: sticky;
    top: 0;
    z-index: 100;
    box-shadow: var(--shadow);
}

.site-header .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
}

.site-title {
    font-size: 1.4rem;
    font-weight: 700;
}

.main-nav {
    display: flex;
    gap: 8px;
}

.nav-link {
    color: rgba(255, 255, 255, 0.85);
    text-decoration: none;
    padding: 8px 16px;
    border-radius: var(--radius);
    transition: background-color 0.2s, color 0.2s;
}

.nav-link:hover {
    background-color: rgba(255, 255, 255, 0.15);
    color: white;
}

.nav-link.active {
    background-color: rgba(255, 255, 255, 0.2);
    color: white;
    font-weight: 600;
}

main {
    flex: 1;
    padding: 32px 0;
}

.filters-section {
    margin-bottom: 24px;
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    align-items: center;
}

.search-bar {
    flex: 1;
    min-width: 200px;
}

#search-input {
    width: 100%;
    padding: 12px 16px;
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 1rem;
    transition: border-color 0.2s;
}

#search-input:focus {
    outline: none;
    border-color: var(--color-primary);
}

.filter-controls {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}

.filter-group {
    display: flex;
    align-items: center;
    gap: 8px;
}

.filter-group label {
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--color-text-light);
}

.filter-group select {
    padding: 10px 12px;
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 0.9rem;
    background-color: var(--color-surface);
    cursor: pointer;
}

.filter-group select:focus {
    outline: none;
    border-color: var(--color-primary);
}

.species-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 20px;
}

.species-card {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    padding: 20px;
    box-shadow: var(--shadow);
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
    border: 2px solid transparent;
}

.species-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-hover);
    border-color: var(--color-primary-light);
}

.species-card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 8px;
}

.species-card h3 {
    font-size: 1.1rem;
    color: var(--color-primary-dark);
}

.species-card .scientific-name {
    font-style: italic;
    font-size: 0.85rem;
    color: var(--color-text-light);
    margin-bottom: 12px;
}

.badge {
    display: inline-block;
    padding: 4px 10px;
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
}

.badge-easy {
    background-color: #e0f5ea;
    color: var(--color-success);
}

.badge-moderate {
    background-color: #fff4d6;
    color: #b8860b;
}

.badge-advanced {
    background-color: #fde8e8;
    color: var(--color-danger);
}

.species-card-info {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}

.info-tag {
    font-size: 0.8rem;
    padding: 4px 8px;
    background-color: var(--color-bg);
    border-radius: 4px;
    color: var(--color-text-light);
}

.no-results {
    text-align: center;
    padding: 48px 20px;
    color: var(--color-text-light);
    font-size: 1.1rem;
}

.modal-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: 200;
    padding: 20px;
}

.modal {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    max-width: 600px;
    width: 100%;
    max-height: 80vh;
    overflow-y: auto;
    padding: 32px;
    position: relative;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}

.modal-close {
    position: absolute;
    top: 12px;
    right: 16px;
    background: none;
    border: none;
    font-size: 2rem;
    cursor: pointer;
    color: var(--color-text-light);
    line-height: 1;
}

.modal-close:hover {
    color: var(--color-text);
}

.modal-content h2 {
    color: var(--color-primary-dark);
    margin-bottom: 4px;
}

.modal-content .scientific-name {
    font-style: italic;
    color: var(--color-text-light);
    margin-bottom: 16px;
}

.modal-content .detail-section {
    margin-bottom: 16px;
}

.modal-content .detail-section h4 {
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--color-primary);
    margin-bottom: 6px;
}

.modal-content .params-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 8px;
}

.param-item {
    background-color: var(--color-bg);
    padding: 10px 12px;
    border-radius: var(--radius);
}

.param-item .param-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    color: var(--color-text-light);
    margin-bottom: 2px;
}

.param-item .param-value {
    font-weight: 600;
    font-size: 0.95rem;
}

.site-footer {
    background-color: var(--color-primary-dark);
    color: rgba(255, 255, 255, 0.7);
    text-align: center;
    padding: 20px;
    margin-top: auto;
}

@media (max-width: 768px) {
    .site-header .container {
        flex-direction: column;
        align-items: flex-start;
    }

    .filters-section {
        flex-direction: column;
        align-items: stretch;
    }

    .filter-controls {
        flex-direction: column;
    }

    .species-grid {
        grid-template-columns: 1fr;
    }

    .modal {
        padding: 20px;
        max-height: 90vh;
    }
}

Your species catalog now has full detail views with all the care information a fishkeeper needs. Next up, you'll build the Health Guide page so users can identify and treat common fish diseases.

Build the Fish Health Guide

Your species catalog now has interactive detail views that surface the full care profile for each fish. The next challenge is helping fishkeepers when something goes wrong.

In this step, you'll build a dedicated health guide on the second page of your site. You'll create a disease data array, render it into styled cards with severity indicators, and give users a quick-reference tool for diagnosing and treating common fish illnesses.

In this step, get ready to:
  • Create a health data array covering 8 common diseases and parasites.
  • Render disease cards on the health page with symptoms, causes, and treatments.
  • Style the health section with colour-coded severity indicators.
Create the health data array

Just like the species catalog, the health guide is powered by a JavaScript data array. Each disease object stores the name, type, severity level, cause, symptoms list, treatment instructions, and prevention tips.

  • In data.js, scroll to the very bottom of the file (after the closing ]; of the fishSpecies array).
  • Add the following fishDiseases array below it:
const fishDiseases = [
    {
        id: 1,
        name: "Ich (White Spot Disease)",
        type: "Parasite",
        severity: "high",
        causedBy: "Ichthyophthirius multifiliis (protozoan parasite)",
        symptoms: [
            "White pinhead-sized spots on body and fins",
            "Fish rubbing against objects (flashing)",
            "Rapid breathing",
            "Lethargy and loss of appetite",
            "Clamped fins"
        ],
        treatment: "Raise temperature to 30°C (86°F) for 10-14 days if species tolerates it, OR use ich-specific medication (such as malachite green or copper-based treatments) for 7-14 days. Treat the entire tank. Remove activated carbon from filter during treatment.",
        prevention: "Quarantine new fish for 2-4 weeks. Avoid sudden temperature drops. Maintain stable water parameters and low stress."
    },
    {
        id: 2,
        name: "Fin Rot",
        type: "Bacterial",
        severity: "medium",
        causedBy: "Opportunistic bacteria (Aeromonas, Pseudomonas) triggered by poor water quality",
        symptoms: [
            "Frayed, ragged, or decaying fin edges",
            "Whitish edges on fins",
            "Redness or inflammation at fin base",
            "Progressive fin tissue loss",
            "Decreased activity"
        ],
        treatment: "Start with 50% water change and correct water parameters. If no improvement after a week, add aquarium salt (1 tbsp per 5 gallons). For severe cases, use antibiotics. Most mild cases resolve with clean water alone.",
        prevention: "Maintain 0 ammonia, 0 nitrite, and nitrate under 20 ppm. Perform weekly 20-25% water changes. Avoid overcrowding and aggressive tank mates."
    },
    {
        id: 3,
        name: "Velvet Disease",
        type: "Parasite",
        severity: "high",
        causedBy: "Piscinoodinium (dinoflagellate parasite, partly photosynthetic)",
        symptoms: [
            "Gold or rust-coloured dusty appearance on skin",
            "Clamped fins",
            "Rubbing against objects",
            "Heavy breathing and lethargy",
            "Loss of appetite"
        ],
        treatment: "Total darkness (parasite is photosynthetic) combined with copper-based medication for 14 days. Move to hospital tank if you have invertebrates, as copper is lethal to shrimp and snails.",
        prevention: "Quarantine all new fish. Maintain excellent water quality. Velvet is more aggressive than ich and can kill within days if untreated."
    },
    {
        id: 4,
        name: "Dropsy",
        type: "Bacterial/Organ Failure",
        severity: "high",
        causedBy: "Usually kidney failure from bacterial infection (Aeromonas), chronic stress, or poor water quality over time",
        symptoms: [
            "Swollen/bloated abdomen",
            "Scales protruding outward (pineconing)",
            "Lethargy",
            "Loss of appetite",
            "Difficulty swimming"
        ],
        treatment: "Low success rate once pineconing is visible. Move to hospital tank. Add Epsom salt (magnesium sulfate) at 1 tbsp per 5 gallons to reduce fluid retention. Medicated food with antibiotics may help if caught very early.",
        prevention: "Maintain pristine water quality long-term. Reduce stress factors. Feed a varied, high-quality diet. Regular water changes are the best prevention."
    },
    {
        id: 5,
        name: "Columnaris",
        type: "Bacterial",
        severity: "high",
        causedBy: "Flavobacterium columnare (bacterial, often mistaken for fungus)",
        symptoms: [
            "White or grey cotton-like patches on body, fins, or mouth",
            "Frayed fins with white edges",
            "Mouth erosion (mouth rot)",
            "Rapid progression (can kill in 24-72 hours)"
        ],
        treatment: "Move to hospital tank immediately. Lower temperature to 24°C (75°F) as columnaris thrives in warmth. Use antibiotics (kanamycin or nitrofurazone). Requires aggressive, immediate treatment.",
        prevention: "Avoid overstocking. Maintain clean water. Columnaris spreads fast in warm water above 27°C (80°F). Quarantine new fish."
    },
    {
        id: 6,
        name: "Swim Bladder Disease",
        type: "Dietary/Bacterial",
        severity: "medium",
        causedBy: "Constipation, overfeeding, bacterial infection, or physical injury to swim bladder",
        symptoms: [
            "Fish floating to one side or upside down",
            "Difficulty maintaining position in water",
            "Sinking to bottom and unable to rise",
            "Bloated abdomen"
        ],
        treatment: "Fast the fish for 2-3 days. On day 3-4, offer blanched, deshelled pea (fibre helps move blockage) or frozen daphnia. If bacterial, antibiotics may be needed. Adjust water temperature slightly warmer.",
        prevention: "Avoid overfeeding. Soak dry foods before feeding. Feed varied diet with fibre. Offer small meals rather than one large feeding."
    },
    {
        id: 7,
        name: "Fish Flukes",
        type: "Parasite",
        severity: "medium",
        causedBy: "Gyrodactylus (skin flukes) and Dactylogyrus (gill flukes) parasites",
        symptoms: [
            "Loss of colour",
            "Difficulty breathing and flared gills",
            "Excess mucus production",
            "Listlessness and clamped fins",
            "Small blood spots on fins and body"
        ],
        treatment: "Praziquantel is the safest and most effective treatment. Treat the whole tank. Multiple treatments may be needed for egg-laying species. Monitor for secondary bacterial infections on damaged tissue.",
        prevention: "Quarantine all new fish for 2-4 weeks. Maintain good water quality. Avoid overcrowding and stress."
    },
    {
        id: 8,
        name: "Fungal Infection",
        type: "Fungal",
        severity: "medium",
        causedBy: "Saprolegnia and related water moulds. Typically secondary to wounds or weakened immune system",
        symptoms: [
            "White or grey cotton-like growths on skin, fins, or eyes",
            "Growths typically form around wounds or damaged areas",
            "Lethargy",
            "Loss of appetite"
        ],
        treatment: "Improve water quality immediately. Use antifungal medication. Remove affected fish to hospital tank if growths are severe. Treat underlying cause (wound, stress, poor water quality).",
        prevention: "Avoid injuries from sharp decorations. Keep water temperature stable and within species range. Fish in cold water below their optimal range are especially susceptible."
    }
];

What does this data represent?

Each disease object follows the same structure: an id for identification, type to categorise (Parasite, Bacterial, Fungal), and severity to drive visual colour-coding later.

The symptoms field is an array of strings. This lets you render each symptom as its own list item in the HTML, making cards scannable at a glance.

  • Save data.js.
  • You won't see a change yet because nothing reads this array. The next chunk connects it to the health page.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full data.js at this point. The fishSpecies array is unchanged. The new fishDiseases array sits below it.

const fishSpecies = [
    {
        id: 1,
        name: "Neon Tetra",
        scientificName: "Paracheirodon innesi",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "20-26°C (68-79°F)",
        pH: "5.0-7.5",
        hardness: "1-10 dGH",
        diet: "Omnivore. Micro pellets, crushed flakes, frozen bloodworms, brine shrimp.",
        lifespan: "3-5 years",
        size: "3.5-4 cm",
        tankMates: "Corydoras, Harlequin Rasboras, Otocinclus, Cherry Shrimp, other small tetras",
        description: "One of the most popular freshwater fish. Brilliant blue-green stripe and red tail. Keep in schools of 10 or more for best behaviour and colour."
    },
    {
        id: 2,
        name: "Cardinal Tetra",
        scientificName: "Paracheirodon axelrodi",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "23-29°C (73-84°F)",
        pH: "5.5-7.0",
        hardness: "4-10 dGH",
        diet: "Omnivore. Frozen brine shrimp, daphnia, micro pellets, bloodworms.",
        lifespan: "4-5 years",
        size: "2-3.5 cm",
        tankMates: "Rummynose Tetras, Ember Tetras, Corydoras, Otocinclus, Hatchetfish",
        description: "Similar to Neon Tetras but with a full red stripe along the body. Prefers soft, acidic water. Keep in groups of 8-12 or more."
    },
    {
        id: 3,
        name: "Betta Fish",
        scientificName: "Betta splendens",
        careLevel: "Easy",
        temperament: "Semi-aggressive",
        tankSize: "19 litres (5 gallons)",
        temperature: "24-28°C (75-82°F)",
        pH: "6.5-7.5",
        hardness: "3-12 dGH",
        diet: "Carnivore. Betta pellets, frozen bloodworms, daphnia, brine shrimp.",
        lifespan: "3-5 years",
        size: "6-7 cm",
        tankMates: "Snails, Corydoras (in 40L+ tanks), avoid other Bettas and fin-nippers",
        description: "Stunning flowing fins and vibrant colours. Males are territorial and must be kept alone or with peaceful, non-flashy tank mates."
    },
    {
        id: 4,
        name: "Platy",
        scientificName: "Xiphophorus maculatus",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "60 litres (13 gallons)",
        temperature: "20-28°C (68-82°F)",
        pH: "6.8-8.0",
        hardness: "10-25 dGH",
        diet: "Omnivore. Flakes, micro pellets, blanched vegetables, spirulina, frozen foods.",
        lifespan: "2-3 years",
        size: "5-7 cm",
        tankMates: "Mollies, Swordtails, Corydoras, Tetras, Guppies",
        description: "One of the best beginner fish. Peaceful, active, and available in dozens of colour forms. Breeds readily in community tanks."
    },
    {
        id: 5,
        name: "Corydoras Catfish",
        scientificName: "Corydoras paleatus",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-26°C (72-79°F)",
        pH: "6.0-7.5",
        hardness: "2-12 dGH",
        diet: "Omnivore. Sinking pellets, wafers, frozen bloodworms, blanched vegetables.",
        lifespan: "5-8 years",
        size: "5-7 cm",
        tankMates: "Tetras, Rasboras, Guppies, Platies, other peaceful community fish",
        description: "Armoured bottom-dwellers that constantly forage the substrate. Social fish that must be kept in groups of 6 or more. Great for cleaning up leftover food."
    },
    {
        id: 6,
        name: "Angelfish",
        scientificName: "Pterophyllum scalare",
        careLevel: "Moderate",
        temperament: "Semi-aggressive",
        tankSize: "120 litres (30 gallons)",
        temperature: "24-30°C (75-86°F)",
        pH: "6.0-7.5",
        hardness: "3-8 dGH",
        diet: "Omnivore. Quality flakes/pellets, frozen bloodworms, daphnia, brine shrimp.",
        lifespan: "8-10 years",
        size: "15 cm body, 20+ cm fin tip to fin tip",
        tankMates: "Larger tetras, Corydoras, peaceful Gouramis, avoid small fish like Neons",
        description: "Elegant cichlid with tall, compressed body. Needs tall tanks (45cm+ height). Territorial during breeding. Not a beginner species."
    },
    {
        id: 7,
        name: "Pearl Gourami",
        scientificName: "Trichopodus leerii",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "115 litres (30 gallons)",
        temperature: "23-28°C (74-82°F)",
        pH: "6.0-7.5",
        hardness: "5-15 dGH",
        diet: "Omnivore. Tropical flakes/pellets, frozen brine shrimp, bloodworms, daphnia.",
        lifespan: "5-8 years",
        size: "10 cm (4 inches)",
        tankMates: "Small tetras, Rasboras, Corydoras, peaceful loaches",
        description: "Beautiful pearl-like iridescent patterning. Labyrinth fish that breathes air at the surface. Shy and peaceful. Keep 1 male with 1-2 females."
    },
    {
        id: 8,
        name: "Cherry Barb",
        scientificName: "Puntius titteya",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "50 litres (13 gallons)",
        temperature: "23-27°C (73-81°F)",
        pH: "6.0-7.0",
        hardness: "4-10 dGH",
        diet: "Omnivore. Flakes, micro pellets, frozen or live brine shrimp, daphnia.",
        lifespan: "4-6 years",
        size: "4-5 cm",
        tankMates: "Tetras, Rasboras, Corydoras, Gouramis, shrimp",
        description: "Hardy, peaceful barb with vibrant cherry-red colouring in males. Unlike many barbs, they are not fin-nippers. Keep in groups of 6+."
    },
    {
        id: 9,
        name: "Guppy",
        scientificName: "Poecilia reticulata",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-28°C (72-82°F)",
        pH: "6.8-7.8",
        hardness: "8-12 dGH",
        diet: "Omnivore. Flakes, micro pellets, frozen bloodworms, brine shrimp, blanched vegetables.",
        lifespan: "2-3 years",
        size: "3-6 cm",
        tankMates: "Platies, Mollies, Corydoras, Tetras, Rasboras",
        description: "Incredibly colourful livebearers with elaborate tail fins. Males are smaller and more colourful. Breeds readily. Keep 1 male per 2-3 females."
    },
    {
        id: 10,
        name: "Dwarf Gourami",
        scientificName: "Trichogaster lalius",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "24-28°C (75-82°F)",
        pH: "6.0-7.5",
        hardness: "4-10 dGH",
        diet: "Omnivore. Tropical flakes, pellets, frozen bloodworms, brine shrimp.",
        lifespan: "4-5 years",
        size: "6-9 cm",
        tankMates: "Tetras, Rasboras, Corydoras, Otocinclus",
        description: "Vibrant colours with striking patterns. Labyrinth fish that gulps air at the surface. Can be shy. Keep only one male per tank to avoid aggression."
    },
    {
        id: 11,
        name: "Otocinclus",
        scientificName: "Otocinclus vittatus",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-26°C (72-79°F)",
        pH: "6.0-7.5",
        hardness: "2-10 dGH",
        diet: "Herbivore. Algae, blanched zucchini, algae wafers, biofilm.",
        lifespan: "3-5 years",
        size: "3-5 cm",
        tankMates: "Tetras, Rasboras, Corydoras, shrimp, peaceful community fish",
        description: "Tiny algae-eating catfish. Excellent for planted tanks. Keep in groups of 6+. Sensitive to poor water quality. Best in established tanks with algae growth."
    },
    {
        id: 12,
        name: "Harlequin Rasbora",
        scientificName: "Trigonostigma heteromorpha",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "40 litres (10 gallons)",
        temperature: "22-27°C (72-80°F)",
        pH: "6.0-7.5",
        hardness: "2-10 dGH",
        diet: "Omnivore. Micro pellets, flakes, frozen daphnia, brine shrimp.",
        lifespan: "5-8 years",
        size: "4-5 cm",
        tankMates: "Tetras, Corydoras, Gouramis, other Rasboras, shrimp",
        description: "Copper-orange body with distinctive black triangular patch. Tight schooling fish. Keep in groups of 8-10+ for impressive shoaling behaviour."
    },
    {
        id: 13,
        name: "Bristlenose Pleco",
        scientificName: "Ancistrus cirrhosus",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "23-27°C (73-81°F)",
        pH: "6.0-7.5",
        hardness: "2-12 dGH",
        diet: "Herbivore. Algae wafers, blanched zucchini, cucumber, driftwood (for fibre).",
        lifespan: "5-8 years",
        size: "12-15 cm",
        tankMates: "Most community fish. Tetras, Rasboras, Barbs, Gouramis",
        description: "Hardy algae eater with distinctive bristle-like growths on males. Nocturnal. Needs driftwood in the tank for digestion. One of the best algae cleaners."
    },
    {
        id: 14,
        name: "Kuhli Loach",
        scientificName: "Pangio kuhlii",
        careLevel: "Moderate",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "24-28°C (75-82°F)",
        pH: "5.5-7.0",
        hardness: "1-10 dGH",
        diet: "Omnivore. Sinking pellets, frozen bloodworms, brine shrimp, live foods.",
        lifespan: "10+ years",
        size: "8-12 cm",
        tankMates: "Tetras, Rasboras, Corydoras, Gouramis, other peaceful fish",
        description: "Eel-like striped loach that hides during the day and forages at night. Keep in groups of 5+. Needs sandy substrate and hiding places."
    },
    {
        id: 15,
        name: "Emperor Tetra",
        scientificName: "Nematobrycon palmeri",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "23-27°C (73-81°F)",
        pH: "5.0-7.8",
        hardness: "3-6 dKH",
        diet: "Omnivore. Flakes, pellets, brine shrimp, daphnia, tubifex.",
        lifespan: "6 years",
        size: "5-7.5 cm",
        tankMates: "Corydoras, Rasboras, other peaceful tetras, small peaceful fish",
        description: "Iridescent blue-purple colouration that shifts with the light. Keep in groups of 6+. Males have extended tail fin rays. Stunning in planted tanks."
    },
    {
        id: 16,
        name: "Molly",
        scientificName: "Poecilia sphenops",
        careLevel: "Easy",
        temperament: "Peaceful",
        tankSize: "75 litres (20 gallons)",
        temperature: "22-28°C (72-82°F)",
        pH: "7.0-8.5",
        hardness: "10-25 dGH",
        diet: "Omnivore. Flakes, pellets, spirulina, blanched vegetables, frozen foods.",
        lifespan: "3-5 years",
        size: "6-10 cm",
        tankMates: "Platies, Guppies, Swordtails, Corydoras, larger tetras",
        description: "Hardy livebearers available in many colour variants (black, dalmatian, sailfin). Prefer harder, more alkaline water. Breeds readily."
    }
];

const fishDiseases = [
    {
        id: 1,
        name: "Ich (White Spot Disease)",
        type: "Parasite",
        severity: "high",
        causedBy: "Ichthyophthirius multifiliis (protozoan parasite)",
        symptoms: [
            "White pinhead-sized spots on body and fins",
            "Fish rubbing against objects (flashing)",
            "Rapid breathing",
            "Lethargy and loss of appetite",
            "Clamped fins"
        ],
        treatment: "Raise temperature to 30°C (86°F) for 10-14 days if species tolerates it, OR use ich-specific medication (such as malachite green or copper-based treatments) for 7-14 days. Treat the entire tank. Remove activated carbon from filter during treatment.",
        prevention: "Quarantine new fish for 2-4 weeks. Avoid sudden temperature drops. Maintain stable water parameters and low stress."
    },
    {
        id: 2,
        name: "Fin Rot",
        type: "Bacterial",
        severity: "medium",
        causedBy: "Opportunistic bacteria (Aeromonas, Pseudomonas) triggered by poor water quality",
        symptoms: [
            "Frayed, ragged, or decaying fin edges",
            "Whitish edges on fins",
            "Redness or inflammation at fin base",
            "Progressive fin tissue loss",
            "Decreased activity"
        ],
        treatment: "Start with 50% water change and correct water parameters. If no improvement after a week, add aquarium salt (1 tbsp per 5 gallons). For severe cases, use antibiotics. Most mild cases resolve with clean water alone.",
        prevention: "Maintain 0 ammonia, 0 nitrite, and nitrate under 20 ppm. Perform weekly 20-25% water changes. Avoid overcrowding and aggressive tank mates."
    },
    {
        id: 3,
        name: "Velvet Disease",
        type: "Parasite",
        severity: "high",
        causedBy: "Piscinoodinium (dinoflagellate parasite, partly photosynthetic)",
        symptoms: [
            "Gold or rust-coloured dusty appearance on skin",
            "Clamped fins",
            "Rubbing against objects",
            "Heavy breathing and lethargy",
            "Loss of appetite"
        ],
        treatment: "Total darkness (parasite is photosynthetic) combined with copper-based medication for 14 days. Move to hospital tank if you have invertebrates, as copper is lethal to shrimp and snails.",
        prevention: "Quarantine all new fish. Maintain excellent water quality. Velvet is more aggressive than ich and can kill within days if untreated."
    },
    {
        id: 4,
        name: "Dropsy",
        type: "Bacterial/Organ Failure",
        severity: "high",
        causedBy: "Usually kidney failure from bacterial infection (Aeromonas), chronic stress, or poor water quality over time",
        symptoms: [
            "Swollen/bloated abdomen",
            "Scales protruding outward (pineconing)",
            "Lethargy",
            "Loss of appetite",
            "Difficulty swimming"
        ],
        treatment: "Low success rate once pineconing is visible. Move to hospital tank. Add Epsom salt (magnesium sulfate) at 1 tbsp per 5 gallons to reduce fluid retention. Medicated food with antibiotics may help if caught very early.",
        prevention: "Maintain pristine water quality long-term. Reduce stress factors. Feed a varied, high-quality diet. Regular water changes are the best prevention."
    },
    {
        id: 5,
        name: "Columnaris",
        type: "Bacterial",
        severity: "high",
        causedBy: "Flavobacterium columnare (bacterial, often mistaken for fungus)",
        symptoms: [
            "White or grey cotton-like patches on body, fins, or mouth",
            "Frayed fins with white edges",
            "Mouth erosion (mouth rot)",
            "Rapid progression (can kill in 24-72 hours)"
        ],
        treatment: "Move to hospital tank immediately. Lower temperature to 24°C (75°F) as columnaris thrives in warmth. Use antibiotics (kanamycin or nitrofurazone). Requires aggressive, immediate treatment.",
        prevention: "Avoid overstocking. Maintain clean water. Columnaris spreads fast in warm water above 27°C (80°F). Quarantine new fish."
    },
    {
        id: 6,
        name: "Swim Bladder Disease",
        type: "Dietary/Bacterial",
        severity: "medium",
        causedBy: "Constipation, overfeeding, bacterial infection, or physical injury to swim bladder",
        symptoms: [
            "Fish floating to one side or upside down",
            "Difficulty maintaining position in water",
            "Sinking to bottom and unable to rise",
            "Bloated abdomen"
        ],
        treatment: "Fast the fish for 2-3 days. On day 3-4, offer blanched, deshelled pea (fibre helps move blockage) or frozen daphnia. If bacterial, antibiotics may be needed. Adjust water temperature slightly warmer.",
        prevention: "Avoid overfeeding. Soak dry foods before feeding. Feed varied diet with fibre. Offer small meals rather than one large feeding."
    },
    {
        id: 7,
        name: "Fish Flukes",
        type: "Parasite",
        severity: "medium",
        causedBy: "Gyrodactylus (skin flukes) and Dactylogyrus (gill flukes) parasites",
        symptoms: [
            "Loss of colour",
            "Difficulty breathing and flared gills",
            "Excess mucus production",
            "Listlessness and clamped fins",
            "Small blood spots on fins and body"
        ],
        treatment: "Praziquantel is the safest and most effective treatment. Treat the whole tank. Multiple treatments may be needed for egg-laying species. Monitor for secondary bacterial infections on damaged tissue.",
        prevention: "Quarantine all new fish for 2-4 weeks. Maintain good water quality. Avoid overcrowding and stress."
    },
    {
        id: 8,
        name: "Fungal Infection",
        type: "Fungal",
        severity: "medium",
        causedBy: "Saprolegnia and related water moulds. Typically secondary to wounds or weakened immune system",
        symptoms: [
            "White or grey cotton-like growths on skin, fins, or eyes",
            "Growths typically form around wounds or damaged areas",
            "Lethargy",
            "Loss of appetite"
        ],
        treatment: "Improve water quality immediately. Use antifungal medication. Remove affected fish to hospital tank if growths are severe. Treat underlying cause (wound, stress, poor water quality).",
        prevention: "Avoid injuries from sharp decorations. Keep water temperature stable and within species range. Fish in cold water below their optimal range are especially susceptible."
    }
];
Render disease cards on the health page

The health page needs two things: HTML containers for the cards to live in, and a JavaScript function that reads the data array and creates the card elements. You'll follow the same data-driven rendering pattern you used for the species catalog.

  • In health.html, find the empty <main class="container"> element.
  • Add the following two sections inside it (between the opening and closing <main> tags):
        <section class="health-intro">
            <h2>Fish Health Guide</h2>
            <p>Identify common diseases, parasites, and health issues in aquarium fish. Early detection and treatment are key to keeping your fish healthy.</p>
        </section>

        <section class="health-grid" id="health-grid">
        </section>

What does this HTML do?

The health-intro section provides a heading and description for the page. The health-grid section is an empty container that JavaScript will fill with disease cards, just like species-grid on the index page.

  • Save health.html.
  • You won't see cards yet because the rendering function hasn't been written. The next chunk handles that.

Now write the rendering logic that loops through the disease data and creates a card for each entry.

  • Switch to health.js (currently empty) and paste the following code:
const healthGrid = document.getElementById('health-grid');

function renderHealthCards() {
    healthGrid.innerHTML = '';

    fishDiseases.forEach(function(disease) {
        const card = document.createElement('div');
        card.className = 'health-card severity-' + disease.severity;

        const symptomsList = disease.symptoms.map(function(symptom) {
            return '<li>' + symptom + '</li>';
        }).join('');

        card.innerHTML =
            '<h3>' + disease.name + '</h3>' +
            '<p class="disease-type">' + disease.type + '</p>' +

            '<div class="detail-section">' +
                '<h4>Caused By</h4>' +
                '<p>' + disease.causedBy + '</p>' +
            '</div>' +

            '<div class="detail-section">' +
                '<h4>Symptoms</h4>' +
                '<ul>' + symptomsList + '</ul>' +
            '</div>' +

            '<div class="detail-section">' +
                '<h4>Treatment</h4>' +
                '<p>' + disease.treatment + '</p>' +
            '</div>' +

            '<div class="detail-section">' +
                '<h4>Prevention</h4>' +
                '<p>' + disease.prevention + '</p>' +
            '</div>';

        healthGrid.appendChild(card);
    });
}

renderHealthCards();

What does this code do?

The renderHealthCards() function follows the same pattern as renderSpeciesCards(). It loops through the fishDiseases array, creates a div for each disease, and appends it to the grid container.

The class severity- plus the disease's severity value (e.g. severity-high) is how CSS will colour-code the cards. The symptoms array is mapped into <li> elements and joined into a single string for the unordered list.

  • Save health.js.
  • Open health.html in your browser (or refresh if it's already open). You should see 8 disease/parasite cards rendered on the page with headings, symptoms lists, and treatment text.

Don't see any cards on the health page?

Open DevTools (right-click the page, then **Inspect**, then the **Console** tab) and look for errors.

  • If you see fishDiseases is not defined, check that health.html loads data.js before health.js in the script tags at the bottom of the file.
  • If you see Cannot read properties of null, confirm you added id="health-grid" to the section element in health.html.
  • If the page is blank with no errors, make sure you saved both data.js and health.js.

Why aren't my health cards rendering on the page?

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before styling.

ⓧ I'd like to double check the full code

Here is the complete health.html at this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Health Guide - Fish Keeper's Reference</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header class="site-header">
        <div class="container">
            <h1 class="site-title">🐠 Fish Keeper's Reference</h1>
            <nav class="main-nav">
                <a href="index.html" class="nav-link">Species Catalog</a>
                <a href="health.html" class="nav-link active">Health Guide</a>
            </nav>
        </div>
    </header>

    <main class="container">
        <section class="health-intro">
            <h2>Fish Health Guide</h2>
            <p>Identify common diseases, parasites, and health issues in aquarium fish. Early detection and treatment are key to keeping your fish healthy.</p>
        </section>

        <section class="health-grid" id="health-grid">
        </section>
    </main>

    <footer class="site-footer">
        <div class="container">
            <p>&copy; 2026 Fish Keeper's Reference. Built for the fishkeeping community.</p>
        </div>
    </footer>

    <script src="data.js"></script>
    <script src="health.js"></script>
</body>
</html>

And the complete health.js:

const healthGrid = document.getElementById('health-grid');

function renderHealthCards() {
    healthGrid.innerHTML = '';

    fishDiseases.forEach(function(disease) {
        const card = document.createElement('div');
        card.className = 'health-card severity-' + disease.severity;

        const symptomsList = disease.symptoms.map(function(symptom) {
            return '<li>' + symptom + '</li>';
        }).join('');

        card.innerHTML =
            '<h3>' + disease.name + '</h3>' +
            '<p class="disease-type">' + disease.type + '</p>' +

            '<div class="detail-section">' +
                '<h4>Caused By</h4>' +
                '<p>' + disease.causedBy + '</p>' +
            '</div>' +

            '<div class="detail-section">' +
                '<h4>Symptoms</h4>' +
                '<ul>' + symptomsList + '</ul>' +
            '</div>' +

            '<div class="detail-section">' +
                '<h4>Treatment</h4>' +
                '<p>' + disease.treatment + '</p>' +
            '</div>' +

            '<div class="detail-section">' +
                '<h4>Prevention</h4>' +
                '<p>' + disease.prevention + '</p>' +
            '</div>';

        healthGrid.appendChild(card);
    });
}

renderHealthCards();
Style the health section with severity indicators

The cards are rendering, but they need visual structure. You'll add a coloured left border that changes based on severity: red for high, amber for medium, and green for low.

  • Switch back to styles.css.
  • Add the following health section styles at the bottom of the file, just above the @media query:
.health-intro {
    margin-bottom: 24px;
}

.health-intro h2 {
    color: var(--color-primary-dark);
    margin-bottom: 8px;
}

.health-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 20px;
}

.health-card {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    padding: 24px;
    box-shadow: var(--shadow);
    border-left: 4px solid var(--color-warning);
}

.health-card.severity-high {
    border-left-color: var(--color-danger);
}

.health-card.severity-medium {
    border-left-color: var(--color-warning);
}

.health-card.severity-low {
    border-left-color: var(--color-success);
}

.health-card h3 {
    color: var(--color-primary-dark);
    margin-bottom: 4px;
}

.health-card .disease-type {
    font-size: 0.8rem;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--color-text-light);
    margin-bottom: 12px;
}

.health-card .detail-section {
    margin-bottom: 12px;
}

.health-card .detail-section h4 {
    font-size: 0.85rem;
    color: var(--color-primary);
    margin-bottom: 4px;
}

.health-card .detail-section p,
.health-card .detail-section ul {
    font-size: 0.9rem;
    color: var(--color-text-light);
}

.health-card .detail-section ul {
    padding-left: 16px;
}

.health-card .detail-section li {
    margin-bottom: 4px;
}

What does this CSS do?

The .health-grid uses CSS Grid with auto-fill to create a responsive layout. Cards fill available columns at a minimum width of 320px.

The border-left on .health-card defaults to amber (warning colour). The .severity-high, .severity-medium, and .severity-low modifiers override that border colour. This gives users an instant visual signal about how dangerous a disease is.

You also need to make the health grid collapse to a single column on small screens.

  • Find the existing @media (max-width: 768px) block at the bottom of styles.css.
  • Add this line inside the media query, below the .species-grid rule:
    .health-grid {
        grid-template-columns: 1fr;
    }
  • Save styles.css and refresh health.html in your browser.
  • You should see 8 neatly styled cards in a grid layout. Cards for Ich, Velvet, Dropsy, Columnaris have a red left border (high severity). Cards for Fin Rot, Swim Bladder Disease, Fish Flukes, and Fungal Infection have an amber left border (medium severity).

Cards showing but no coloured border?

  • Check that the new CSS rules are placed above the @media block, not inside it. Only the single-column override belongs inside the media query.
  • Confirm the class names match exactly. In health.js the card gets health-card severity-high as its class. In CSS you target .health-card.severity-high (no space between the two class selectors).

Why aren't my severity colours showing on the health cards?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Here are the health-specific styles you should have in styles.css (placed before the media query), plus the media query addition:

.health-intro {
    margin-bottom: 24px;
}

.health-intro h2 {
    color: var(--color-primary-dark);
    margin-bottom: 8px;
}

.health-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 20px;
}

.health-card {
    background-color: var(--color-surface);
    border-radius: var(--radius);
    padding: 24px;
    box-shadow: var(--shadow);
    border-left: 4px solid var(--color-warning);
}

.health-card.severity-high {
    border-left-color: var(--color-danger);
}

.health-card.severity-medium {
    border-left-color: var(--color-warning);
}

.health-card.severity-low {
    border-left-color: var(--color-success);
}

.health-card h3 {
    color: var(--color-primary-dark);
    margin-bottom: 4px;
}

.health-card .disease-type {
    font-size: 0.8rem;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--color-text-light);
    margin-bottom: 12px;
}

.health-card .detail-section {
    margin-bottom: 12px;
}

.health-card .detail-section h4 {
    font-size: 0.85rem;
    color: var(--color-primary);
    margin-bottom: 4px;
}

.health-card .detail-section p,
.health-card .detail-section ul {
    font-size: 0.9rem;
    color: var(--color-text-light);
}

.health-card .detail-section ul {
    padding-left: 16px;
}

.health-card .detail-section li {
    margin-bottom: 4px;
}

/* Inside your existing @media (max-width: 768px) block: */
/* .health-grid { grid-template-columns: 1fr; } */

Your fish health guide is live and colour-coded. Next up, you'll take this entire project and deploy it to the internet so anyone can use it.

Deploy to Netlify

Your fish keeper's reference website is complete and working locally. Both the species catalog and health guide are ready for the world to see.

In this step, you'll deploy your project to the internet using Netlify Drop. This is a drag-and-drop hosting service that takes your project folder and makes it available at a public URL in seconds.

In this step, get ready to:
  • Deploy your project folder to Netlify using drag-and-drop.
  • Verify both pages work at the live URL.
Deploy your project with Netlify Drop

Netlify Drop lets you publish a static site by dragging your project folder into the browser. No terminal commands, no Git setup, no build tools required.

  • Open your browser and navigate to https://app.netlify.com/drop.
  • Sign in with your Netlify account if prompted.
  • You'll see a Drop zone area on the page where you can drag files.
  • Open Finder (macOS) or File Explorer (Windows) and navigate to your Desktop.
  • Find the fish-keepers-reference folder.
  • Drag the entire fish-keepers-reference folder onto the Drop zone in Netlify.
  • Wait a few seconds for the upload and deploy to complete.

What just happened?

Netlify uploaded all your project files (HTML, CSS, JavaScript) and published them to their global network. Your site is now accessible to anyone with the URL.

Because your project has an index.html at the root level, Netlify knows it's the homepage. The health.html file is served alongside it, so navigation between pages works automatically.

After the deploy completes, Netlify shows your project dashboard with a Preview URL ending in netlify.app.

  • Find your site's live URL at the top of the project dashboard.
  • Click the URL to open your deployed site in a new tab.

Deploy not working?

Make sure you dragged the fish-keepers-reference folder itself, not individual files from inside it. Netlify needs the folder containing index.html at its root.

If the deploy seems stuck, check that your folder is under 50MB and no individual file is over 10MB. Your project is well under these limits.

Why isn't my Netlify Drop deploy working?

Verify the live site

Your site is live, but you should confirm that both pages and all the interactive features work at the public URL.

  • On the live site, check that the species cards load in the grid layout.
  • Type a fish name into the search bar to confirm filtering works.
  • Click a species card to verify the detail modal opens with full care information.
  • Click the Health Guide link in the navigation header.
  • Confirm the health.html page loads with disease and parasite cards, complete with severity indicators.
  • Click Species Catalog in the header to navigate back to the main page.

What is a netlify.app URL?

Netlify generates a random subdomain for your site (something like random-name-123456.netlify.app). This is a free, publicly accessible URL you can share with anyone.

If you want to update your site later, just drag the updated folder to the deploy dropzone on your project's Deploys page in Netlify. The same URL gets the new version instantly.

Health Guide page showing a 404?

Check that health.html is at the same folder level as index.html inside your fish-keepers-reference folder. If it's nested inside a subfolder, the navigation link won't find it.

Also confirm the link in your HTML says href="health.html" (not a full file path like /Users/yourname/...).

Why is my health.html page returning a 404 on Netlify?

Secret mission

Add a "Request a Fish" Form

Your species catalog is great, but what if a visitor wants a fish that's not listed? In this bonus mission, you'll add a "Request a Species" form powered by Netlify Forms. No backend server required. Visitors submit their request, and it appears in your Netlify dashboard automatically.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses Netlify's free tier for hosting, so there are no ongoing costs.

Resources you used:

  • Local fish-keepers-reference project folder on your Desktop.
  • Netlify site (deployed via Netlify Drop, live at your .netlify.app URL).
  • Netlify Forms submissions stored in your Netlify dashboard.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your Fish Keeper's Reference live for others to use.

  • Your site remains live at its .netlify.app URL at no cost.
  • Your local project files stay on your Desktop for future edits.
  • Form submissions continue to collect in your Netlify dashboard.

Pause - I'll come back to this later

Take your site offline while keeping your Netlify project configuration intact. You can re-enable it any time without redeploying.

  • Log in to your Netlify dashboard at app.netlify.com.
  • Select your Fish Keeper's Reference project.
  • Navigate to Project configuration, then General.
  • Scroll down to the Danger zone section.
  • Select Disable project.
  • Your site goes offline but all configuration is preserved. To bring it back, return to the same location and select Enable project.

Delete - I don't want to use this again

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

  • Log in to your Netlify dashboard at app.netlify.com.
  • Select your Fish Keeper's Reference project.
  • Navigate to Project configuration, then General.
  • Scroll down to the Danger zone section and select the option to delete the project.
  • Confirm when prompted. This permanently removes your site, deploy history, and form submissions.

Delete your local project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/fish-keepers-reference

Windows

Remove-Item -Recurse -Force ~\Desktop\fish-keepers-reference

Your Netlify account remains active on the free tier with no charges. You can use it for future projects.

Nice Work!

Nice Work!

You just built and deployed a fully functional Fish Keeper's Reference website. It's live on the internet, searchable, filterable, and genuinely useful for aquarium hobbyists.

You've learned how to:

  • Built a multi-page static website with a JavaScript data layer that dynamically renders species cards and health guides from arrays of objects.
  • Implemented real-time filtering and search across a dataset, plus modal interactions with event delegation and DOM manipulation.
  • Deployed a live site to Netlify using drag-and-drop, with responsive layouts built using CSS Grid and Flexbox.
  • Integrated Netlify Forms for serverless form handling, capturing user submissions without writing a single line of backend code.

Ready to quiz yourself?