Black Clover Interactive Fan Site

Fetch live anime data from a REST API and build interactive character cards.

Introduction

30 Second Summary

Every anime fan has a favorite character they could talk about for hours. Imagine turning that passion into an interactive website that pulls real data from the internet and responds to every hover and click.

In this project, you will build a Black Clover fan website that fetches live anime data from a free API and displays interactive character cards with flip animations and detail modals.

What You'll Build

A dark-themed Black Clover character guide that pulls real anime data and character portraits from the Jikan REST API and presents them with interactive card animations and a detail modal.

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

  • A hero section that displays live anime data (title, synopsis, score, and cover image) fetched from a REST API.
  • An interactive character card grid with 3D flip animations on hover that reveal each character's strengths and weaknesses.
  • A click-triggered modal that shows detailed grimoire, weapons, and magic type information for each character.
  • Secret Mission: Add a Magic Knight Squad filter so users can filter characters by their squad.

Are there any prerequisites?

You only need a basic understanding of HTML tags and attributes. No prior CSS or JavaScript experience is required. This project teaches you both from scratch.

Set Up Your Project

Every interactive website starts with a solid file structure. Before you can fetch anime data or animate character cards, you need a home for your HTML, CSS, and JavaScript.

In this step, you will create a project folder, set up the base HTML skeleton, and confirm everything loads cleanly in the browser.

In this step, get ready to:
  • Create the project folder and three core files.
  • Build the base HTML structure with hero, characters, and modal sections.
  • Verify the page loads in Chrome with no console errors.
Create the project folder and files

You need a dedicated folder to keep your HTML, CSS, and JavaScript files together. You will create this folder on your Desktop so it is easy to find.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir black-clover-site
cd black-clover-site
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the black-clover-site folder on your Desktop, then click Open.

Why open the folder this way?

Opening a folder in VS Code sets it as your workspace. The Explorer sidebar shows your project files, and the integrated terminal starts inside that folder automatically.

VS Code reloads with the folder name visible in the Explorer sidebar on the left. Now you can create the three project files.

  • Open the integrated terminal again by pressing Ctrl+` (macOS and Windows).
  • Create your three project files by running this command:

macOS

touch index.html style.css script.js

Windows

New-Item index.html, style.css, script.js
  • Confirm the files exist by running:
ls

You should see index.html, style.css, and script.js in the output.

Don't see the files?

Make sure you ran the file creation command after VS Code reopened with the black-clover-site folder. If the terminal shows a different directory, run cd ~/Desktop/black-clover-site and try the create command again.

Help me figure out what went wrong

Set up the base HTML structure

Your index.html file is the skeleton of the entire website. It defines three key sections: a hero banner for the anime info, a grid container for character cards, and a hidden modal for detailed character popups.

  • In the VS Code Explorer sidebar, click index.html to open it in the editor.
  • Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Black Clover - Character Guide</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="hero">
        <div id="hero-text">
            <h1 id="hero-title">Loading...</h1>
            <p id="hero-synopsis"></p>
            <span id="hero-score"></span>
        </div>
        <div id="hero-image">
            <img id="hero-cover" src="" alt="Black Clover Cover">
        </div>
    </div>

    <h2 class="section-title">Characters</h2>
    <div id="characters"></div>

    <div id="modal" class="modal hidden">
        <div class="modal-overlay"></div>
        <div class="modal-content">
            <button class="modal-close">&times;</button>
            <img id="modal-image" src="" alt="">
            <h2 id="modal-name"></h2>
            <div class="modal-details">
                <div class="modal-section">
                    <h3>Grimoire</h3>
                    <p id="modal-grimoire"></p>
                </div>
                <div class="modal-section">
                    <h3>Magic Type</h3>
                    <p id="modal-magic"></p>
                </div>
                <div class="modal-section">
                    <h3>Weapons</h3>
                    <ul id="modal-weapons"></ul>
                </div>
            </div>
        </div>
    </div>

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

What does this HTML do?

  • The <link rel="stylesheet" href="style.css"> tag connects your CSS file so styles apply automatically.
  • The <div id="hero"> section holds the anime title, synopsis, score badge, and cover image. JavaScript will fill these in later.
  • The <div id="characters"> is an empty container. JavaScript will inject character cards into it after fetching data from the API.
  • The <div id="modal" class="modal hidden"> is a popup overlay that starts invisible. Clicking a character card will reveal it.
  • The <script src="script.js"> tag loads your JavaScript file at the bottom so the HTML elements exist before your code tries to access them.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Seeing red underlines or errors in the editor?

Make sure you pasted the full code starting from <!DOCTYPE html> and ending with </html>. If VS Code shows a warning about missing closing tags, check that every opening tag has a matching closing tag.

Help me troubleshoot my HTML

✔️ Awesome, I've got everything!

Great. Make sure you have saved index.html before moving on.

ⓧ I'd like to double check the full code

Here is the complete 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>Black Clover - Character Guide</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="hero">
        <div id="hero-text">
            <h1 id="hero-title">Loading...</h1>
            <p id="hero-synopsis"></p>
            <span id="hero-score"></span>
        </div>
        <div id="hero-image">
            <img id="hero-cover" src="" alt="Black Clover Cover">
        </div>
    </div>

    <h2 class="section-title">Characters</h2>
    <div id="characters"></div>

    <div id="modal" class="modal hidden">
        <div class="modal-overlay"></div>
        <div class="modal-content">
            <button class="modal-close">&times;</button>
            <img id="modal-image" src="" alt="">
            <h2 id="modal-name"></h2>
            <div class="modal-details">
                <div class="modal-section">
                    <h3>Grimoire</h3>
                    <p id="modal-grimoire"></p>
                </div>
                <div class="modal-section">
                    <h3>Magic Type</h3>
                    <p id="modal-magic"></p>
                </div>
                <div class="modal-section">
                    <h3>Weapons</h3>
                    <ul id="modal-weapons"></ul>
                </div>
            </div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>
Open the page in Chrome

You need to open your index.html file in a browser to see it rendered. Chrome's DevTools console also tells you if any linked files are missing.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Google Chrome and press Enter to open it.
  • Press Cmd+O (macOS) or Ctrl+O (Windows) to open the file picker.
  • Navigate to your Desktop, open the black-clover-site folder, select index.html, and click Open.
  • Press F12 to open DevTools.
  • Click the Console tab at the top of the DevTools panel.

You should see a mostly blank white page with the text "Loading..." and "Characters" visible. The Console tab should show no red error messages.

Why check the console?

If the browser cannot find style.css or script.js, it logs a 404 error in the console. Seeing a clean console confirms all three files are linked correctly.

Seeing 404 errors in the console?

A 404 error means the browser cannot find a linked file. Check that style.css and script.js exist in the same folder as index.html. The filenames must match exactly (lowercase, no spaces).

Help me fix my 404 errors

Your project skeleton is ready. Next up, you will fetch live anime data from the Jikan API and bring the hero section to life.

Build the Hero Section

Your page structure is ready, but it's empty. The hero section is the first thing visitors see, so it needs to grab attention with real anime data.

In this step, you'll fetch live data from the Jikan API (a free anime database) and display the Black Clover title, synopsis, score, and cover art. Then you'll style everything with a dark gradient theme.

In this step, get ready to:
  • Fetch Black Clover anime data from the Jikan REST API.
  • Style the hero section with a dark gradient background and flexbox layout.
  • Verify the hero banner loads correctly in Chrome.
Fetch anime data from the Jikan API

The Jikan API provides free access to anime data from MyAnimeList. No API key is required.

You'll write an async function that fetches Black Clover's title, synopsis, score, and cover image, then inserts them into the HTML elements you set up in Step 1.

  • Switch back to VS Code and open script.js.
  • Add the loadHero() function and its call by pasting this code:
async function loadHero() {
    try {
        // Fetch Black Clover anime data from Jikan API
        const response = await fetch("https://api.jikan.moe/v4/anime/34572");
        const json = await response.json();
        const anime = json.data;

        // Insert the anime data into our hero section elements
        document.getElementById("hero-title").textContent = anime.title;
        document.getElementById("hero-synopsis").textContent = anime.synopsis;
        document.getElementById("hero-score").textContent = "Score: " + anime.score + " / 10";
        document.getElementById("hero-cover").src = anime.images.jpg.large_image_url;
        document.getElementById("hero-cover").alt = anime.title;
    } catch (error) {
        // Show a fallback title if the API call fails
        document.getElementById("hero-title").textContent = "Black Clover";
        console.error("Failed to load anime data:", error);
    }
}

// Call the function to kick off the fetch
loadHero();

What does this code do?

This function reaches out to the Jikan API and pulls back data about Black Clover (MAL ID 34572).

  • await fetch(...) sends a network request to the API and waits for the response.
  • response.json() converts the raw response into a JavaScript object you can work with.
  • document.getElementById(...) finds each HTML element by its id attribute and sets its content to the matching API data.
  • The try/catch block handles errors gracefully. If the API is down, the page shows a fallback title instead of breaking.
  • Save script.js.
  • Return to Chrome and refresh the page.
  • You should see "Black Clover" as the page heading, a paragraph of synopsis text, "Score: 8.14 / 10", and the cover image. The styling will look plain for now.

Still seeing "Loading..." text?

  • Open DevTools with F12 and check the Console tab for red error messages. A network error means the API might be temporarily rate-limited. Wait 30 seconds and refresh.
  • Make sure your script.js file is saved. Unsaved changes won't run in the browser.
  • Confirm the <script src="script.js"></script> tag is at the bottom of your index.html (just before </body>).

Help me debug why my fetch call to the Jikan API is not returning data.

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js before moving on to styling.

ⓧ I'd like to double check the full code

async function loadHero() {
    try {
        const response = await fetch("https://api.jikan.moe/v4/anime/34572");
        const json = await response.json();
        const anime = json.data;

        document.getElementById("hero-title").textContent = anime.title;
        document.getElementById("hero-synopsis").textContent = anime.synopsis;
        document.getElementById("hero-score").textContent = "Score: " + anime.score + " / 10";
        document.getElementById("hero-cover").src = anime.images.jpg.large_image_url;
        document.getElementById("hero-cover").alt = anime.title;
    } catch (error) {
        document.getElementById("hero-title").textContent = "Black Clover";
        console.error("Failed to load anime data:", error);
    }
}

loadHero();
Style the hero section with a dark theme

Right now the page uses browser defaults. You'll add a CSS reset to clear built-in margins, set a dark background, and then style the hero layout with flexbox.

  • Open style.css in VS Code.
  • Add the base reset and body styles by pasting this code:
/* Reset browser defaults */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Dark theme base */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #0a0a0f;
    color: #ffffff;
    min-height: 100vh;
}

What does this code do?

  • The * selector targets every element on the page and removes built-in margins and padding that browsers add by default.
  • box-sizing: border-box makes sizing calculations predictable. Padding and borders are included inside the element's width instead of adding to it.
  • The body styles set the near-black background (#0a0a0f) and white text to create the dark anime aesthetic.
  • Save style.css and refresh Chrome.
  • The page background should now be dark with white text. The layout is still unstyled.
  • Now add the hero section styles below your body styles in style.css:
/* Hero section layout */
#hero {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 40px;
    background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 50%, #16213e 100%);
    border-bottom: 2px solid #2d2d44;
    gap: 40px;
    flex-wrap: wrap;
}

#hero-text {
    flex: 1;
    min-width: 300px;
}

/* Hero text styles */
#hero-title {
    font-size: 2.5rem;
    margin-bottom: 16px;
    color: #e4e4e4;
}

#hero-synopsis {
    font-size: 1rem;
    line-height: 1.6;
    color: #b0b0b0;
    margin-bottom: 16px;
    max-height: 150px;
    overflow: hidden;
}

/* Score badge */
#hero-score {
    display: inline-block;
    background: #e63946;
    color: #ffffff;
    padding: 8px 16px;
    border-radius: 8px;
    font-weight: bold;
    font-size: 1.1rem;
}

/* Cover image */
#hero-image img {
    width: 200px;
    border-radius: 12px;
    box-shadow: 0 0 20px rgba(230, 57, 70, 0.3);
}

/* Characters section heading */
.section-title {
    text-align: center;
    font-size: 2rem;
    margin: 40px 0 20px;
    color: #e4e4e4;
}

What's happening here?

  • display: flex on #hero arranges the text and image side by side. The flex-wrap: wrap property makes it stack on smaller screens.
  • The linear-gradient creates a subtle color shift from near-black to dark blue, adding depth without being distracting.
  • #hero-score uses a red background (#e63946) to make the score pop as a badge. The rounded corners and padding give it a button-like shape.
  • box-shadow on the cover image creates a subtle red glow that matches the score badge color.
  • Save style.css and refresh Chrome.
  • You should see the hero section with a dark gradient background, the title and synopsis on the left, and the cover image on the right with a red glow.

Hero section not looking right?

  • If the image and text are stacked vertically instead of side by side, check that display: flex is on the #hero selector (not #hero-text).
  • If the gradient background is missing, make sure you typed background (not background-color) for the gradient.
  • If nothing changed at all, confirm you saved style.css and that the <link rel="stylesheet" href="style.css"> tag exists in your index.html head.

Help me fix my hero section CSS layout.

✔️ Awesome, I've got everything!

Great. Make sure you've saved style.css before moving on.

ⓧ I'd like to double check the full code

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

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #0a0a0f;
    color: #ffffff;
    min-height: 100vh;
}

#hero {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 40px;
    background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 50%, #16213e 100%);
    border-bottom: 2px solid #2d2d44;
    gap: 40px;
    flex-wrap: wrap;
}

#hero-text {
    flex: 1;
    min-width: 300px;
}

#hero-title {
    font-size: 2.5rem;
    margin-bottom: 16px;
    color: #e4e4e4;
}

#hero-synopsis {
    font-size: 1rem;
    line-height: 1.6;
    color: #b0b0b0;
    margin-bottom: 16px;
    max-height: 150px;
    overflow: hidden;
}

#hero-score {
    display: inline-block;
    background: #e63946;
    color: #ffffff;
    padding: 8px 16px;
    border-radius: 8px;
    font-weight: bold;
    font-size: 1.1rem;
}

#hero-image img {
    width: 200px;
    border-radius: 12px;
    box-shadow: 0 0 20px rgba(230, 57, 70, 0.3);
}

.section-title {
    text-align: center;
    font-size: 2rem;
    margin: 40px 0 20px;
    color: #e4e4e4;
}
Verify the hero banner in Chrome
  • Refresh your page in Chrome one more time.
  • Press F12 to open DevTools and click the Console tab.
  • Confirm there are no red error messages.

Your hero section should display the Black Clover title, a synopsis paragraph, a red score badge showing "Score: 8.14 / 10", and the cover image with a subtle glow. All of this data came live from the Jikan API.

Why use a live API instead of hardcoding?

Fetching from an API means your page always shows the latest data. If the community score changes on MyAnimeList, your site reflects it automatically. This is the same pattern real web apps use to stay up to date.

Your hero section is pulling live data and looking sharp. Next up, you'll build an interactive character card grid that fetches character portraits from the same API.

Create the Character Card Grid

Your hero section is pulling live data from the Jikan API. Now it's time to populate the page with the characters that make Black Clover exciting.

In this step, you'll build a custom data object that pairs each character's unique ID with their lore (strengths, weaknesses, grimoire). Then you'll fetch character portraits from the API and display them in a responsive grid.

In this step, get ready to:
  • Define a character lore object with custom data for 6 characters.
  • Fetch character images from the Jikan API and render cards.
  • Style the cards in a responsive CSS Grid layout.
Define the character lore data

The Jikan API provides character images and names, but it doesn't include custom lore like grimoire types or combat strengths. You'll create your own data object to pair with the API data.

  • In script.js, add the following characterLore object at the very top of the file, above the existing loadHero function:
const characterLore = {
    124731: {
        name: "Asta",
        strengths: ["Incredible physical strength", "Anti-Magic nullifies all spells", "Never gives up attitude"],
        weaknesses: ["Cannot use magic at all", "Reckless in battle", "Limited range without swords"],
        grimoire: "Five-Leaf Clover Grimoire (Black Clover)",
        magic: "Anti-Magic",
        weapons: ["Demon-Slayer Sword", "Demon-Dweller Sword", "Demon-Destroyer Sword"],
        squad: "Black Bulls"
    },
    124732: {
        name: "Yuno",
        strengths: ["Immense mana reserves", "Spirit Magic with Sylph", "Calm and strategic"],
        weaknesses: ["Can be overconfident", "Emotionally reserved", "Relies heavily on wind spirit"],
        grimoire: "Four-Leaf Clover Grimoire",
        magic: "Wind Magic / Spirit Magic",
        weapons: ["Spirit of Zephyr (Sylph)", "Spirit Storm", "Tornado Fang"],
        squad: "Golden Dawn"
    },
    127141: {
        name: "Noelle Silva",
        strengths: ["Royal-level mana", "Valkyrie Dress combat form", "Sea Dragon's Roar"],
        weaknesses: ["Struggled with mana control early on", "Emotional when family is involved", "Self-doubt from past trauma"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Water Magic",
        weapons: ["Sea Dragon's Roar", "Valkyrie Dress", "Sea Dragon's Cradle"],
        squad: "Black Bulls"
    },
    127764: {
        name: "Yami Sukehiro",
        strengths: ["Dark Magic cuts through dimensions", "Ki sensing ability", "Extreme physical power"],
        weaknesses: ["Spells are slow to activate", "Chain smoker with poor habits", "Reckless leadership style"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Dark Magic",
        weapons: ["Katana", "Dimension Slash", "Dark Cloaked Black Blade"],
        squad: "Black Bulls"
    },
    127886: {
        name: "Luck Voltia",
        strengths: ["Lightning speed in combat", "Battle-hungry instincts", "Lightning Magic versatility"],
        weaknesses: ["Obsessed with fighting", "Ignores teamwork sometimes", "Loses control in excitement"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Lightning Magic",
        weapons: ["Thunderclap Crumbling Orb", "Thunder Fiend", "Lightning Boots"],
        squad: "Black Bulls"
    },
    127049: {
        name: "Magna Swing",
        strengths: ["Creative spell combinations", "Soul Chain Deathmatch technique", "Strong determination"],
        weaknesses: ["Low mana reserves", "Hot-headed personality", "Outclassed by nobles in raw power"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Fire Magic",
        weapons: ["Exploding Fireball", "Vanishing Magic Ball", "Soul Chain Deathmatch"],
        squad: "Black Bulls"
    }
};

What does this code do?

Each key in characterLore is a character's MAL ID (the unique identifier that MyAnimeList uses). The value is an object containing your custom lore data.

Later, when you fetch character images from the API, you'll use these IDs to match each portrait with its lore. Characters not in this object will be skipped.

  • Save script.js.

You won't see a change yet in the browser. The next chunk uses this data to build actual cards.

Fetch character data and build cards

The Jikan API returns character names in "Last, First" format (e.g., "Silva, Noelle"). You need a helper function to flip those names before displaying them.

  • In script.js, add the formatName helper function directly below the characterLore object (above the existing loadHero function):
function formatName(apiName) {
    // Jikan returns names as "Last, First" - flip them to "First Last"
    if (apiName.includes(", ")) {
        const parts = apiName.split(", ");
        return parts[1] + " " + parts[0];
    }
    return apiName;
}

What does this code do?

The API returns names like "Sukehiro, Yami" and "Silva, Noelle". This function checks for a comma, splits the string, and reassembles it as "Yami Sukehiro" and "Noelle Silva". Names without commas (like "Asta") pass through unchanged.

  • Now add the loadCharacters function below the existing loadHero function:
async function loadCharacters() {
    try {
        // Fetch character list for Black Clover from Jikan API
        const response = await fetch("https://api.jikan.moe/v4/anime/34572/characters");
        const json = await response.json();
        const characters = json.data;

        const container = document.getElementById("characters");

        characters.forEach(function(entry) {
            const malId = entry.character.mal_id;

            // Skip characters not in our lore object
            if (!characterLore[malId]) return;

            const lore = characterLore[malId];
            const imageUrl = entry.character.images.jpg.image_url;
            const displayName = formatName(entry.character.name);

            // Create a card with front face (image + name) and back face (strengths/weaknesses)
            const card = document.createElement("div");
            card.className = "card";
            card.setAttribute("data-mal-id", malId);

            card.innerHTML =
                '<div class="card-inner">' +
                    '<div class="card-front">' +
                        '<img src="' + imageUrl + '" alt="' + displayName + '">' +
                        '<div class="card-name">' + displayName + '</div>' +
                    '</div>' +
                    '<div class="card-back">' +
                        '<h3>' + displayName + '</h3>' +
                        '<div class="strengths">' +
                            '<h4>Strengths</h4>' +
                            '<ul>' + lore.strengths.map(function(s) { return '<li>' + s + '</li>'; }).join('') + '</ul>' +
                        '</div>' +
                        '<div class="weaknesses">' +
                            '<h4>Weaknesses</h4>' +
                            '<ul>' + lore.weaknesses.map(function(w) { return '<li>' + w + '</li>'; }).join('') + '</ul>' +
                        '</div>' +
                    '</div>' +
                '</div>';

            container.appendChild(card);
        });
    } catch (error) {
        console.error("Failed to load characters:", error);
    }
}

What does this code do?

This function fetches all characters associated with Black Clover from the Jikan API. For each character, it checks if their MAL ID exists in your characterLore object. If it does, the function creates a card element with two faces.

The front face shows the character's portrait image and name. The back face holds their strengths and weaknesses in list form. You'll add the flip animation in the next step.

  • Find the loadHero(); call at the bottom of script.js. Add the loadCharacters(); call directly below it:
loadHero();
loadCharacters();
  • Save script.js.

You won't see styled cards yet because the CSS isn't in place. If you refresh Chrome and open DevTools (press F12, then the Elements tab), you should see <div class="card"> elements inside the #characters container.

Not seeing any card elements in DevTools?

  • Check the Console tab for errors. If you see a network error, the Jikan API may be temporarily rate-limited (it allows 3 requests per second). Wait a few seconds and refresh.
  • Make sure characterLore is defined above loadHero so it's available when loadCharacters runs.
  • Confirm loadCharacters(); appears at the very bottom of the file after loadHero();.

Still stuck? Why aren't my character cards appearing in the DOM?

Style the character grid

The card elements exist in the DOM but they need CSS to appear as a grid of styled cards. You'll use CSS Grid for the responsive layout and set up the card structure for the flip animation that comes next.

  • In style.css, add the following styles below the existing .section-title rule:
#characters {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 24px;
    padding: 20px 40px 60px;
}

.card {
    perspective: 1000px;
    height: 350px;
    cursor: pointer;
}

.card-inner {
    position: relative;
    width: 100%;
    height: 100%;
    transition: transform 0.6s;
    transform-style: preserve-3d;
}

.card-front,
.card-back {
    position: absolute;
    width: 100%;
    height: 100%;
    backface-visibility: hidden;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 0 15px rgba(100, 100, 255, 0.2);
}

.card-front {
    background-color: #1a1a2e;
}

.card-front img {
    width: 100%;
    height: 280px;
    object-fit: cover;
}

.card-front .card-name {
    text-align: center;
    padding: 12px;
    font-size: 1.1rem;
    font-weight: bold;
    color: #e4e4e4;
}

.card-back {
    background-color: #1a1a2e;
    transform: rotateY(180deg);
    padding: 24px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

What does this code do?

  • grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)) creates a responsive grid that automatically fills rows with cards at least 250px wide.
  • perspective: 1000px on .card creates the 3D space needed for the flip effect you'll add next step.
  • backface-visibility: hidden ensures only one face of the card shows at a time during the flip.
  • The .card-back starts rotated 180 degrees so it's hidden behind the front face until the flip triggers.
  • box-shadow: 0 0 15px rgba(100, 100, 255, 0.2) adds a subtle blue glow around each card.
  • Save style.css and refresh Chrome.

You should see six character cards arranged in a responsive grid. Each card shows the character's portrait image and their name below it.

Cards appearing but images are broken?

  • Broken image icons mean the API returned an invalid URL. Check the Console tab for 404 errors on image requests.
  • If the cards are stacked on top of each other instead of in a grid, make sure your #characters selector matches the id="characters" in your HTML.
  • If you see no cards at all, check that script.js is saved and the Console has no syntax errors.

Still stuck? Why aren't my character cards showing up in a grid layout?

✔️ Awesome, I've got everything!

Great. Double check you've saved both script.js and style.css.

ⓧ I'd like to double check the full code

Here is the complete script.js at this point:

const characterLore = {
    124731: {
        name: "Asta",
        strengths: ["Incredible physical strength", "Anti-Magic nullifies all spells", "Never gives up attitude"],
        weaknesses: ["Cannot use magic at all", "Reckless in battle", "Limited range without swords"],
        grimoire: "Five-Leaf Clover Grimoire (Black Clover)",
        magic: "Anti-Magic",
        weapons: ["Demon-Slayer Sword", "Demon-Dweller Sword", "Demon-Destroyer Sword"],
        squad: "Black Bulls"
    },
    124732: {
        name: "Yuno",
        strengths: ["Immense mana reserves", "Spirit Magic with Sylph", "Calm and strategic"],
        weaknesses: ["Can be overconfident", "Emotionally reserved", "Relies heavily on wind spirit"],
        grimoire: "Four-Leaf Clover Grimoire",
        magic: "Wind Magic / Spirit Magic",
        weapons: ["Spirit of Zephyr (Sylph)", "Spirit Storm", "Tornado Fang"],
        squad: "Golden Dawn"
    },
    127141: {
        name: "Noelle Silva",
        strengths: ["Royal-level mana", "Valkyrie Dress combat form", "Sea Dragon's Roar"],
        weaknesses: ["Struggled with mana control early on", "Emotional when family is involved", "Self-doubt from past trauma"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Water Magic",
        weapons: ["Sea Dragon's Roar", "Valkyrie Dress", "Sea Dragon's Cradle"],
        squad: "Black Bulls"
    },
    127764: {
        name: "Yami Sukehiro",
        strengths: ["Dark Magic cuts through dimensions", "Ki sensing ability", "Extreme physical power"],
        weaknesses: ["Spells are slow to activate", "Chain smoker with poor habits", "Reckless leadership style"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Dark Magic",
        weapons: ["Katana", "Dimension Slash", "Dark Cloaked Black Blade"],
        squad: "Black Bulls"
    },
    127886: {
        name: "Luck Voltia",
        strengths: ["Lightning speed in combat", "Battle-hungry instincts", "Lightning Magic versatility"],
        weaknesses: ["Obsessed with fighting", "Ignores teamwork sometimes", "Loses control in excitement"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Lightning Magic",
        weapons: ["Thunderclap Crumbling Orb", "Thunder Fiend", "Lightning Boots"],
        squad: "Black Bulls"
    },
    127049: {
        name: "Magna Swing",
        strengths: ["Creative spell combinations", "Soul Chain Deathmatch technique", "Strong determination"],
        weaknesses: ["Low mana reserves", "Hot-headed personality", "Outclassed by nobles in raw power"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Fire Magic",
        weapons: ["Exploding Fireball", "Vanishing Magic Ball", "Soul Chain Deathmatch"],
        squad: "Black Bulls"
    }
};

function formatName(apiName) {
    if (apiName.includes(", ")) {
        const parts = apiName.split(", ");
        return parts[1] + " " + parts[0];
    }
    return apiName;
}

async function loadHero() {
    try {
        const response = await fetch("https://api.jikan.moe/v4/anime/34572");
        const json = await response.json();
        const anime = json.data;

        document.getElementById("hero-title").textContent = anime.title;
        document.getElementById("hero-synopsis").textContent = anime.synopsis;
        document.getElementById("hero-score").textContent = "Score: " + anime.score + " / 10";
        document.getElementById("hero-cover").src = anime.images.jpg.large_image_url;
        document.getElementById("hero-cover").alt = anime.title;
    } catch (error) {
        document.getElementById("hero-title").textContent = "Black Clover";
        console.error("Failed to load anime data:", error);
    }
}

async function loadCharacters() {
    try {
        const response = await fetch("https://api.jikan.moe/v4/anime/34572/characters");
        const json = await response.json();
        const characters = json.data;

        const container = document.getElementById("characters");

        characters.forEach(function(entry) {
            const malId = entry.character.mal_id;

            if (!characterLore[malId]) return;

            const lore = characterLore[malId];
            const imageUrl = entry.character.images.jpg.image_url;
            const displayName = formatName(entry.character.name);

            const card = document.createElement("div");
            card.className = "card";
            card.setAttribute("data-mal-id", malId);

            card.innerHTML =
                '<div class="card-inner">' +
                    '<div class="card-front">' +
                        '<img src="' + imageUrl + '" alt="' + displayName + '">' +
                        '<div class="card-name">' + displayName + '</div>' +
                    '</div>' +
                    '<div class="card-back">' +
                        '<h3>' + displayName + '</h3>' +
                        '<div class="strengths">' +
                            '<h4>Strengths</h4>' +
                            '<ul>' + lore.strengths.map(function(s) { return '<li>' + s + '</li>'; }).join('') + '</ul>' +
                        '</div>' +
                        '<div class="weaknesses">' +
                            '<h4>Weaknesses</h4>' +
                            '<ul>' + lore.weaknesses.map(function(w) { return '<li>' + w + '</li>'; }).join('') + '</ul>' +
                        '</div>' +
                    '</div>' +
                '</div>';

            container.appendChild(card);
        });
    } catch (error) {
        console.error("Failed to load characters:", error);
    }
}

loadHero();
loadCharacters();

Here is the complete style.css at this point:

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

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #0a0a0f;
    color: #ffffff;
    min-height: 100vh;
}

#hero {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 40px;
    background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 50%, #16213e 100%);
    border-bottom: 2px solid #2d2d44;
    gap: 40px;
    flex-wrap: wrap;
}

#hero-text {
    flex: 1;
    min-width: 300px;
}

#hero-title {
    font-size: 2.5rem;
    margin-bottom: 16px;
    color: #e4e4e4;
}

#hero-synopsis {
    font-size: 1rem;
    line-height: 1.6;
    color: #b0b0b0;
    margin-bottom: 16px;
    max-height: 150px;
    overflow: hidden;
}

#hero-score {
    display: inline-block;
    background: #e63946;
    color: #ffffff;
    padding: 8px 16px;
    border-radius: 8px;
    font-weight: bold;
    font-size: 1.1rem;
}

#hero-image img {
    width: 200px;
    border-radius: 12px;
    box-shadow: 0 0 20px rgba(230, 57, 70, 0.3);
}

.section-title {
    text-align: center;
    font-size: 2rem;
    margin: 40px 0 20px;
    color: #e4e4e4;
}

#characters {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 24px;
    padding: 20px 40px 60px;
}

.card {
    perspective: 1000px;
    height: 350px;
    cursor: pointer;
}

.card-inner {
    position: relative;
    width: 100%;
    height: 100%;
    transition: transform 0.6s;
    transform-style: preserve-3d;
}

.card-front,
.card-back {
    position: absolute;
    width: 100%;
    height: 100%;
    backface-visibility: hidden;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 0 15px rgba(100, 100, 255, 0.2);
}

.card-front {
    background-color: #1a1a2e;
}

.card-front img {
    width: 100%;
    height: 280px;
    object-fit: cover;
}

.card-front .card-name {
    text-align: center;
    padding: 12px;
    font-size: 1.1rem;
    font-weight: bold;
    color: #e4e4e4;
}

.card-back {
    background-color: #1a1a2e;
    transform: rotateY(180deg);
    padding: 24px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

Your character grid is live with real portraits pulled from the API. Next up, you'll add a 3D flip animation so hovering over any card reveals their strengths and weaknesses.

Add Hover Effects for Strengths and Weaknesses

Your character grid looks great with images and names, but right now the cards are static. Each card already has a hidden back face with strengths and weaknesses data waiting to be revealed.

In this step, you will bring those cards to life with a 3D flip animation. When a user hovers over a card, it rotates to show strengths and weaknesses on the back.

In this step, get ready to:
  • Add a CSS hover trigger that rotates the card 180 degrees.
  • Style the back face with color-coded strengths and weaknesses.
  • Test the flip animation across all 6 character cards.
Review the card structure already in place

The loadCharacters() function in script.js already builds each card with a front face (image + name) and a back face (strengths and weaknesses lists). The back face is hidden by default thanks to the backface-visibility: hidden CSS property you added in the previous step.

How does a 3D card flip work?

The flip uses three CSS properties working together. perspective on the parent card creates the 3D space. transform-style: preserve-3d on .card-inner keeps child elements in that 3D space.

The back face starts rotated 180 degrees (so it faces away from you). When you hover, the whole .card-inner rotates 180 degrees, flipping the front away and bringing the back into view.

Add the flip animation and back face styles

The structural CSS for the card (perspective, 3D transform, backface-visibility) is already in your stylesheet. Now you need the hover trigger and the visual styles for the back face content.

  • In style.css, find the .card-inner rule (the one with transform-style: preserve-3d).
  • Add the following CSS directly after the closing brace of .card-inner and before .card-front, .card-back:
/* Trigger the flip when user hovers over a card */
.card:hover .card-inner {
    transform: rotateY(180deg);
}
  • Now scroll to the bottom of style.css (after the .card-back rule). Add these styles to format the back face content:
/* Character name on the back face */
.card-back h3 {
    text-align: center;
    margin-bottom: 16px;
    color: #e4e4e4;
    font-size: 1.2rem;
}

/* Spacing for strengths and weaknesses sections */
.card-back .strengths,
.card-back .weaknesses {
    margin-bottom: 12px;
}

/* Green heading for strengths */
.card-back .strengths h4 {
    color: #4ecdc4;
    margin-bottom: 6px;
}

/* Red heading for weaknesses */
.card-back .weaknesses h4 {
    color: #e63946;
    margin-bottom: 6px;
}

/* Remove default list bullets */
.card-back ul {
    list-style: none;
    padding-left: 0;
}

/* Style each list item */
.card-back ul li {
    padding: 3px 0;
    font-size: 0.9rem;
    color: #b0b0b0;
}

/* Add a dash before each item */
.card-back ul li::before {
    content: "- ";
}

What does this CSS do?

  • .card:hover .card-inner is the trigger. When the user hovers over a .card, its inner container rotates 180 degrees, revealing the back face.
  • The strengths heading uses #4ecdc4 (a teal green) and weaknesses uses #e63946 (a bold red) so users can instantly tell them apart.
  • The ul li::before pseudo-element adds a dash prefix to every list item, replacing the default bullet style for a cleaner look.
  • Save style.css.

Cards not flipping?

  • Make sure the .card:hover .card-inner rule is placed AFTER the .card-inner rule in your file. CSS order matters.
  • Check that transform-style: preserve-3d is present on your .card-inner rule. Without it, child elements ignore the 3D rotation.
  • Confirm your .card-back rule has transform: rotateY(180deg). This starts the back face flipped so it only appears after the hover rotation.

Why isn't my CSS card flip animation working?

Test the flip animation
  • Switch back to Google Chrome and refresh the page.
  • Hover over any character card.
  • Verify that the card smoothly flips to reveal the back face with the character name, strengths in teal text, and weaknesses in red text.
  • Move your mouse away from the card and confirm it flips back to show the character image.
  • Test all 6 cards to make sure each one flips correctly and shows their unique strengths and weaknesses.

Slow down the animation to inspect it

If you want to see the flip in slow motion, temporarily change transition: transform 0.6s in your .card-inner rule to transition: transform 2s. Change it back to 0.6s when you are done inspecting.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css and all 6 cards flip on hover.

ⓧ I'd like to double check the full code

Here is the complete style.css file at this point:

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

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #0a0a0f;
    color: #ffffff;
    min-height: 100vh;
}

#hero {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 40px;
    background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 50%, #16213e 100%);
    border-bottom: 2px solid #2d2d44;
    gap: 40px;
    flex-wrap: wrap;
}

#hero-text {
    flex: 1;
    min-width: 300px;
}

#hero-title {
    font-size: 2.5rem;
    margin-bottom: 16px;
    color: #e4e4e4;
}

#hero-synopsis {
    font-size: 1rem;
    line-height: 1.6;
    color: #b0b0b0;
    margin-bottom: 16px;
    max-height: 150px;
    overflow: hidden;
}

#hero-score {
    display: inline-block;
    background: #e63946;
    color: #ffffff;
    padding: 8px 16px;
    border-radius: 8px;
    font-weight: bold;
    font-size: 1.1rem;
}

#hero-image img {
    width: 200px;
    border-radius: 12px;
    box-shadow: 0 0 20px rgba(230, 57, 70, 0.3);
}

.section-title {
    text-align: center;
    font-size: 2rem;
    margin: 40px 0 20px;
    color: #e4e4e4;
}

#characters {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 24px;
    padding: 20px 40px 60px;
}

.card {
    perspective: 1000px;
    height: 350px;
    cursor: pointer;
}

.card-inner {
    position: relative;
    width: 100%;
    height: 100%;
    transition: transform 0.6s;
    transform-style: preserve-3d;
}

.card:hover .card-inner {
    transform: rotateY(180deg);
}

.card-front,
.card-back {
    position: absolute;
    width: 100%;
    height: 100%;
    backface-visibility: hidden;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 0 15px rgba(100, 100, 255, 0.2);
}

.card-front {
    background-color: #1a1a2e;
}

.card-front img {
    width: 100%;
    height: 280px;
    object-fit: cover;
}

.card-front .card-name {
    text-align: center;
    padding: 12px;
    font-size: 1.1rem;
    font-weight: bold;
    color: #e4e4e4;
}

.card-back {
    background-color: #1a1a2e;
    transform: rotateY(180deg);
    padding: 24px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.card-back h3 {
    text-align: center;
    margin-bottom: 16px;
    color: #e4e4e4;
    font-size: 1.2rem;
}

.card-back .strengths,
.card-back .weaknesses {
    margin-bottom: 12px;
}

.card-back .strengths h4 {
    color: #4ecdc4;
    margin-bottom: 6px;
}

.card-back .weaknesses h4 {
    color: #e63946;
    margin-bottom: 6px;
}

.card-back ul {
    list-style: none;
    padding-left: 0;
}

.card-back ul li {
    padding: 3px 0;
    font-size: 0.9rem;
    color: #b0b0b0;
}

.card-back ul li::before {
    content: "- ";
}

Your cards now flip to reveal each character's combat profile. Next up, you will add a click interaction that opens a full modal with grimoire and weapons details.

Add Click Modal for Grimoire and Weapons

Your character cards now flip to reveal strengths and weaknesses on hover. That's great for a quick glance, but what about the deeper lore?

Each character in Black Clover has a unique grimoire, magic type, and signature weapons. A modal popup gives you the space to showcase all of that without cluttering the card grid.

In this step, get ready to:
  • Write an openModal function that fills the popup with character-specific data.
  • Wire up click events on each card to trigger the modal.
  • Add close functionality so users can dismiss the modal.
Add the openModal function and card click listeners

When a user clicks a character card, you need a function that grabs that character's grimoire, magic type, and weapons from your characterLore object and injects them into the modal's HTML elements.

  • In script.js, find the closing brace of your loadCharacters() function.
  • Add the following openModal function below it:
function openModal(malId, imageUrl, displayName) {
    // Look up the character's lore using their MAL ID
    const lore = characterLore[malId];
    const modal = document.getElementById("modal");

    // Fill the modal with this character's data
    document.getElementById("modal-image").src = imageUrl;
    document.getElementById("modal-name").textContent = displayName;
    document.getElementById("modal-grimoire").textContent = lore.grimoire;
    document.getElementById("modal-magic").textContent = lore.magic;
    document.getElementById("modal-weapons").innerHTML = lore.weapons.map(function(w) {
        return '<li>' + w + '</li>';
    }).join('');

    // Show the modal by removing the hidden class
    modal.classList.remove("hidden");
}

What does this code do?

The function takes three parameters: malId to look up the character's lore data, imageUrl for their portrait, and displayName for the formatted name.

It fills each modal element (grimoire, magic, weapons list) with the correct character's data, then removes the hidden class to make the modal visible.

Now you need to call this function when the learner clicks a card. The click listener goes inside your loadCharacters() function, right where each card is built.

  • In script.js, scroll up to the loadCharacters() function.
  • Find this line near the bottom of the forEach loop:
            container.appendChild(card);
  • Add the following click event listener directly above that line:
            card.addEventListener("click", function() {
                openModal(malId, imageUrl, displayName);
            });

Why add the listener inside the loop?

Each card needs its own click handler that passes its specific malId, imageUrl, and displayName. By adding the listener inside the loop, each card's handler captures the correct values for that character.

  • Save script.js.

Modal not appearing when you click?

Make sure the card.addEventListener line sits before container.appendChild(card) and after the card.innerHTML assignment.

Check the Console tab in DevTools for errors. A typo in openModal or characterLore will show a reference error there.

Help me debug why my modal isn't opening when I click a card.

Review the modal styles in your CSS

Your style.css already has modal styles from when you set up the page structure. These styles control how the modal appears as a full-screen overlay with centered content.

What do the modal styles do?

  • .modal.hidden uses display: none to completely hide the modal until a card is clicked.
  • .modal is position: fixed covering the full viewport with flexbox centering the content.
  • .modal-overlay creates a dark semi-transparent background (rgba(0, 0, 0, 0.8)) behind the popup.
  • #modal-image displays the character portrait as a circle using border-radius: 50%.
  • Each .modal-section gets a darker background card to visually separate grimoire, magic, and weapons info.
  • Refresh Chrome and click any character card.
  • You should see a dark overlay appear with the character's portrait in a circle, their name, and sections for Grimoire, Magic Type, and Weapons.
Add close functionality and test the modal

The modal opens, but right now there's no way to close it. You need a closeModal function that re-adds the hidden class, plus event listeners on both the close button and the dark overlay.

  • In script.js, add the following code below your openModal function:
function closeModal() {
    // Re-add hidden class to hide the modal
    document.getElementById("modal").classList.add("hidden");
}

// Close when clicking the dark overlay background
document.querySelector(".modal-overlay").addEventListener("click", closeModal);
// Close when clicking the X button
document.querySelector(".modal-close").addEventListener("click", closeModal);

What does this code do?

The closeModal function adds the hidden class back to the modal element, which triggers display: none in your CSS.

Two separate event listeners call this function. One fires when the user clicks the dark overlay area around the popup. The other fires when they click the X close button inside the modal.

  • Save script.js and refresh Chrome.
  • Click any character card to open the modal. Verify you see that character's grimoire, magic type, and weapons list.
  • Click the X button in the top-right corner of the modal to close it.
  • Open a different card, then click the dark overlay area outside the popup to close it.

Close button or overlay not working?

Make sure the document.querySelector lines sit outside any function (at the top level of your script). They run once when the page loads and attach listeners to the overlay and close button.

If clicking the overlay does nothing, check that your HTML has a <div class="modal-overlay"></div> inside the modal div. The class name must match exactly.

Help me figure out why my modal close button or overlay click isn't dismissing the modal.

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js before moving on.

ⓧ I'd like to double check the full code

Here is the complete script.js at the end of this step:

const characterLore = {
    124731: {
        name: "Asta",
        strengths: ["Incredible physical strength", "Anti-Magic nullifies all spells", "Never gives up attitude"],
        weaknesses: ["Cannot use magic at all", "Reckless in battle", "Limited range without swords"],
        grimoire: "Five-Leaf Clover Grimoire (Black Clover)",
        magic: "Anti-Magic",
        weapons: ["Demon-Slayer Sword", "Demon-Dweller Sword", "Demon-Destroyer Sword"],
        squad: "Black Bulls"
    },
    124732: {
        name: "Yuno",
        strengths: ["Immense mana reserves", "Spirit Magic with Sylph", "Calm and strategic"],
        weaknesses: ["Can be overconfident", "Emotionally reserved", "Relies heavily on wind spirit"],
        grimoire: "Four-Leaf Clover Grimoire",
        magic: "Wind Magic / Spirit Magic",
        weapons: ["Spirit of Zephyr (Sylph)", "Spirit Storm", "Tornado Fang"],
        squad: "Golden Dawn"
    },
    127141: {
        name: "Noelle Silva",
        strengths: ["Royal-level mana", "Valkyrie Dress combat form", "Sea Dragon's Roar"],
        weaknesses: ["Struggled with mana control early on", "Emotional when family is involved", "Self-doubt from past trauma"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Water Magic",
        weapons: ["Sea Dragon's Roar", "Valkyrie Dress", "Sea Dragon's Cradle"],
        squad: "Black Bulls"
    },
    127764: {
        name: "Yami Sukehiro",
        strengths: ["Dark Magic cuts through dimensions", "Ki sensing ability", "Extreme physical power"],
        weaknesses: ["Spells are slow to activate", "Chain smoker with poor habits", "Reckless leadership style"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Dark Magic",
        weapons: ["Katana", "Dimension Slash", "Dark Cloaked Black Blade"],
        squad: "Black Bulls"
    },
    127886: {
        name: "Luck Voltia",
        strengths: ["Lightning speed in combat", "Battle-hungry instincts", "Lightning Magic versatility"],
        weaknesses: ["Obsessed with fighting", "Ignores teamwork sometimes", "Loses control in excitement"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Lightning Magic",
        weapons: ["Thunderclap Crumbling Orb", "Thunder Fiend", "Lightning Boots"],
        squad: "Black Bulls"
    },
    127049: {
        name: "Magna Swing",
        strengths: ["Creative spell combinations", "Soul Chain Deathmatch technique", "Strong determination"],
        weaknesses: ["Low mana reserves", "Hot-headed personality", "Outclassed by nobles in raw power"],
        grimoire: "Three-Leaf Clover Grimoire",
        magic: "Fire Magic",
        weapons: ["Exploding Fireball", "Vanishing Magic Ball", "Soul Chain Deathmatch"],
        squad: "Black Bulls"
    }
};

function formatName(apiName) {
    if (apiName.includes(", ")) {
        const parts = apiName.split(", ");
        return parts[1] + " " + parts[0];
    }
    return apiName;
}

async function loadHero() {
    try {
        const response = await fetch("https://api.jikan.moe/v4/anime/34572");
        const json = await response.json();
        const anime = json.data;

        document.getElementById("hero-title").textContent = anime.title;
        document.getElementById("hero-synopsis").textContent = anime.synopsis;
        document.getElementById("hero-score").textContent = "Score: " + anime.score + " / 10";
        document.getElementById("hero-cover").src = anime.images.jpg.large_image_url;
        document.getElementById("hero-cover").alt = anime.title;
    } catch (error) {
        document.getElementById("hero-title").textContent = "Black Clover";
        console.error("Failed to load anime data:", error);
    }
}

async function loadCharacters() {
    try {
        const response = await fetch("https://api.jikan.moe/v4/anime/34572/characters");
        const json = await response.json();
        const characters = json.data;

        const container = document.getElementById("characters");

        characters.forEach(function(entry) {
            const malId = entry.character.mal_id;

            if (!characterLore[malId]) return;

            const lore = characterLore[malId];
            const imageUrl = entry.character.images.jpg.image_url;
            const displayName = formatName(entry.character.name);

            const card = document.createElement("div");
            card.className = "card";
            card.setAttribute("data-mal-id", malId);

            card.innerHTML =
                '<div class="card-inner">' +
                    '<div class="card-front">' +
                        '<img src="' + imageUrl + '" alt="' + displayName + '">' +
                        '<div class="card-name">' + displayName + '</div>' +
                    '</div>' +
                    '<div class="card-back">' +
                        '<h3>' + displayName + '</h3>' +
                        '<div class="strengths">' +
                            '<h4>Strengths</h4>' +
                            '<ul>' + lore.strengths.map(function(s) { return '<li>' + s + '</li>'; }).join('') + '</ul>' +
                        '</div>' +
                        '<div class="weaknesses">' +
                            '<h4>Weaknesses</h4>' +
                            '<ul>' + lore.weaknesses.map(function(w) { return '<li>' + w + '</li>'; }).join('') + '</ul>' +
                        '</div>' +
                    '</div>' +
                '</div>';

            card.addEventListener("click", function() {
                openModal(malId, imageUrl, displayName);
            });

            container.appendChild(card);
        });
    } catch (error) {
        console.error("Failed to load characters:", error);
    }
}

function openModal(malId, imageUrl, displayName) {
    const lore = characterLore[malId];
    const modal = document.getElementById("modal");

    document.getElementById("modal-image").src = imageUrl;
    document.getElementById("modal-name").textContent = displayName;
    document.getElementById("modal-grimoire").textContent = lore.grimoire;
    document.getElementById("modal-magic").textContent = lore.magic;
    document.getElementById("modal-weapons").innerHTML = lore.weapons.map(function(w) {
        return '<li>' + w + '</li>';
    }).join('');

    modal.classList.remove("hidden");
}

function closeModal() {
    document.getElementById("modal").classList.add("hidden");
}

document.querySelector(".modal-overlay").addEventListener("click", closeModal);
document.querySelector(".modal-close").addEventListener("click", closeModal);

loadHero();
loadCharacters();

Your Black Clover fan site is fully interactive. Cards flip on hover and open detailed modals on click. You've combined live API data with custom lore and CSS animations to build a polished front-end project.

Secret mission

Add a Magic Knight Squad Filter

Your character grid shows every character at once. What if a user only wants to see Black Bulls members, or filter down to the Golden Dawn? Build a squad filter bar that dynamically generates buttons and filters the card grid with a single click.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. All data comes from the free Jikan API, which requires no account or API key.

Decide whether to keep your project files on your Desktop or delete them entirely.

Resources you used:

  • The black-clover-site folder on your Desktop (containing index.html, style.css, and script.js).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your Black Clover site or add more characters and features.

  • Your files stay in the black-clover-site folder on your Desktop.
  • You can reopen the project anytime by opening the folder in VS Code.
  • You can host it for free using GitHub Pages if you want to share it with others.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files are already saved on your Desktop and ready whenever you come back.

  • Close the Chrome tab with your site open.
  • Close the VS Code window.
  • Your black-clover-site folder remains on your Desktop for next time.

Delete - I don't want to use this again

Remove all project files from your computer. This cannot be undone.

  • Close VS Code and the Chrome tab showing your site.
  • Delete the project folder by running one of the following commands in your terminal:

macOS

rm -rf ~/Desktop/black-clover-site

Windows

rmdir /s /q "$HOME\Desktop\black-clover-site"
  • Confirm the folder is gone by checking your Desktop.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully interactive Black Clover fan website that pulls live data from a real API and turns it into a polished, animated experience.

You've learned how to:

  • Fetched live anime data from the Jikan REST API and rendered it into a hero banner with the cover image, synopsis, and score.
  • Created an interactive character card grid with CSS 3D flip animations that reveal strengths and weaknesses on hover.
  • Built a click-triggered modal system using DOM manipulation and event listeners to display grimoire and weapons details for each character.
  • Secret Mission: Added a Magic Knight Squad filter system with dynamically generated buttons and active state management.

Ready to quiz yourself?