Build an Interactive Landing Page

Integrate Rive animations into a landing page with state machine controls.

Introduction

30 Second Summary

The best websites feel alive. Buttons react to your cursor, backgrounds shift and flow, and every interaction feels intentional. Those aren't just videos or GIFs playing on a loop. They're real-time interactive animations built with specialized tools.

In this project, you will build an interactive landing page powered by Rive animations. You'll load animation files, make them respond to hover and click automatically, and control playback with JavaScript.

What You'll Build

A modern landing page for a fictional product featuring a full-width animated hero section and an interactive call-to-action button, both powered by the Rive web runtime.

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

  • A responsive landing page with a dark theme, fixed navigation, and full-viewport hero section.
  • A Rive-powered animated hero that plays automatically on load and stays crisp at any screen size.
  • An interactive animated button driven by a state machine that responds to hover and click with no extra JavaScript needed.
  • Secret Mission: A scroll-triggered animation section that plays only when the user scrolls it into view.

Are there any prerequisites?

You should be comfortable with HTML, CSS, and JavaScript fundamentals. No prior experience with Rive or animation tools is needed.

Set Up Your Project

Before you can bring animations to life on a webpage, you need a clean project structure and the right assets in place. Rive animations live inside .riv files that you load at runtime using a JavaScript library.

In this step, you'll create your project folder, download two community animation files from Rive, and wire up the HTML boilerplate that loads the Rive runtime.

In this step, get ready to:
  • Create the project folder and base files.
  • Download two animation files from the Rive community.
  • Set up the HTML5 boilerplate with the Rive runtime script.
Create your project folder and files

Your project needs three code files and one folder for animation assets. Let's create them all from the terminal.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir rive-landing-page
cd rive-landing-page
  • Create the project files and the assets folder by running these commands:
touch index.html styles.css app.js
mkdir assets
  • Confirm the files exist by running:
ls

You should see app.js, assets, index.html, and styles.css in the output.

Don't see all four items?

Make sure you ran cd rive-landing-page before running touch and mkdir. If you created the files in the wrong folder, delete them and start again from the cd ~/Desktop step.

help me debug my project folder setup

Now open the project in VS Code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the rive-landing-page folder, then click Open.

You should see all four items (app.js, assets, index.html, styles.css) listed in the Explorer sidebar on the left.

Download the Rive animation files

Rive has a community library where designers share free, ready-to-use animation files. You'll download two of these .riv files to use as your hero background animation and your interactive button.

  • Open your browser and go to rive.app.
  • Log in to your free Rive account.
  • Click Community in the top navigation bar.
  • In the search bar, type Hero Animation Use Case and press Enter.
  • Find the result by JcToon and click on it to open the file page.
  • Click the Download button to download the .riv file.
  • Go back to the Community section.
  • Search for Like Button and press Enter.
  • Find the result by guidorosso and click on it.
  • Click Download to download the second .riv file.

Now move both downloaded files into your project's assets folder.

  • Open Finder (macOS) or File Explorer (Windows) and navigate to your Downloads folder.
  • Rename the hero animation file to hero_animation.riv.
  • Rename the like button file to like_button.riv.
  • Drag both files into the assets folder inside rive-landing-page on your Desktop.

Back in VS Code, expand the assets folder in the Explorer sidebar. You should see hero_animation.riv and like_button.riv listed inside.

What are .riv files?

A .riv file is Rive's animation format. It contains the artwork, animations, and state machine logic in a single compact file that the Rive runtime can load and play in the browser.

All community files on Rive are CC BY licensed, meaning you can use them freely with attribution.

Can't find the community files?

Make sure you're logged in to your Rive account. Community downloads require authentication. If the search returns no results, try browsing the Community page manually and filtering by name.

help me find and download community files from Rive

Add the HTML5 boilerplate with the Rive runtime

The Rive web runtime is a JavaScript library that knows how to read .riv files and render them onto HTML canvas elements. You'll load it from a CDN so there's no build tooling required.

  • In VS Code, click on index.html in the Explorer sidebar to open it.
  • Copy the following HTML into the file:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Rive Landing Page</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>

    <script src="https://unpkg.com/@rive-app/webgl2@2.37.5"></script>
    <script src="app.js"></script>
  </body>
</html>

What does this code do?

  • The <link> tag connects your stylesheet so CSS styles apply to the page.
  • The first <script> tag loads the Rive runtime (version 2.37.5) from a CDN. This makes a global rive object available in your JavaScript.
  • The second <script> tag loads your own app.js file. It comes after the Rive runtime so that your code can use the rive object.

Why WebGL2 instead of canvas?

Rive offers two renderer packages: @rive-app/webgl2 and @rive-app/canvas. The WebGL2 package uses GPU acceleration and supports all Rive features including text, layouts, and audio.

The canvas package is primarily useful when rendering many animations simultaneously to avoid WebGL context limits. For a landing page with a couple of animations, WebGL2 is the better fit.

  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the file in your browser by right-clicking index.html in the Explorer sidebar and selecting Reveal in Finder (macOS) or Reveal in File Explorer (Windows), then double-click the file.

You should see a blank white page with the title "Rive Landing Page" in the browser tab. The page is empty because you haven't added any visible content yet.

Seeing an error in the browser console?

Open DevTools with F12 and check the Console tab. If you see a 404 error for the Rive script, make sure the URL matches exactly: https://unpkg.com/@rive-app/webgl2@2.37.5. A typo in the version number or package name will prevent the library from loading.

help me fix a script loading error in my HTML file

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html and that both .riv files are inside the assets folder.

ⓧ I'd like to double check the full code

Here's the full index.html file at this point. Your styles.css and app.js files should still be empty.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Rive Landing Page</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>

    <script src="https://unpkg.com/@rive-app/webgl2@2.37.5"></script>
    <script src="app.js"></script>
  </body>
</html>

Your project structure is ready with all the files and assets in place. Next up, you'll build the landing page layout with a full-screen hero section and an interactive button area.

Build the Landing Page Layout

Your project files are ready and your Rive animation assets are in place. Now it's time to build the visual foundation of your landing page.

Every interactive web experience starts with solid structure and styling. You'll create the HTML skeleton that positions two canvas elements for your Rive animations, then layer on CSS to create a polished dark-themed layout.

In this step, get ready to:
  • Create the HTML page structure with a hero section and CTA section.
  • Style the page with a dark theme and full-viewport hero layout.
  • Add responsive styles for different screen sizes.
Create the HTML page structure

Your landing page needs three main sections. A fixed navigation bar at the top, a full-screen hero section with a canvas for the Rive animation, and a call-to-action section with another canvas for the interactive button.

  • In index.html, find the empty space between the opening <body> tag and the first <script> tag.
  • Add the following page structure in that space:
    <!-- Navigation bar -->
    <header>
      <nav class="navbar">
        <div class="logo">RiveLand</div>
        <ul class="nav-links">
          <li><a href="#hero">Home</a></li>
          <li><a href="#cta">Try It</a></li>
        </ul>
      </nav>
    </header>

    <!-- Hero section with Rive animation canvas -->
    <section id="hero">
      <canvas id="hero-canvas"></canvas>
      <div class="hero-overlay">
        <h1>Interactive Experiences, Powered by Rive</h1>
        <p>Animations that respond to your every move.</p>
        <a href="#cta" class="hero-cta" id="scroll-to-cta">See it in action</a>
      </div>
    </section>

    <!-- CTA section with interactive button -->
    <section id="cta">
      <h2>Interactive Button</h2>
      <p>Hover and click the animation below. It reacts to your pointer automatically.</p>
      <canvas id="button-canvas"></canvas>
      <div class="controls">
        <button id="btn-play">Play</button>
        <button id="btn-pause">Pause</button>
        <button id="btn-stop">Stop</button>
      </div>
    </section>

What does this code do?

The header contains a fixed navigation bar with the site logo and anchor links that jump to each section of the page.

The #hero section has two layers. A canvas element sits behind everything (for the Rive animation) and a .hero-overlay div sits on top with the headline, subtitle, and a call-to-action link.

The #cta section holds a second canvas for the interactive button animation, plus three playback control buttons you'll wire up in a later step.

  • Save index.html.
  • Open index.html in your browser by right-clicking the file in the VS Code Explorer sidebar and selecting Reveal in Finder (macOS) or Reveal in File Explorer (Windows), then double-clicking the file.
  • You should see unstyled text content. The heading, paragraphs, links, and buttons appear in your browser's default styling against a white background.

Seeing a blank page?

  • Make sure you pasted the HTML between the opening body tag and the first script tag, not after the scripts.
  • Check that you saved the file after pasting.
  • Confirm that all tags are properly closed. A missing </section> or </header> can cause elements to disappear.

Help me figure out why my HTML page is blank.

✔️ Awesome, I've got everything!

Great. Make sure you've saved your file before moving on to the CSS.

ⓧ I'd like to double check the full code

Here's the complete index.html file 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>Rive Landing Page</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <header>
      <nav class="navbar">
        <div class="logo">RiveLand</div>
        <ul class="nav-links">
          <li><a href="#hero">Home</a></li>
          <li><a href="#cta">Try It</a></li>
        </ul>
      </nav>
    </header>

    <section id="hero">
      <canvas id="hero-canvas"></canvas>
      <div class="hero-overlay">
        <h1>Interactive Experiences, Powered by Rive</h1>
        <p>Animations that respond to your every move.</p>
        <a href="#cta" class="hero-cta" id="scroll-to-cta">See it in action</a>
      </div>
    </section>

    <section id="cta">
      <h2>Interactive Button</h2>
      <p>Hover and click the animation below. It reacts to your pointer automatically.</p>
      <canvas id="button-canvas"></canvas>
      <div class="controls">
        <button id="btn-play">Play</button>
        <button id="btn-pause">Pause</button>
        <button id="btn-stop">Stop</button>
      </div>
    </section>

    <script src="https://unpkg.com/@rive-app/webgl2@2.37.5"></script>
    <script src="app.js"></script>
  </body>
</html>
Style the page with a dark theme

The HTML structure is in place, but right now it looks like plain browser defaults. You'll build up the stylesheet in three passes: base styles and navbar, hero section, then the CTA section.

  • Open styles.css and add the global reset, body styles, and navbar styles:
/* Reset default browser spacing */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Dark theme base */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background-color: #0a0a0f;
  color: #ffffff;
}

/* Fixed navbar with frosted glass effect */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: rgba(10, 10, 15, 0.8);
  backdrop-filter: blur(10px);
}

.logo {
  font-size: 1.5rem;
  font-weight: bold;
  color: #7c3aed;
}

.nav-links {
  list-style: none;
  display: flex;
  gap: 2rem;
}

.nav-links a {
  color: #ffffff;
  text-decoration: none;
  opacity: 0.8;
  transition: opacity 0.2s;
}

.nav-links a:hover {
  opacity: 1;
}

What does this code do?

  • The * reset removes all default margins and padding so you have full control over spacing.
  • The navbar uses position: fixed so it stays pinned to the top while the user scrolls.
  • The backdrop-filter: blur(10px) creates a frosted glass effect, letting background content show through slightly.
  • Save styles.css and refresh your browser.
  • You should see a dark background with white text. The navbar should be visible at the top with "RiveLand" in purple on the left and two links on the right.

Still seeing a white background?

  • Confirm that styles.css is saved and that your HTML file has <link rel="stylesheet" href="styles.css" /> in the <head>.
  • Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear any cached styles.

Help me debug why my CSS isn't loading.

  • Next, add the hero section styles below the navbar styles in styles.css:
/* Hero section takes full viewport height */
#hero {
  position: relative;
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* Canvas fills the hero and starts invisible */
#hero-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.5s ease-in;
}

/* JavaScript adds this class after the animation loads */
#hero-canvas.loaded {
  opacity: 1;
}

/* Overlay text sits above the canvas */
.hero-overlay {
  position: relative;
  z-index: 10;
  text-align: center;
  padding: 2rem;
}

/* Gradient text for the headline */
.hero-overlay h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
  background: linear-gradient(135deg, #7c3aed, #a78bfa);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.hero-overlay p {
  font-size: 1.25rem;
  opacity: 0.8;
  margin-bottom: 2rem;
}

/* CTA button in the hero overlay */
.hero-cta {
  display: inline-block;
  padding: 0.75rem 2rem;
  background: #7c3aed;
  color: #ffffff;
  text-decoration: none;
  border-radius: 8px;
  font-weight: 600;
  transition: background 0.2s;
}

.hero-cta:hover {
  background: #6d28d9;
}

What does this code do?

  • The hero section uses height: 100vh to fill the entire browser viewport, creating a dramatic full-screen entry point.
  • The canvas starts with opacity: 0 and fades in when JavaScript adds the .loaded class. This prevents a flash of empty canvas before the animation loads.
  • The overlay uses z-index: 10 to guarantee the text always sits above the canvas animation.
  • The gradient headline uses background-clip: text to fill the letters with a purple gradient instead of a solid color.
  • Save styles.css and refresh your browser.
  • The hero section now fills your entire screen. You should see a large purple gradient headline that reads "Interactive Experiences, Powered by Rive" centered on the page, with subtitle text and a purple "See it in action" button below it.

Headline text not showing the purple gradient?

  • Make sure you have both -webkit-background-clip: text and background-clip: text on the .hero-overlay h1 rule. Chrome needs the prefixed version.
  • Check that -webkit-text-fill-color: transparent is present. Without it, the solid text color hides the gradient.

Help me fix my CSS gradient text.

  • Now add the CTA section and controls styles below the hero styles in styles.css:
/* CTA section layout */
#cta {
  padding: 4rem 2rem;
  text-align: center;
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}

#cta h2 {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

#cta p {
  opacity: 0.7;
  max-width: 500px;
}

/* Canvas for the interactive button animation */
#button-canvas {
  width: 200px;
  height: 200px;
  cursor: pointer;
}

/* Playback control buttons */
.controls {
  display: flex;
  gap: 1rem;
  margin-top: 1rem;
}

.controls button {
  padding: 0.5rem 1.5rem;
  background: #1e1e2e;
  color: #ffffff;
  border: 1px solid #333;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: background 0.2s;
}

.controls button:hover {
  background: #2e2e3e;
}

What does this code do?

  • The CTA section uses flexbox with flex-direction: column to stack its children vertically and center them on the page.
  • The button canvas is set to 200x200 pixels. This gives the Rive animation a fixed space to render into.
  • The control buttons use a subtle dark background with a light border, matching the overall dark theme.
  • Save styles.css and refresh your browser.
  • Scroll down past the hero section. You should see the "Interactive Button" heading centered on the page, with three dark control buttons (Play, Pause, Stop) arranged in a row.

CTA section not visible?

  • The hero section fills the full viewport. Scroll down to see the CTA section below it.
  • If the control buttons don't appear, check that your HTML has the <div class="controls"> wrapper around the three button elements.

Help me figure out why my CTA section doesn't look right.

Add responsive styles

Your page looks great at full width, but the large headline and button canvas won't scale well on smaller screens. A media query adjusts sizes when the viewport shrinks below a breakpoint.

  • Add the following responsive styles at the bottom of styles.css:
/* Responsive adjustments for smaller screens */
@media (max-width: 768px) {
  .hero-overlay h1 {
    font-size: 2rem;
  }

  .hero-overlay p {
    font-size: 1rem;
  }

  #button-canvas {
    width: 150px;
    height: 150px;
  }
}

What does this code do?

The @media (max-width: 768px) block only applies when the browser width is 768 pixels or less (a common tablet/phone breakpoint). It shrinks the headline from 3rem to 2rem and reduces the button canvas from 200px to 150px so they fit comfortably on narrow screens.

  • Save styles.css and refresh your browser.
  • Resize your browser window to be narrower than 768px. The headline text should shrink and the button area should get smaller.

Use DevTools to test responsiveness

Press Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows) to open DevTools. Click the device toggle icon (a phone and tablet icon) to preview your page at different screen sizes without manually resizing the window.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here's the complete styles.css file:

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

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

.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: rgba(10, 10, 15, 0.8);
  backdrop-filter: blur(10px);
}

.logo {
  font-size: 1.5rem;
  font-weight: bold;
  color: #7c3aed;
}

.nav-links {
  list-style: none;
  display: flex;
  gap: 2rem;
}

.nav-links a {
  color: #ffffff;
  text-decoration: none;
  opacity: 0.8;
  transition: opacity 0.2s;
}

.nav-links a:hover {
  opacity: 1;
}

#hero {
  position: relative;
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

#hero-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.5s ease-in;
}

#hero-canvas.loaded {
  opacity: 1;
}

.hero-overlay {
  position: relative;
  z-index: 10;
  text-align: center;
  padding: 2rem;
}

.hero-overlay h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
  background: linear-gradient(135deg, #7c3aed, #a78bfa);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.hero-overlay p {
  font-size: 1.25rem;
  opacity: 0.8;
  margin-bottom: 2rem;
}

.hero-cta {
  display: inline-block;
  padding: 0.75rem 2rem;
  background: #7c3aed;
  color: #ffffff;
  text-decoration: none;
  border-radius: 8px;
  font-weight: 600;
  transition: background 0.2s;
}

.hero-cta:hover {
  background: #6d28d9;
}

#cta {
  padding: 4rem 2rem;
  text-align: center;
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}

#cta h2 {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

#cta p {
  opacity: 0.7;
  max-width: 500px;
}

#button-canvas {
  width: 200px;
  height: 200px;
  cursor: pointer;
}

.controls {
  display: flex;
  gap: 1rem;
  margin-top: 1rem;
}

.controls button {
  padding: 0.5rem 1.5rem;
  background: #1e1e2e;
  color: #ffffff;
  border: 1px solid #333;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: background 0.2s;
}

.controls button:hover {
  background: #2e2e3e;
}

@media (max-width: 768px) {
  .hero-overlay h1 {
    font-size: 2rem;
  }

  .hero-overlay p {
    font-size: 1rem;
  }

  #button-canvas {
    width: 150px;
    height: 150px;
  }
}

Your landing page layout is complete. You have a polished dark-themed page with a full-screen hero section and a CTA section ready to host Rive animations. Next up, you'll load your first Rive animation file and bring the hero section to life.

Load and Display the Hero Animation

Your landing page layout is built and styled, but right now the hero section is just an empty dark space. Time to bring it to life with animation.

The Rive web runtime loads .riv files and renders them onto a canvas element. In this step, you'll create a Rive instance that loads your hero animation, plays it automatically, and resizes cleanly on any screen.

In this step, get ready to:
  • Create a Rive instance that loads and plays the hero animation with a state machine.
  • Add responsive resizing so the animation stays crisp at any viewport size.
  • Test the animation in your browser and confirm the fade-in effect works.
Create the hero Rive instance

A Rive instance is the main object that connects a .riv file to a canvas element on your page. You configure it with the file path, the target canvas, and a state machine name that tells Rive which interactive logic to run.

  • Open app.js in VS Code and add the following code:
// Hero Animation
const heroCanvas = document.getElementById("hero-canvas");

const heroRive = new rive.Rive({
  src: "assets/hero_animation.riv",
  canvas: heroCanvas,
  autoplay: true,
  stateMachines: "State Machine 1",
  onLoad: () => {
    heroRive.resizeDrawingSurfaceToCanvas();
    heroCanvas.classList.add("loaded");
  },
  onStateChange: (event) => {
    console.log("Hero state changed:", event.data);
  },
});

What does this code do?

heroCanvas grabs a reference to the <canvas> element in your HTML where the animation will render.

new rive.Rive({...}) creates the Rive instance. The src points to your downloaded file, autoplay: true starts the animation immediately, and stateMachines tells Rive which interactive logic layer to activate.

resizeDrawingSurfaceToCanvas() matches the canvas drawing buffer to the element's displayed size and device pixel ratio. Without it, the animation may appear blurry on high-DPI screens.

classList.add("loaded") triggers the CSS fade-in transition you set up earlier, preventing a flash of empty canvas. The onStateChange callback logs state machine transitions to the console so you can observe the animation reacting in real time.

Verify your state machine name

The state machine name must match the name inside the .riv file exactly. Most community files use "State Machine 1" as the default name. If your animation does not play, open the file in the Rive editor to check the actual name.

  • Save app.js.

Don't open the browser just yet. The resize listener in the next chunk will ensure the animation renders at the correct size from the start.

Add responsive canvas resizing

The onLoad callback handles the initial sizing, but what happens when a user resizes their browser window? The canvas needs to recalculate its drawing buffer every time the viewport dimensions change.

  • Add this code below the Rive instance in app.js:
// Resize hero canvas on window resize
window.addEventListener("resize", () => {
  heroRive.resizeDrawingSurfaceToCanvas();
});

What does this code do?

Every time the browser window is resized, this listener calls resizeDrawingSurfaceToCanvas() again. This keeps the animation pixel-perfect at any viewport width, including when users rotate a tablet or snap a window to half-screen.

  • Save app.js.
  • Open index.html in your browser (right-click the file in VS Code's Explorer and select Open with Live Server, or double-click the file in your file manager to open it directly).

You should see the hero animation fade in smoothly behind your headline text. The animation plays automatically and fills the full viewport height.

  • Open your browser's DevTools console (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows).
  • Move your cursor over the hero animation. You should see "Hero state changed:" messages appearing in the console as the state machine responds to your pointer.

Animation not showing up?

  • Check the console for errors. If you see a 404, confirm that assets/hero_animation.riv exists and the filename matches exactly (including case).
  • If you see "stateMachine not found", the state machine name in your code does not match the one inside the .riv file. Open the file in the Rive editor at rive.app to check the correct name.
  • If the canvas loads but looks blurry or tiny, make sure resizeDrawingSurfaceToCanvas() is being called inside onLoad. Without it, the drawing buffer does not match the canvas element size.
  • Still stuck? Help me debug why my Rive hero animation is not rendering on the canvas.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.js and your animation is playing smoothly in the hero section.

ⓧ I'd like to double check the full code

Here is the complete app.js file at this point. Make sure yours matches exactly.

// Hero Animation
const heroCanvas = document.getElementById("hero-canvas");

const heroRive = new rive.Rive({
  src: "assets/hero_animation.riv",
  canvas: heroCanvas,
  autoplay: true,
  stateMachines: "State Machine 1",
  onLoad: () => {
    heroRive.resizeDrawingSurfaceToCanvas();
    heroCanvas.classList.add("loaded");
  },
  onStateChange: (event) => {
    console.log("Hero state changed:", event.data);
  },
});

// Resize hero canvas on window resize
window.addEventListener("resize", () => {
  heroRive.resizeDrawingSurfaceToCanvas();
});

Your hero section is alive with motion. Next up, you'll add the interactive button animation that responds to hover and click.

Add the Interactive Button Animation

Your hero animation is alive and playing in the background. Now it's time to add the second Rive animation: an interactive button that reacts to hover and click without any extra JavaScript code from you.

The Like Button animation uses Rive's state machine Listeners to respond to pointer events automatically. You'll load it into the page the same way you loaded the hero animation, then wire up Play, Pause, and Stop buttons to control its playback programmatically.

In this step, get ready to:
  • Load the Like Button .riv file as a second Rive instance.
  • Verify the state machine responds to hover and click automatically.
  • Wire up Play, Pause, and Stop buttons for programmatic playback control.
Load the Like Button animation

You already have the like_button.riv file in your assets folder and a canvas#button-canvas element in your HTML. The next step is creating a Rive instance in JavaScript that connects the two.

  • In app.js, add the following code below the window resize event listener:
// Button Animation
const buttonCanvas = document.getElementById("button-canvas");

const buttonRive = new rive.Rive({
  src: "assets/like_button.riv",
  canvas: buttonCanvas,
  autoplay: true,
  stateMachines: "State Machine 1",
  onLoad: () => {
    buttonRive.resizeDrawingSurfaceToCanvas();
  },
});

What does this code do?

This follows the exact same pattern as your hero animation. You grab the canvas element, then create a new rive.Rive instance pointing to the Like Button file.

  • src tells Rive where to find the animation file.
  • autoplay: true starts the state machine immediately after loading.
  • stateMachines specifies which state machine to run (this file uses "State Machine 1").
  • resizeDrawingSurfaceToCanvas() matches the drawing buffer to the canvas size so the animation looks crisp on all screens.

State machine name mismatch

Community .riv files typically name their default state machine "State Machine 1", but the name can vary per file. If your animation loads but does not respond to interaction, open the file in the Rive editor to check the exact state machine name.

  • Save app.js.
  • Refresh your browser and scroll down past the hero section. You should see the Like Button animation rendering inside the CTA section.

Don't see the button animation?

  • Open DevTools (F12) and check the Console for errors. A 404 on like_button.riv means the file path is wrong or the file is not in your assets folder.
  • Make sure the file is named exactly like_button.riv (no spaces, lowercase). The name must match what you wrote in src.
  • If you see a blank canvas, the state machine name might not match. Double-check that the file uses "State Machine 1" by previewing it in the Rive editor.

Help me debug why my Like Button .riv file isn't loading in the browser.

Test the interactive pointer events

The Like Button file has Listeners defined inside its state machine. These are pointer event handlers (hover, click) that the animation designer set up in the Rive editor.

Why does the button react without extra JavaScript?

When a state machine has Listeners defined, the Rive runtime automatically forwards pointer events from the canvas to the state machine. Your browser sends hover and click events to the canvas element, and the Rive runtime passes them along to the animation.

This means interactivity is baked into the animation file itself. No additional JavaScript wiring is needed for hover and click to work.

  • Hover your cursor over the Like Button canvas. You should see the animation react with a hover state change.
  • Click the button canvas. The animation should toggle to a "liked" state.

The animation responds to your pointer without a single line of event-handling code on your end. The Rive runtime handles the forwarding automatically.

Wire up the playback controls

Your HTML already has three buttons labeled Play, Pause, and Stop below the animation canvas. These give you programmatic control over the Rive instance's playback state.

  • In app.js, add the following code below the buttonRive instance you just created:
// Playback controls
document.getElementById("btn-play").addEventListener("click", () => {
  buttonRive.play();
});

document.getElementById("btn-pause").addEventListener("click", () => {
  buttonRive.pause();
});

document.getElementById("btn-stop").addEventListener("click", () => {
  buttonRive.stop();
});

What does this code do?

Each button gets a click event listener that calls a playback method on the Rive instance.

  • .play() resumes the state machine from where it left off.
  • .pause() freezes the animation at its current frame.
  • .stop() halts the animation and resets it to the beginning.
  • Save app.js and refresh your browser.
  • Click Pause. The animation should freeze in place.
  • Click Play. The animation should resume from where it paused.
  • Click Stop. The animation should reset to its initial state.

Buttons not doing anything?

  • Check that the button IDs in your HTML match exactly: btn-play, btn-pause, and btn-stop. JavaScript's getElementById returns null if the ID doesn't match.
  • Make sure your playback control code is outside the buttonRive constructor object. It should be at the same indentation level, not nested inside the Rive options.

Help me debug why my Rive playback controls aren't working.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is the complete app.js file at this point. Compare it against yours:

// Hero Animation
const heroCanvas = document.getElementById("hero-canvas");

const heroRive = new rive.Rive({
  src: "assets/hero_animation.riv",
  canvas: heroCanvas,
  autoplay: true,
  stateMachines: "State Machine 1",
  onLoad: () => {
    heroRive.resizeDrawingSurfaceToCanvas();
    heroCanvas.classList.add("loaded");
  },
  onStateChange: (event) => {
    console.log("Hero state changed:", event.data);
  },
});

// Resize hero canvas on window resize
window.addEventListener("resize", () => {
  heroRive.resizeDrawingSurfaceToCanvas();
});

// Button Animation
const buttonCanvas = document.getElementById("button-canvas");

const buttonRive = new rive.Rive({
  src: "assets/like_button.riv",
  canvas: buttonCanvas,
  autoplay: true,
  stateMachines: "State Machine 1",
  onLoad: () => {
    buttonRive.resizeDrawingSurfaceToCanvas();
  },
});

// Playback controls
document.getElementById("btn-play").addEventListener("click", () => {
  buttonRive.play();
});

document.getElementById("btn-pause").addEventListener("click", () => {
  buttonRive.pause();
});

document.getElementById("btn-stop").addEventListener("click", () => {
  buttonRive.stop();
});

You now have two Rive animations running on the same page. One plays automatically in the background, and the other responds to your pointer and your playback buttons. Next up, you'll add polish with smooth scrolling, state change logging, and a loading transition.

Polish and Add Scroll Interactions

Your landing page now has a full-screen animated hero and an interactive button with playback controls. The core functionality is working, but a polished page needs smooth navigation between sections.

In this step, you will wire up smooth scrolling to the call-to-action link and verify the polish features already baked into your code. You will also explore how the onStateChange callback lets you observe state machine transitions in real time.

In this step, get ready to:
  • Add smooth scrolling so the hero CTA link glides to the button section.
  • Observe state machine transitions in the browser console.
  • Verify the canvas fade-in loading state prevents a flash of empty canvas.
Add smooth scrolling to the CTA link

The "See it in action" link in your hero section has an href="#cta" attribute. By default, clicking it jumps instantly to the CTA section. A smooth scroll creates a much more polished experience.

  • In app.js, add the following code at the very bottom of the file, below the playback control listeners:
// Smooth scroll
document.getElementById("scroll-to-cta").addEventListener("click", (e) => {
  e.preventDefault();
  document.getElementById("cta").scrollIntoView({ behavior: "smooth" });
});

What does this code do?

This grabs the "See it in action" link by its scroll-to-cta ID and listens for clicks.

  • e.preventDefault() stops the browser from jumping instantly to #cta.
  • scrollIntoView({ behavior: "smooth" }) animates the scroll to the CTA section over a short duration instead.
  • Save app.js.
  • Refresh your browser and click the purple "See it in action" button in the hero section. The page should glide smoothly down to the Interactive Button section.

Page jumps instead of scrolling smoothly?

  • Make sure e.preventDefault() is the first line inside the callback. Without it, the browser handles the anchor link jump before your smooth scroll runs.
  • Check that the ID in your JavaScript (scroll-to-cta) matches the id attribute on the anchor tag in your HTML exactly.

Still stuck? Ask for help.

Observe state machine transitions in the console

Your hero animation already includes an onStateChange callback that logs state transitions to the console. This is a powerful debugging tool for understanding how a state machine responds to user interaction in real time.

  • Open your browser's DevTools by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
  • Click the Console tab if it is not already selected.
  • Move your mouse over the hero animation and click on it.

You should see messages like Hero state changed: followed by the state name(s) the animation transitioned to. Each log represents the state machine reacting to your pointer events.

Why is this useful?

When you work with more complex Rive files, the onStateChange callback helps you verify which states the animation enters. You can use these state names to trigger other actions in your app, like showing a tooltip or navigating to a new page.

Verify the canvas fade-in loading state

Your page already handles a common animation pitfall: the flash of an empty canvas before the .riv file finishes loading. The hero canvas starts hidden and fades in only after the Rive file is ready.

  • Do a hard refresh in your browser by pressing Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). This clears the cache so the animation file loads from scratch.
  • Watch the hero section carefully. The canvas should fade in smoothly once the animation is ready, instead of popping in abruptly.

How does the fade-in work?

In your CSS, #hero-canvas starts with opacity: 0 and a transition: opacity 0.5s ease-in rule. When the Rive file finishes loading, the onLoad callback adds the loaded class, which sets opacity: 1. The CSS transition animates between the two values.

Simulate a slow network

To see the fade-in more clearly, open DevTools, go to the Network tab, and set throttling to Slow 3G. Hard refresh again. You will clearly see the canvas stay invisible until the animation file loads, then fade in gracefully.

✔️ Awesome, I've got everything!

Double check you have saved app.js. Your landing page is now fully polished and interactive.

ⓧ I'd like to double check the full code

Here is the complete app.js file. Compare it against yours:

// Hero Animation
const heroCanvas = document.getElementById("hero-canvas");

const heroRive = new rive.Rive({
  src: "assets/hero_animation.riv",
  canvas: heroCanvas,
  autoplay: true,
  stateMachines: "State Machine 1",
  onLoad: () => {
    heroRive.resizeDrawingSurfaceToCanvas();
    heroCanvas.classList.add("loaded");
  },
  onStateChange: (event) => {
    console.log("Hero state changed:", event.data);
  },
});

// Resize hero canvas on window resize
window.addEventListener("resize", () => {
  heroRive.resizeDrawingSurfaceToCanvas();
});

// Button Animation
const buttonCanvas = document.getElementById("button-canvas");

const buttonRive = new rive.Rive({
  src: "assets/like_button.riv",
  canvas: buttonCanvas,
  autoplay: true,
  stateMachines: "State Machine 1",
  onLoad: () => {
    buttonRive.resizeDrawingSurfaceToCanvas();
  },
});

// Playback controls
document.getElementById("btn-play").addEventListener("click", () => {
  buttonRive.play();
});

document.getElementById("btn-pause").addEventListener("click", () => {
  buttonRive.pause();
});

document.getElementById("btn-stop").addEventListener("click", () => {
  buttonRive.stop();
});

// Smooth scroll
document.getElementById("scroll-to-cta").addEventListener("click", (e) => {
  e.preventDefault();
  document.getElementById("cta").scrollIntoView({ behavior: "smooth" });
});

Your landing page is complete. It has a full-screen animated hero, an interactive button with playback controls, smooth scrolling, observable state transitions, and a polished loading experience. Next, take on the Secret Mission to add a scroll-triggered animation that plays when the user reaches a new section.

Secret mission

Scroll-Triggered Animation Section

Your landing page already plays animations on load and reacts to pointer events. But what about animations that wait for the perfect moment? Add a scroll-triggered section that plays a Rive animation only when the user scrolls it into view.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

  • Project folder (index.html, styles.css, app.js).
  • The /assets folder containing downloaded .riv files.
  • Free Rive account (rive.app).
  • Browser tabs and any running Live Server instances.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend your landing page with more Rive animations.

  • Your project folder stays on your Desktop ready for editing.
  • Your Rive account stays active for browsing and downloading more community files.

Pause - I'll come back to this later

Close your running processes to free up memory. Your files stay on disk for when you return.

  • Stop Live Server if it is running (click the port number in the VS Code status bar to stop it).
  • Close the browser tabs showing your landing page.
  • Close VS Code. Your project folder and all files remain on your Desktop.

Delete - I don't want to use this again

Remove all project resources and start fresh. This cannot be undone.

  • Stop Live Server and close all related browser tabs.
  • Delete the entire project folder from your Desktop.
  • Your Rive account is free and does not need to be deleted. Keep it if you want to explore more community animations later.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully interactive landing page powered by Rive animations that respond to user interaction in real time.

You've learned how to:

  • Loaded and rendered .riv files using the Rive web runtime with responsive, pixel-perfect canvas sizing.
  • Built interactive animations that respond to hover and click automatically via state machine Listeners.
  • Programmatically controlled animation playback with play, pause, and stop, and observed state transitions with onStateChange.
  • Secret Mission: Added a scroll-triggered animation using the Intersection Observer API to play and pause a Rive animation based on viewport visibility.

Ready to quiz yourself?