Build an Animated Landing Page

Use Motion's vanilla JS APIs to animate a portfolio landing page.

Introduction

30 Second Summary

The best websites feel alive. Buttons respond to your touch, text sweeps onto the screen, and backgrounds shift as you scroll.

In this project, you will build an animated landing page using Motion's vanilla JavaScript API. You'll create staggered text animations, hover effects, scroll-triggered reveals, and parallax motion in a single HTML file.

What You'll Build

A single-file animated portfolio landing page showcasing four distinct Motion animation techniques, all without React or a build system.

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

  • A hero section with staggered text that cascades in using spring physics animations.
  • Interactive project cards that respond to hover with smooth scaling and lift effects.
  • A skills section that slides into view as you scroll down the page.
  • A parallax background effect where elements move at different speeds as you scroll.
  • Secret Mission: A scroll-linked progress bar that fills as the user scrolls down the page.

Are there any prerequisites?

Basic HTML and CSS knowledge is all you need. No JavaScript frameworks or build tools are required for this project.

Set Up Your Project

To build an animated landing page, you need a single HTML file that combines structure, styling, and the Motion animation library.

In this step, you'll create an index.html file with four page sections, import Motion from a CDN, and add CSS that prepares elements for animation. No install step required.

In this step, get ready to:
  • Create index.html with four page sections and a Motion CDN import.
  • Style the page with a dark colour scheme and animation-ready elements.
  • Verify the page loads correctly in your browser.
Create the HTML file with page structure and Motion import

Motion loads directly from a CDN using a standard <script type="module"> tag. This means zero installation and zero build tools.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save it immediately with Cmd+S (macOS) or Ctrl+S (Windows).
  • Navigate to your Desktop, name the file index.html, and click Save.
  • Set up the HTML page structure by pasting this 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>Animated Portfolio</title>
    <style>
        /* CSS styles will go here in the next substep */
    </style>
</head>
<body>
    <!-- Hero Section -->
    <section class="hero">
        <div class="parallax-bg"></div>
        <div class="hero-content">
            <h1 class="hero-text">
                <span>Hi,</span>
                <span>I</span>
                <span>build</span>
                <span>things</span>
                <span>for</span>
                <span>the</span>
                <span>web.</span>
            </h1>
            <p class="hero-subtitle">Frontend developer and animation enthusiast</p>
        </div>
    </section>

    <!-- Projects Section -->
    <section class="projects">
        <h2>Projects</h2>
        <div class="cards-grid">
            <div class="card">
                <h3>Weather Dashboard</h3>
                <p>A real-time weather app with animated transitions between forecasts.</p>
            </div>
            <div class="card">
                <h3>Task Manager</h3>
                <p>A drag-and-drop task board with smooth reordering animations.</p>
            </div>
            <div class="card">
                <h3>Music Visualizer</h3>
                <p>An audio visualizer that responds to music with dynamic motion.</p>
            </div>
        </div>
    </section>

    <!-- Skills Section -->
    <section class="skills">
        <h2>Skills</h2>
        <div class="skills-list">
            <div class="skill-item">
                <div class="skill-icon">JS</div>
                <span class="skill-name">JavaScript</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Re</div>
                <span class="skill-name">React</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Cs</div>
                <span class="skill-name">CSS Animations</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Mo</div>
                <span class="skill-name">Motion</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Ts</div>
                <span class="skill-name">TypeScript</span>
            </div>
        </div>
    </section>

    <!-- Footer -->
    <section class="footer">
        <p>Built with Motion. Animated with joy.</p>
    </section>

    <script type="module">
        import { animate, stagger, inView, scroll, hover } from "https://cdn.jsdelivr.net/npm/motion@12/+esm"
    </script>
</body>
</html>

What does this code do?

  • The <body> contains four sections: a hero with staggered text spans, a projects grid with cards, a skills list, and a footer.
  • Each word in the hero heading is wrapped in its own <span> so Motion can animate them individually.
  • The <script type="module"> tag imports five Motion functions directly from the jsDelivr CDN. No npm install needed.
  • Using @12 in the CDN URL pins to the major version for stability while still receiving patch updates.

💡 Why CDN instead of npm?

Loading Motion from a CDN means your entire project is a single HTML file. You can open it directly in any browser without running a dev server or installing Node.js. This keeps the focus on learning Motion's animation APIs instead of managing tooling.

  • Save index.html.
  • Navigate to your desktop folder, and double click your new index.html file to open it in your browser.

You won't see a styled page yet since the <style> tag is empty. That comes next.

Add CSS styling for the page layout

The CSS sets up a dark colour scheme, full-viewport section heights, and starting opacity values for elements that Motion will animate later.

  • In index.html, find the comment /* CSS styles will go here in the next substep */ inside the <style> tag.
  • Replace that comment with the following CSS:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #0a0a0a;
    color: #ffffff;
    overflow-x: hidden;
}

/* Hero Section */
.hero {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    position: relative;
    overflow: hidden;
}

.parallax-bg {
    position: absolute;
    top: -50px;
    left: 0;
    right: 0;
    bottom: -50px;
    background: radial-gradient(circle at 30% 40%, #1a0533 0%, transparent 50%),
                radial-gradient(circle at 70% 60%, #0a1628 0%, transparent 50%);
    z-index: 0;
}

.hero-content {
    position: relative;
    z-index: 1;
    text-align: center;
}

.hero-text {
    font-size: 4rem;
    font-weight: 700;
    line-height: 1.2;
    margin-bottom: 1.5rem;
}

.hero-text span {
    display: inline-block;
    opacity: 0;
    margin: 0 0.3rem;
}

.hero-subtitle {
    font-size: 1.3rem;
    color: #888;
    opacity: 0;
}

/* Projects Section */
.projects {
    min-height: 100vh;
    padding: 6rem 2rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.projects h2 {
    font-size: 2.5rem;
    margin-bottom: 3rem;
}

.cards-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 2rem;
    max-width: 900px;
    width: 100%;
}

.card {
    background: #1a1a2e;
    border-radius: 16px;
    padding: 2rem;
    border: 1px solid #2a2a4a;
    cursor: pointer;
}

.card h3 {
    font-size: 1.3rem;
    margin-bottom: 0.75rem;
    color: #e0e0ff;
}

.card p {
    color: #888;
    line-height: 1.6;
}

/* Skills Section */
.skills {
    min-height: 100vh;
    padding: 6rem 2rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.skills h2 {
    font-size: 2.5rem;
    margin-bottom: 3rem;
}

.skills-list {
    max-width: 600px;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.skill-item {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1.25rem 1.5rem;
    background: #1a1a2e;
    border-radius: 12px;
    border: 1px solid #2a2a4a;
    opacity: 0;
}

.skill-icon {
    width: 40px;
    height: 40px;
    border-radius: 8px;
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.2rem;
    flex-shrink: 0;
}

.skill-name {
    font-size: 1.1rem;
    font-weight: 500;
}

/* Footer */
.footer {
    padding: 4rem 2rem;
    text-align: center;
    color: #666;
}

What does this CSS do?

  • Each section uses min-height: 100vh to fill the full viewport height, creating distinct scroll zones.
  • The body background is #0a0a0a (near-black) with white text for a sleek dark theme.
  • Both .hero-text span and .skill-item start with opacity: 0. Motion will animate these to visible when the time is right.
  • The .parallax-bg is positioned absolutely with subtle purple and blue radial gradients to give depth.
  • Save index.html.

Seeing unstyled text or a white page?

  • Make sure your CSS is inside the <style> tags in the <head>, not in the <body>.
  • Check that you replaced the comment entirely. Leftover /* characters can break the stylesheet.

Why isn't my CSS applying to the page?

Open the file in your browser to verify
  • Open your file manager (Finder on macOS or File Explorer on Windows) and navigate to your Desktop.
  • Double-click index.html to open it in your default browser.

You should see a dark page with the "Projects" and "Skills" headings visible. The hero text and skill items are invisible because their opacity is set to 0. That is correct.

  • Open your browser's developer console by pressing Cmd+Option+J (macOS Chrome) or Ctrl+Shift+J (Windows Chrome).
  • Confirm there are no red error messages in the console.

What should I see?

A near-black page with "Projects" and "Skills" as white section headings. The hero area appears mostly empty (the text spans are hidden). Three dark card shapes are visible in the Projects section. The footer text is faintly visible at the bottom.

🙋‍♀️ Seeing console errors?

  • If you see a CORS or network error related to the Motion import, make sure you are connected to the internet. The CDN import needs a network connection to load.
  • If you see a syntax error, double-check that <script type="module"> appears exactly as written, with the type="module" attribute present.

Help me fix console errors in my Motion CDN import.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete index.html file you should have 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>Animated Portfolio</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #0a0a0a;
            color: #ffffff;
            overflow-x: hidden;
        }

        /* Hero Section */
        .hero {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            position: relative;
            overflow: hidden;
        }

        .parallax-bg {
            position: absolute;
            top: -50px;
            left: 0;
            right: 0;
            bottom: -50px;
            background: radial-gradient(circle at 30% 40%, #1a0533 0%, transparent 50%),
                        radial-gradient(circle at 70% 60%, #0a1628 0%, transparent 50%);
            z-index: 0;
        }

        .hero-content {
            position: relative;
            z-index: 1;
            text-align: center;
        }

        .hero-text {
            font-size: 4rem;
            font-weight: 700;
            line-height: 1.2;
            margin-bottom: 1.5rem;
        }

        .hero-text span {
            display: inline-block;
            opacity: 0;
            margin: 0 0.3rem;
        }

        .hero-subtitle {
            font-size: 1.3rem;
            color: #888;
            opacity: 0;
        }

        /* Projects Section */
        .projects {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .projects h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .cards-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 2rem;
            max-width: 900px;
            width: 100%;
        }

        .card {
            background: #1a1a2e;
            border-radius: 16px;
            padding: 2rem;
            border: 1px solid #2a2a4a;
            cursor: pointer;
        }

        .card h3 {
            font-size: 1.3rem;
            margin-bottom: 0.75rem;
            color: #e0e0ff;
        }

        .card p {
            color: #888;
            line-height: 1.6;
        }

        /* Skills Section */
        .skills {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .skills h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .skills-list {
            max-width: 600px;
            width: 100%;
            display: flex;
            flex-direction: column;
            gap: 1.5rem;
        }

        .skill-item {
            display: flex;
            align-items: center;
            gap: 1rem;
            padding: 1.25rem 1.5rem;
            background: #1a1a2e;
            border-radius: 12px;
            border: 1px solid #2a2a4a;
            opacity: 0;
        }

        .skill-icon {
            width: 40px;
            height: 40px;
            border-radius: 8px;
            background: linear-gradient(135deg, #6366f1, #8b5cf6);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1.2rem;
            flex-shrink: 0;
        }

        .skill-name {
            font-size: 1.1rem;
            font-weight: 500;
        }

        /* Footer */
        .footer {
            padding: 4rem 2rem;
            text-align: center;
            color: #666;
        }
    </style>
</head>
<body>
    <!-- Hero Section -->
    <section class="hero">
        <div class="parallax-bg"></div>
        <div class="hero-content">
            <h1 class="hero-text">
                <span>Hi,</span>
                <span>I</span>
                <span>build</span>
                <span>things</span>
                <span>for</span>
                <span>the</span>
                <span>web.</span>
            </h1>
            <p class="hero-subtitle">Frontend developer and animation enthusiast</p>
        </div>
    </section>

    <!-- Projects Section -->
    <section class="projects">
        <h2>Projects</h2>
        <div class="cards-grid">
            <div class="card">
                <h3>Weather Dashboard</h3>
                <p>A real-time weather app with animated transitions between forecasts.</p>
            </div>
            <div class="card">
                <h3>Task Manager</h3>
                <p>A drag-and-drop task board with smooth reordering animations.</p>
            </div>
            <div class="card">
                <h3>Music Visualizer</h3>
                <p>An audio visualizer that responds to music with dynamic motion.</p>
            </div>
        </div>
    </section>

    <!-- Skills Section -->
    <section class="skills">
        <h2>Skills</h2>
        <div class="skills-list">
            <div class="skill-item">
                <div class="skill-icon">JS</div>
                <span class="skill-name">JavaScript</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Re</div>
                <span class="skill-name">React</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Cs</div>
                <span class="skill-name">CSS Animations</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Mo</div>
                <span class="skill-name">Motion</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Ts</div>
                <span class="skill-name">TypeScript</span>
            </div>
        </div>
    </section>

    <!-- Footer -->
    <section class="footer">
        <p>Built with Motion. Animated with joy.</p>
    </section>

    <script type="module">
        import { animate, stagger, inView, scroll, hover } from "https://cdn.jsdelivr.net/npm/motion@12/+esm"
    </script>
</body>
</html>

Your page skeleton is ready. Next up, you'll bring the hero section to life with staggered spring animations.

Animate the Hero Section

Your page structure is in place, but right now the hero section is invisible. The .hero-text span elements all have opacity: 0 in the CSS, waiting for JavaScript to bring them to life.

In this step, you'll write your first Motion animations using animate() and stagger() to make the hero heading cascade in word-by-word with spring physics.

In this step, get ready to:
  • Animate the hero heading with staggered spring physics.
  • Add a subtitle animation with a timed delay and custom easing.
Animate the hero heading with stagger and spring physics

The hero heading is split into seven <span> elements, one per word. You'll animate all of them with a single animate() call, using stagger() to offset each word's start time so they cascade in one after another.

  • In index.html, scroll down to the <script type="module"> block at the bottom of the file.
  • Find the import line:
import { animate, stagger, inView, scroll, hover } from "https://cdn.jsdelivr.net/npm/motion@12/+esm"
  • Add the staggered hero animation below the import by pasting this code:
// Step 2: Staggered hero animation with spring physics
animate(".hero-text span", { opacity: 1, y: [40, 0] }, {
    delay: stagger(0.1),
    type: "spring",
    stiffness: 100,
    damping: 15
})

What does this code do?

This single animate() call targets every <span> inside .hero-text using a CSS selector. Motion finds all seven word spans and animates them together.

  • opacity: 1 fades each word from invisible to fully visible.
  • y: [40, 0] moves each word from 40 pixels below its final position up to 0 (its natural spot).
  • delay: stagger(0.1) offsets each word by 0.1 seconds, creating the cascading effect.
  • type: "spring" uses physics-based motion instead of a fixed duration. The words overshoot slightly and settle into place.

💡 What are stiffness and damping?

Think of a spring on a door. stiffness: 100 controls how snappy the spring is. Higher values make the animation faster and more aggressive.

damping: 15 controls how quickly the bouncing settles. Lower damping means more oscillation. Higher damping means the element arrives at rest sooner with less bounce.

  • Save index.html.
  • Refresh your browser. You should see the hero words cascade in one by one from below, each with a slight bounce as they land.

Don't see the words animating in?

  • Make sure you saved index.html. Unsaved changes won't appear in the browser.
  • Open DevTools (F12) and check the Console tab for errors. A typo in the import URL or the animate call will show a red error message.
  • Confirm your code is inside the <script type="module"> block, not outside it.

Help me debug why my staggered hero animation isn't playing

Add the subtitle animation with a timed delay

The subtitle should appear after the heading finishes cascading in. Instead of spring physics, you'll use a fixed duration with a custom easing curve for a smooth, polished feel.

  • Add the subtitle animation below the hero text animation by pasting this code:
animate(".hero-subtitle", { opacity: 1, y: [20, 0] }, {
    duration: 0.8,
    delay: 0.6,
    easing: [0.25, 0.1, 0.25, 1]
})

What does this code do?

  • delay: 0.6 waits 0.6 seconds before starting, giving the heading time to finish cascading in first.
  • duration: 0.8 makes the animation last 0.8 seconds. Unlike the spring above, this uses a fixed time rather than physics.
  • easing: [0.25, 0.1, 0.25, 1] is a cubic-bezier curve that starts slowly, accelerates, then eases into the final position. This gives the subtitle a polished, editorial feel.

💡 Spring vs duration-based animation

The heading uses type: "spring" which has no fixed duration. It simulates physics until the element comes to rest. The subtitle uses duration and easing instead, which always completes in exactly the time you specify.

Springs feel more natural and alive. Duration-based easing feels more controlled and predictable. Mixing both in the same sequence creates visual contrast.

  • Save index.html and refresh your browser.
  • You should see the hero words cascade in with a springy bounce, followed by the subtitle "Frontend developer and animation enthusiast" fading up smoothly about half a second later.

Subtitle not appearing?

  • Check that the selector is .hero-subtitle (with a dot). Missing the dot means Motion looks for a <hero-subtitle> element that doesn't exist.
  • Make sure your second animate() call is after the first one, not inside it. Each call is a separate statement.

Help me debug why my subtitle animation isn't showing

💡 Experiment with spring values

Try changing stiffness to 300 and damping to 5 in the hero text animation, then refresh. You'll see a much snappier, bouncier result. Ask Claude to help you pick spring values for different moods ("playful", "elegant", "punchy").

✔️ 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 complete index.html file as it should look 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>Animated Portfolio</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #0a0a0a;
            color: #ffffff;
            overflow-x: hidden;
        }

        /* Hero Section */
        .hero {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            position: relative;
            overflow: hidden;
        }

        .parallax-bg {
            position: absolute;
            top: -50px;
            left: 0;
            right: 0;
            bottom: -50px;
            background: radial-gradient(circle at 30% 40%, #1a0533 0%, transparent 50%),
                        radial-gradient(circle at 70% 60%, #0a1628 0%, transparent 50%);
            z-index: 0;
        }

        .hero-content {
            position: relative;
            z-index: 1;
            text-align: center;
        }

        .hero-text {
            font-size: 4rem;
            font-weight: 700;
            line-height: 1.2;
            margin-bottom: 1.5rem;
        }

        .hero-text span {
            display: inline-block;
            opacity: 0;
            margin: 0 0.3rem;
        }

        .hero-subtitle {
            font-size: 1.3rem;
            color: #888;
            opacity: 0;
        }

        /* Projects Section */
        .projects {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .projects h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .cards-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 2rem;
            max-width: 900px;
            width: 100%;
        }

        .card {
            background: #1a1a2e;
            border-radius: 16px;
            padding: 2rem;
            border: 1px solid #2a2a4a;
            cursor: pointer;
        }

        .card h3 {
            font-size: 1.3rem;
            margin-bottom: 0.75rem;
            color: #e0e0ff;
        }

        .card p {
            color: #888;
            line-height: 1.6;
        }

        /* Skills Section */
        .skills {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .skills h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .skills-list {
            max-width: 600px;
            width: 100%;
            display: flex;
            flex-direction: column;
            gap: 1.5rem;
        }

        .skill-item {
            display: flex;
            align-items: center;
            gap: 1rem;
            padding: 1.25rem 1.5rem;
            background: #1a1a2e;
            border-radius: 12px;
            border: 1px solid #2a2a4a;
            opacity: 0;
        }

        .skill-icon {
            width: 40px;
            height: 40px;
            border-radius: 8px;
            background: linear-gradient(135deg, #6366f1, #8b5cf6);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1.2rem;
            flex-shrink: 0;
        }

        .skill-name {
            font-size: 1.1rem;
            font-weight: 500;
        }

        /* Footer */
        .footer {
            padding: 4rem 2rem;
            text-align: center;
            color: #666;
        }
    </style>
</head>
<body>
    <!-- Hero Section -->
    <section class="hero">
        <div class="parallax-bg"></div>
        <div class="hero-content">
            <h1 class="hero-text">
                <span>Hi,</span>
                <span>I</span>
                <span>build</span>
                <span>things</span>
                <span>for</span>
                <span>the</span>
                <span>web.</span>
            </h1>
            <p class="hero-subtitle">Frontend developer and animation enthusiast</p>
        </div>
    </section>

    <!-- Projects Section -->
    <section class="projects">
        <h2>Projects</h2>
        <div class="cards-grid">
            <div class="card">
                <h3>Weather Dashboard</h3>
                <p>A real-time weather app with animated transitions between forecasts.</p>
            </div>
            <div class="card">
                <h3>Task Manager</h3>
                <p>A drag-and-drop task board with smooth reordering animations.</p>
            </div>
            <div class="card">
                <h3>Music Visualizer</h3>
                <p>An audio visualizer that responds to music with dynamic motion.</p>
            </div>
        </div>
    </section>

    <!-- Skills Section -->
    <section class="skills">
        <h2>Skills</h2>
        <div class="skills-list">
            <div class="skill-item">
                <div class="skill-icon">JS</div>
                <span class="skill-name">JavaScript</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Re</div>
                <span class="skill-name">React</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Cs</div>
                <span class="skill-name">CSS Animations</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Mo</div>
                <span class="skill-name">Motion</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Ts</div>
                <span class="skill-name">TypeScript</span>
            </div>
        </div>
    </section>

    <!-- Footer -->
    <section class="footer">
        <p>Built with Motion. Animated with joy.</p>
    </section>

    <script type="module">
        import { animate, stagger, inView, scroll, hover } from "https://cdn.jsdelivr.net/npm/motion@12/+esm"

        // Step 2: Staggered hero animation with spring physics
        animate(".hero-text span", { opacity: 1, y: [40, 0] }, {
            delay: stagger(0.1),
            type: "spring",
            stiffness: 100,
            damping: 15
        })

        animate(".hero-subtitle", { opacity: 1, y: [20, 0] }, {
            duration: 0.8,
            delay: 0.6,
            easing: [0.25, 0.1, 0.25, 1]
        })
    </script>
</body>
</html>

Your hero section now greets visitors with a satisfying cascade of text. Next up, you'll make the project cards respond to hover with smooth gesture-driven animations.

Add Hover Animations to Project Cards

Your hero section now animates on load with staggered spring physics. The next layer of polish comes from making your page respond to user interaction.

In this step, you will use Motion's hover() function to make your project cards react when someone moves their mouse over them. The cards will scale up and lift with a spring animation, then settle back into place when the mouse leaves.

In this step, get ready to:
  • Add a hover gesture animation to your project cards using Motion's hover() function.
  • Explore spring physics values to understand how stiffness and damping create different animation feels.
Write the hover animation for project cards

So far, your animations play automatically when the page loads. Gesture-based animations are different. They respond to something the user does, like hovering their mouse over an element.

Motion's hover() function takes a CSS selector and a callback. The callback fires when the mouse enters the element. If you return a function from the callback, that returned function fires when the mouse leaves.

  • In index.html, scroll down to the <script type="module"> block.
  • Find the last animate() call (the one targeting .hero-subtitle).
  • Add the hover animation below it by pasting this code:
        // Step 3: Hover gesture animations on project cards
        hover(".card", (element) => {
            animate(element, { scale: 1.05, y: -8 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })

            return () => animate(element, { scale: 1, y: 0 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })
        })

What does this code do?

  • hover(".card", ...) listens for hover events on every element with the class card. Motion automatically filters out touch events, so this only fires for real mouse hovers.
  • The callback receives the hovered element and animates it to scale: 1.05 (5% bigger) and y: -8 (lifts 8 pixels upward).
  • The return () => function fires when the mouse leaves. It animates the card back to its original size and position.
  • Both animations use type: "spring" with higher stiffness (300) and damping (20) than the hero animation, making the card feel snappy and responsive.
  • Save index.html.
  • Refresh your browser and scroll down to the Projects section.
  • Hover your mouse over any project card. You should see the card smoothly scale up and lift, then settle back down when your mouse leaves.

Cards not animating on hover?

  • Make sure you pasted the code AFTER the .hero-subtitle animate call, not inside it. The hover() call should be its own standalone block.
  • Check that ".card" in your code matches the class name class="card" on your HTML elements. Class selectors are case-sensitive.
  • Open your browser's developer console (right-click the page, then click Inspect, then Console) and look for any red error messages.

Still stuck? Ask for help.

Explore spring physics values with Claude

Your hover animation uses stiffness: 300 and damping: 20. These two values control how the spring behaves. Changing them gives you completely different animation feels.

  • Switch to Claude and ask it to explain the difference between a "bouncy card lift" and a "snappy card lift" in terms of spring stiffness and damping values.
  • Try changing stiffness to a lower value like 100 or a higher value like 500. Save and hover to see the difference.
  • Try changing damping to a lower value like 8 (more bounce) or a higher value like 30 (less bounce). Save and hover to see the difference.
  • Once you are done experimenting, set the values back to stiffness: 300 and damping: 20 for both the enter and leave animations.

What do stiffness and damping control?

Think of a spring animation like a real spring. Stiffness controls how quickly the spring snaps to its target. Higher stiffness means a faster, more forceful motion.

Damping controls how quickly the spring stops oscillating. Lower damping means more bounce. Higher damping means the element settles quickly without overshooting.

✔️ Awesome, I've got everything!

Great. Make sure you have saved index.html with the final spring values (stiffness: 300, damping: 20) before moving on.

ⓧ I'd like to double check the full code

Here is the full index.html file at this point in the project.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Animated Portfolio</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #0a0a0a;
            color: #ffffff;
            overflow-x: hidden;
        }

        /* Hero Section */
        .hero {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            position: relative;
            overflow: hidden;
        }

        .parallax-bg {
            position: absolute;
            top: -50px;
            left: 0;
            right: 0;
            bottom: -50px;
            background: radial-gradient(circle at 30% 40%, #1a0533 0%, transparent 50%),
                        radial-gradient(circle at 70% 60%, #0a1628 0%, transparent 50%);
            z-index: 0;
        }

        .hero-content {
            position: relative;
            z-index: 1;
            text-align: center;
        }

        .hero-text {
            font-size: 4rem;
            font-weight: 700;
            line-height: 1.2;
            margin-bottom: 1.5rem;
        }

        .hero-text span {
            display: inline-block;
            opacity: 0;
            margin: 0 0.3rem;
        }

        .hero-subtitle {
            font-size: 1.3rem;
            color: #888;
            opacity: 0;
        }

        /* Projects Section */
        .projects {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .projects h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .cards-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 2rem;
            max-width: 900px;
            width: 100%;
        }

        .card {
            background: #1a1a2e;
            border-radius: 16px;
            padding: 2rem;
            border: 1px solid #2a2a4a;
            cursor: pointer;
        }

        .card h3 {
            font-size: 1.3rem;
            margin-bottom: 0.75rem;
            color: #e0e0ff;
        }

        .card p {
            color: #888;
            line-height: 1.6;
        }

        /* Skills Section */
        .skills {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .skills h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .skills-list {
            max-width: 600px;
            width: 100%;
            display: flex;
            flex-direction: column;
            gap: 1.5rem;
        }

        .skill-item {
            display: flex;
            align-items: center;
            gap: 1rem;
            padding: 1.25rem 1.5rem;
            background: #1a1a2e;
            border-radius: 12px;
            border: 1px solid #2a2a4a;
            opacity: 0;
        }

        .skill-icon {
            width: 40px;
            height: 40px;
            border-radius: 8px;
            background: linear-gradient(135deg, #6366f1, #8b5cf6);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1.2rem;
            flex-shrink: 0;
        }

        .skill-name {
            font-size: 1.1rem;
            font-weight: 500;
        }

        /* Footer */
        .footer {
            padding: 4rem 2rem;
            text-align: center;
            color: #666;
        }
    </style>
</head>
<body>
    <!-- Hero Section -->
    <section class="hero">
        <div class="parallax-bg"></div>
        <div class="hero-content">
            <h1 class="hero-text">
                <span>Hi,</span>
                <span>I</span>
                <span>build</span>
                <span>things</span>
                <span>for</span>
                <span>the</span>
                <span>web.</span>
            </h1>
            <p class="hero-subtitle">Frontend developer and animation enthusiast</p>
        </div>
    </section>

    <!-- Projects Section -->
    <section class="projects">
        <h2>Projects</h2>
        <div class="cards-grid">
            <div class="card">
                <h3>Weather Dashboard</h3>
                <p>A real-time weather app with animated transitions between forecasts.</p>
            </div>
            <div class="card">
                <h3>Task Manager</h3>
                <p>A drag-and-drop task board with smooth reordering animations.</p>
            </div>
            <div class="card">
                <h3>Music Visualizer</h3>
                <p>An audio visualizer that responds to music with dynamic motion.</p>
            </div>
        </div>
    </section>

    <!-- Skills Section -->
    <section class="skills">
        <h2>Skills</h2>
        <div class="skills-list">
            <div class="skill-item">
                <div class="skill-icon">JS</div>
                <span class="skill-name">JavaScript</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Re</div>
                <span class="skill-name">React</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Cs</div>
                <span class="skill-name">CSS Animations</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Mo</div>
                <span class="skill-name">Motion</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Ts</div>
                <span class="skill-name">TypeScript</span>
            </div>
        </div>
    </section>

    <!-- Footer -->
    <section class="footer">
        <p>Built with Motion. Animated with joy.</p>
    </section>

    <script type="module">
        import { animate, stagger, inView, scroll, hover } from "https://cdn.jsdelivr.net/npm/motion@12/+esm"

        // Step 2: Staggered hero animation with spring physics
        animate(".hero-text span", { opacity: 1, y: [40, 0] }, {
            delay: stagger(0.1),
            type: "spring",
            stiffness: 100,
            damping: 15
        })

        animate(".hero-subtitle", { opacity: 1, y: [20, 0] }, {
            duration: 0.8,
            delay: 0.6,
            easing: [0.25, 0.1, 0.25, 1]
        })

        // Step 3: Hover gesture animations on project cards
        hover(".card", (element) => {
            animate(element, { scale: 1.05, y: -8 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })

            return () => animate(element, { scale: 1, y: 0 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })
        })
    </script>
</body>
</html>

Your cards now respond to user interaction with satisfying spring physics. Next up, you will make elements reveal themselves as the user scrolls down the page.

Reveal Skills on Scroll with inView

Your project cards now respond to hover with springy lift animations. The next category of animation ties movement to scrolling instead of mouse interaction.

Scroll-triggered animations fire when an element becomes visible in the viewport. They are perfect for revealing content as the user scrolls down a page. In this step, you will use Motion's inView() function to make skill items slide in from the left as you scroll to them.

In this step, get ready to:
  • Use inView() to detect when skill items enter the viewport and animate them in.
  • Add a leave animation so items slide back out when you scroll up.
Animate skill items as they scroll into view

Motion's inView() function watches for elements entering the viewport. When an element becomes visible, it fires a callback where you can run any animation you want.

  • In index.html, find the closing }) of the hover() block (the last line of code before </script>).
  • Add the following code below it:
        // Step 4: Scroll-triggered skill reveal with inView
        inView(".skill-item", (element) => {
            animate(element, { opacity: 1, x: [-100, 0] }, {
                duration: 0.9,
                easing: [0.17, 0.55, 0.55, 1]
            })
        })

What does this code do?

  • inView(".skill-item", ...) watches every element with the class skill-item. When one enters the viewport, the callback fires for that specific element.
  • x: [-100, 0] slides the element from 100 pixels to the left back to its natural position.
  • The easing: [0.17, 0.55, 0.55, 1] is a cubic-bezier curve that starts fast and decelerates smoothly.
  • Save index.html.
  • Refresh your browser and scroll down to the Skills section. Each skill item should slide in from the left as it enters the viewport.

Skills not sliding in?

  • Make sure the new code is placed after the closing }) of the hover() block, not inside it.
  • Check that your .skill-item CSS still has opacity: 0. If the items are already visible, the animation has nothing to reveal.
  • Open the browser console and check for errors. A missing comma or bracket will stop the script.

Why aren't my skill items animating when I scroll down?

Add a leave animation for scrolling back up

Right now, the skill items slide in once and stay visible forever. By returning a function from the inView() callback, you create a leave handler that fires when the element exits the viewport.

  • In index.html, find the inView() block you just added.
  • Find this line:
        inView(".skill-item", (element) => {
            animate(element, { opacity: 1, x: [-100, 0] }, {
                duration: 0.9,
                easing: [0.17, 0.55, 0.55, 1]
            })
        })
  • Replace it with this updated version that includes the leave animation:
        // Step 4: Scroll-triggered skill reveal with inView
        inView(".skill-item", (element) => {
            animate(element, { opacity: 1, x: [-100, 0] }, {
                duration: 0.9,
                easing: [0.17, 0.55, 0.55, 1]
            })

            return () => animate(element, { opacity: 0, x: -100 })
        })

What does the return function do?

return () => animate(element, { opacity: 0, x: -100 }) gives inView() a cleanup function. When the element scrolls out of view, this function runs and slides it back to the left.

Because a leave handler exists, the gesture continues to fire on subsequent entries and exits. Scroll down to see items slide in, scroll back up to watch them slide out, and scroll down again to see them re-enter.

💡 Scroll-triggered vs scroll-linked animations

inView() is a scroll-triggered animation. It fires a callback at a single moment (when the element enters or leaves the viewport). The animation then plays independently of further scrolling.

In the next step, you will use scroll() for scroll-linked animation. That approach ties a value continuously to scroll position, so the effect tracks your scrollbar in real time.

What is the difference between scroll-triggered and scroll-linked animations?

  • Save index.html.
  • Refresh your browser and scroll down to the Skills section. The items should slide in from the left.
  • Scroll back up past the Skills section. The items should slide back out to the left and disappear.

Items slide in but don't slide back out?

  • Make sure you have the return () => line inside the callback function, not outside the inView() call.
  • Check that you replaced the entire previous inView() block and did not accidentally duplicate it.

Help me debug my inView leave animation.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html file up to this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Animated Portfolio</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #0a0a0a;
            color: #ffffff;
            overflow-x: hidden;
        }

        /* Hero Section */
        .hero {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            position: relative;
            overflow: hidden;
        }

        .parallax-bg {
            position: absolute;
            top: -50px;
            left: 0;
            right: 0;
            bottom: -50px;
            background: radial-gradient(circle at 30% 40%, #1a0533 0%, transparent 50%),
                        radial-gradient(circle at 70% 60%, #0a1628 0%, transparent 50%);
            z-index: 0;
        }

        .hero-content {
            position: relative;
            z-index: 1;
            text-align: center;
        }

        .hero-text {
            font-size: 4rem;
            font-weight: 700;
            line-height: 1.2;
            margin-bottom: 1.5rem;
        }

        .hero-text span {
            display: inline-block;
            opacity: 0;
            margin: 0 0.3rem;
        }

        .hero-subtitle {
            font-size: 1.3rem;
            color: #888;
            opacity: 0;
        }

        /* Projects Section */
        .projects {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .projects h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .cards-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 2rem;
            max-width: 900px;
            width: 100%;
        }

        .card {
            background: #1a1a2e;
            border-radius: 16px;
            padding: 2rem;
            border: 1px solid #2a2a4a;
            cursor: pointer;
        }

        .card h3 {
            font-size: 1.3rem;
            margin-bottom: 0.75rem;
            color: #e0e0ff;
        }

        .card p {
            color: #888;
            line-height: 1.6;
        }

        /* Skills Section */
        .skills {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .skills h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .skills-list {
            max-width: 600px;
            width: 100%;
            display: flex;
            flex-direction: column;
            gap: 1.5rem;
        }

        .skill-item {
            display: flex;
            align-items: center;
            gap: 1rem;
            padding: 1.25rem 1.5rem;
            background: #1a1a2e;
            border-radius: 12px;
            border: 1px solid #2a2a4a;
            opacity: 0;
        }

        .skill-icon {
            width: 40px;
            height: 40px;
            border-radius: 8px;
            background: linear-gradient(135deg, #6366f1, #8b5cf6);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1.2rem;
            flex-shrink: 0;
        }

        .skill-name {
            font-size: 1.1rem;
            font-weight: 500;
        }

        /* Footer */
        .footer {
            padding: 4rem 2rem;
            text-align: center;
            color: #666;
        }
    </style>
</head>
<body>
    <!-- Hero Section -->
    <section class="hero">
        <div class="parallax-bg"></div>
        <div class="hero-content">
            <h1 class="hero-text">
                <span>Hi,</span>
                <span>I</span>
                <span>build</span>
                <span>things</span>
                <span>for</span>
                <span>the</span>
                <span>web.</span>
            </h1>
            <p class="hero-subtitle">Frontend developer and animation enthusiast</p>
        </div>
    </section>

    <!-- Projects Section -->
    <section class="projects">
        <h2>Projects</h2>
        <div class="cards-grid">
            <div class="card">
                <h3>Weather Dashboard</h3>
                <p>A real-time weather app with animated transitions between forecasts.</p>
            </div>
            <div class="card">
                <h3>Task Manager</h3>
                <p>A drag-and-drop task board with smooth reordering animations.</p>
            </div>
            <div class="card">
                <h3>Music Visualizer</h3>
                <p>An audio visualizer that responds to music with dynamic motion.</p>
            </div>
        </div>
    </section>

    <!-- Skills Section -->
    <section class="skills">
        <h2>Skills</h2>
        <div class="skills-list">
            <div class="skill-item">
                <div class="skill-icon">JS</div>
                <span class="skill-name">JavaScript</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Re</div>
                <span class="skill-name">React</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Cs</div>
                <span class="skill-name">CSS Animations</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Mo</div>
                <span class="skill-name">Motion</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Ts</div>
                <span class="skill-name">TypeScript</span>
            </div>
        </div>
    </section>

    <!-- Footer -->
    <section class="footer">
        <p>Built with Motion. Animated with joy.</p>
    </section>

    <script type="module">
        import { animate, stagger, inView, scroll, hover } from "https://cdn.jsdelivr.net/npm/motion@12/+esm"

        // Step 2: Staggered hero animation with spring physics
        animate(".hero-text span", { opacity: 1, y: [40, 0] }, {
            delay: stagger(0.1),
            type: "spring",
            stiffness: 100,
            damping: 15
        })

        animate(".hero-subtitle", { opacity: 1, y: [20, 0] }, {
            duration: 0.8,
            delay: 0.6,
            easing: [0.25, 0.1, 0.25, 1]
        })

        // Step 3: Hover gesture animations on project cards
        hover(".card", (element) => {
            animate(element, { scale: 1.05, y: -8 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })

            return () => animate(element, { scale: 1, y: 0 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })
        })

        // Step 4: Scroll-triggered skill reveal with inView
        inView(".skill-item", (element) => {
            animate(element, { opacity: 1, x: [-100, 0] }, {
                duration: 0.9,
                easing: [0.17, 0.55, 0.55, 1]
            })

            return () => animate(element, { opacity: 0, x: -100 })
        })
    </script>
</body>
</html>

Your skills section now reveals itself on scroll and hides again when you scroll away. Next up, you will create a scroll-linked parallax effect that ties movement directly to scroll position.

Create a Scroll-Linked Parallax Effect

Your skill items now slide in and out of view as the user scrolls. That animation fires once when an element crosses the viewport boundary.

In this step, you will create a different category of animation: scroll-linked motion. Instead of firing at a threshold, scroll-linked animations continuously track the user's scroll position. The further down the page you scroll, the further along the animation progresses.

In this step, get ready to:
  • Create a parallax background that moves at a different speed than the foreground.
  • Fade the hero section out as the user scrolls past it.
Add a parallax background with scroll()

A parallax effect makes background elements move slower than foreground elements as you scroll. This creates an illusion of depth on a flat page.

Motion's scroll() function accepts a callback that receives a progress value between 0 and 1. You can use that value to move an element by any amount you choose.

  • In index.html, scroll down to the <script type="module"> block.
  • Find the inView() block that ends with return () => animate(element, { opacity: 0, x: -100 }).
  • Add the following code below the closing }) of that inView() call:
// Step 5: Scroll-linked parallax effect
scroll((progress) => {
    document.querySelector(".parallax-bg").style.transform =
        "translateY(" + (progress * 200) + "px)"
})

What does this code do?

The scroll() function fires its callback on every scroll frame. The progress parameter goes from 0 (top of page) to 1 (bottom of page).

Multiplying progress by 200 means the background shifts down by up to 200 pixels as you scroll the full page. Because the foreground scrolls at full speed while the background drifts slowly, you get a parallax depth effect.

  • Save index.html and refresh your browser.
  • Scroll down slowly. The dark gradient background behind the hero text should drift downward at a noticeably slower rate than the rest of the page content.

Don't see the parallax movement?

Make sure you placed the code after the closing }) of the inView() block, not inside it.

Check that your selector string is ".parallax-bg" with quotes and a leading dot. A missing dot means no element is found and no transform is applied.

The effect is subtle on short pages. Scroll all the way down to see the full 200px shift.

Help me debug my scroll-linked parallax animation.

Fade the hero section as the user scrolls past it

The parallax uses a callback pattern where you manually set styles on each frame. Motion also supports a second pattern: passing an animate() call directly into scroll(). This lets the browser use its native ScrollTimeline for hardware-accelerated performance.

  • Add this code directly below the parallax scroll() block you just wrote:
scroll(animate(".hero", { opacity: [1, 0] }), {
    target: document.querySelector(".hero"),
    offset: ["start start", "end start"]
})

What does this code do?

Instead of a callback, you pass an animate() call as the first argument. Motion binds the animation's progress directly to scroll position.

  • target tells scroll() which element's position to track (the hero section).
  • offset: ["start start", "end start"] defines when the animation starts and ends. The first value means "target's top meets viewport's top." The second means "target's bottom meets viewport's top."
  • opacity: [1, 0] fades the hero from fully visible to fully transparent as you scroll through that range.

💡 Scroll-linked vs scroll-triggered

You have now used both approaches. inView() is scroll-triggered: it fires once when an element crosses a visibility threshold. The animation plays at its own pace regardless of how fast you scroll.

scroll() is scroll-linked: the animation value is directly tied to the scroll position. Scroll halfway and the animation is exactly at its midpoint. Stop scrolling and the animation freezes.

Ask Claude to explain when to reach for each one.

  • Save index.html and refresh your browser.
  • Scroll down slowly. The hero section should fade out smoothly as its bottom edge approaches the top of your viewport.

Hero not fading out?

Check that you are passing document.querySelector(".hero") to the target option. Without a target, scroll tracks the entire document and the offset values won't match the hero's position.

Make sure the offset array uses double quotes around both strings: ["start start", "end start"].

Help me debug my scroll-linked hero fade animation.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is the complete index.html file with all five steps included:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Animated Portfolio</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #0a0a0a;
            color: #ffffff;
            overflow-x: hidden;
        }

        /* Hero Section */
        .hero {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            position: relative;
            overflow: hidden;
        }

        .parallax-bg {
            position: absolute;
            top: -50px;
            left: 0;
            right: 0;
            bottom: -50px;
            background: radial-gradient(circle at 30% 40%, #1a0533 0%, transparent 50%),
                        radial-gradient(circle at 70% 60%, #0a1628 0%, transparent 50%);
            z-index: 0;
        }

        .hero-content {
            position: relative;
            z-index: 1;
            text-align: center;
        }

        .hero-text {
            font-size: 4rem;
            font-weight: 700;
            line-height: 1.2;
            margin-bottom: 1.5rem;
        }

        .hero-text span {
            display: inline-block;
            opacity: 0;
            margin: 0 0.3rem;
        }

        .hero-subtitle {
            font-size: 1.3rem;
            color: #888;
            opacity: 0;
        }

        /* Projects Section */
        .projects {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .projects h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .cards-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: 2rem;
            max-width: 900px;
            width: 100%;
        }

        .card {
            background: #1a1a2e;
            border-radius: 16px;
            padding: 2rem;
            border: 1px solid #2a2a4a;
            cursor: pointer;
        }

        .card h3 {
            font-size: 1.3rem;
            margin-bottom: 0.75rem;
            color: #e0e0ff;
        }

        .card p {
            color: #888;
            line-height: 1.6;
        }

        /* Skills Section */
        .skills {
            min-height: 100vh;
            padding: 6rem 2rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .skills h2 {
            font-size: 2.5rem;
            margin-bottom: 3rem;
        }

        .skills-list {
            max-width: 600px;
            width: 100%;
            display: flex;
            flex-direction: column;
            gap: 1.5rem;
        }

        .skill-item {
            display: flex;
            align-items: center;
            gap: 1rem;
            padding: 1.25rem 1.5rem;
            background: #1a1a2e;
            border-radius: 12px;
            border: 1px solid #2a2a4a;
            opacity: 0;
        }

        .skill-icon {
            width: 40px;
            height: 40px;
            border-radius: 8px;
            background: linear-gradient(135deg, #6366f1, #8b5cf6);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1.2rem;
            flex-shrink: 0;
        }

        .skill-name {
            font-size: 1.1rem;
            font-weight: 500;
        }

        /* Footer */
        .footer {
            padding: 4rem 2rem;
            text-align: center;
            color: #666;
        }
    </style>
</head>
<body>
    <!-- Hero Section -->
    <section class="hero">
        <div class="parallax-bg"></div>
        <div class="hero-content">
            <h1 class="hero-text">
                <span>Hi,</span>
                <span>I</span>
                <span>build</span>
                <span>things</span>
                <span>for</span>
                <span>the</span>
                <span>web.</span>
            </h1>
            <p class="hero-subtitle">Frontend developer and animation enthusiast</p>
        </div>
    </section>

    <!-- Projects Section -->
    <section class="projects">
        <h2>Projects</h2>
        <div class="cards-grid">
            <div class="card">
                <h3>Weather Dashboard</h3>
                <p>A real-time weather app with animated transitions between forecasts.</p>
            </div>
            <div class="card">
                <h3>Task Manager</h3>
                <p>A drag-and-drop task board with smooth reordering animations.</p>
            </div>
            <div class="card">
                <h3>Music Visualizer</h3>
                <p>An audio visualizer that responds to music with dynamic motion.</p>
            </div>
        </div>
    </section>

    <!-- Skills Section -->
    <section class="skills">
        <h2>Skills</h2>
        <div class="skills-list">
            <div class="skill-item">
                <div class="skill-icon">JS</div>
                <span class="skill-name">JavaScript</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Re</div>
                <span class="skill-name">React</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Cs</div>
                <span class="skill-name">CSS Animations</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Mo</div>
                <span class="skill-name">Motion</span>
            </div>
            <div class="skill-item">
                <div class="skill-icon">Ts</div>
                <span class="skill-name">TypeScript</span>
            </div>
        </div>
    </section>

    <!-- Footer -->
    <section class="footer">
        <p>Built with Motion. Animated with joy.</p>
    </section>

    <script type="module">
        import { animate, stagger, inView, scroll, hover } from "https://cdn.jsdelivr.net/npm/motion@12/+esm"

        // Step 2: Staggered hero animation with spring physics
        animate(".hero-text span", { opacity: 1, y: [40, 0] }, {
            delay: stagger(0.1),
            type: "spring",
            stiffness: 100,
            damping: 15
        })

        animate(".hero-subtitle", { opacity: 1, y: [20, 0] }, {
            duration: 0.8,
            delay: 0.6,
            easing: [0.25, 0.1, 0.25, 1]
        })

        // Step 3: Hover gesture animations on project cards
        hover(".card", (element) => {
            animate(element, { scale: 1.05, y: -8 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })

            return () => animate(element, { scale: 1, y: 0 }, {
                type: "spring",
                stiffness: 300,
                damping: 20
            })
        })

        // Step 4: Scroll-triggered skill reveal with inView
        inView(".skill-item", (element) => {
            animate(element, { opacity: 1, x: [-100, 0] }, {
                duration: 0.9,
                easing: [0.17, 0.55, 0.55, 1]
            })

            return () => animate(element, { opacity: 0, x: -100 })
        })

        // Step 5: Scroll-linked parallax effect
        scroll((progress) => {
            document.querySelector(".parallax-bg").style.transform =
                "translateY(" + (progress * 200) + "px)"
        })

        scroll(animate(".hero", { opacity: [1, 0] }), {
            target: document.querySelector(".hero"),
            offset: ["start start", "end start"]
        })
    </script>
</body>
</html>

Secret mission

Add a Scroll Progress Bar

Your landing page already has scroll-linked parallax and scroll-triggered reveals. But what about giving the user a visual indicator of how far they've scrolled? In this bonus challenge, you'll add a fixed progress bar at the top of the page that fills from left to right as the user scrolls down.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your file for further experimentation or delete it entirely.

Resources you used:

  • One local file: index.html on your Desktop.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your animated landing page.

  • Your index.html file stays on your Desktop, ready to open in any browser.
  • Try tweaking the spring physics values, adding new sections, or deploying your page to a free static host like GitHub Pages, Netlify, or Vercel.

Pause - I'll come back to this later

Since this is a single local file with no running processes, there is nothing to pause.

  • Your index.html file will stay on your Desktop until you decide to return to it or delete it.
  • No servers, no background processes, no costs accumulating.

Delete - I don't want to use this again

Remove all project files and start fresh.

  • Delete the index.html file from your Desktop by running:

macOS

rm ~/Desktop/index.html

Windows

Remove-Item ~\Desktop\index.html
  • Confirm the file is gone by checking your Desktop. You should no longer see index.html listed.

That's a Wrap!

That's a Wrap!

Nice work. You just built a polished, multi-section animated landing page using Motion's vanilla JavaScript API. No frameworks, no build tools, no dependencies beyond a single CDN import.

You've learned how to:

  • Use animate() with spring physics and stagger() to create natural, cascading animations with sequential timing.
  • Use hover() and inView() to add gesture-driven and scroll-triggered animations that respond to user interaction.
  • Use scroll() to create scroll-linked parallax and fade effects tied directly to scroll position.
  • Secret Mission: Added a hardware-accelerated scroll progress bar using scroll() with the animate() pattern for optimal performance.

Ready to quiz yourself?