Micro Interactions with Motion + Rive

Build a React showcase of 5 animated micro interactions using Motion and Rive.

Introduction

30 Second Summary

Think about the subtle bounce when you tap a button on your phone, or the smooth slide of a notification appearing at the bottom of your screen. These tiny animations make apps feel alive and responsive.

In this project, you will build a Micro Interactions Showcase featuring five interactive components that demonstrate two different approaches to animation in React apps.

What You'll Build

A single-page React application showcasing five polished micro interaction components powered by Motion (code-driven spring animations) and Rive (designer-authored state machine animations).

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

  • An animated button with hover and tap spring physics, plus a toast notification system with smooth enter/exit transitions.
  • Two custom .riv files built in the Rive editor with state machines you control from React code.
  • A polished gallery page composing all components into a portfolio-ready demo that proves you can implement both code-driven and designer-authored animations.
  • Secret Mission: A draggable card component with elastic constraints and momentum physics using Motion's drag API.

Are there any prerequisites?

You should be comfortable with basic React concepts like components, JSX, useState, useEffect, and useRef. No prior animation experience is needed.

Set Up the Project

Every showcase project starts with a solid foundation. Before you build any animations, you need a React project with the right tools installed and boilerplate cleared out.

In this step, you will scaffold a Vite + React app, install the two animation libraries (Motion and the Rive web runtime), and strip the starter files down to a clean slate. You will also confirm access to the Rive editor, which you will use in later steps to design your own animations.

In this step, get ready to:
  • Scaffold a Vite + React project and install Motion and the Rive runtime.
  • Clean out boilerplate files and verify the dev server runs.
  • Confirm you can access the Rive editor at rive.app.
Scaffold the project and install dependencies

Vite is a modern build tool that scaffolds React projects instantly. You will use it to create your project folder with all the React plumbing already wired up.

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

What does this command do?

npm create vite@latest downloads and runs the Vite project generator.

micro-interactions-showcase is the folder name for your project. The --template react flag tells Vite to set up a JavaScript React project with all the starter files.

  • Move into your new project folder and install the base dependencies by running these commands:
cd micro-interactions-showcase
npm install
  • Confirm the install succeeded. You should see a message like added 200+ packages in the terminal output.

Seeing permission errors?

On macOS, try prefixing with sudo. On Windows, open your terminal as Administrator. If npm itself is not found, confirm Node.js is installed by running node --version. You need Node.js 20.19+ or 22.12+.

help me fix npm permission errors on my machine

  • Install the two animation libraries by running this command:
npm install motion@12.38.0 @rive-app/webgl2@2.37.5

What are these packages?

Motion (formerly Framer Motion) is a declarative animation library for React. It handles code-driven animations like hover effects, taps, and enter/exit transitions.

@rive-app/webgl2 is the Rive web runtime. It renders .riv animation files on a canvas element using WebGL2, which supports all Rive features including advanced rendering effects.

  • Confirm both packages installed by running:
ls node_modules/motion node_modules/@rive-app/webgl2
  • You should see file listings for both folders, confirming the packages are present.
Clean out boilerplate and verify the dev server

Vite's React template comes with demo content like a counter app, logos, and sample CSS. You will strip all of that out and replace it with a clean starting point for your showcase.

  • Delete the boilerplate files and create a components folder by running these commands:
rm src/App.css
rm -rf src/assets
rm public/vite.svg
mkdir src/components

What are you deleting?

src/App.css is Vite's demo styling. src/assets/ contains a React logo SVG. public/vite.svg is the Vite logo shown in the demo.

The new src/components/ folder is where you will build each animation component in the upcoming steps.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the micro-interactions-showcase folder on your Desktop, then click Open.
  • In the Explorer sidebar, click on src/App.jsx to open it.
  • Select all the existing content (Cmd+A on macOS or Ctrl+A on Windows) and replace it with this code:
function App() {
  return (
    <div>
      <h1>Micro Interactions Showcase</h1>
    </div>
  )
}

export default App

What does this code do?

This is your blank-slate App component. It renders a single heading that you will replace with the full showcase gallery in Step 7.

  • Save src/App.jsx (Cmd+S on macOS or Ctrl+S on Windows).
  • Now open src/index.css in the Explorer sidebar.
  • Select all and replace the content with this code:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f0f0f;
  color: #ffffff;
  min-height: 100vh;
  padding: 2rem;
}

What does this code do?

The * selector resets all margins, padding, and box-sizing for a consistent base across browsers.

The body block sets a dark background (#0f0f0f) with white text, which makes your animation components pop visually.

  • Save src/index.css.
  • Now open src/main.jsx in the Explorer sidebar.
  • Select all and replace the content with this code:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

What does this code do?

This is the entry point that mounts your React app into the HTML page. It imports your App component and index.css stylesheet, then renders everything inside StrictMode (which helps catch common bugs during development).

  • Save src/main.jsx.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is each file's complete content. Compare your files against these.

function App() {
  return (
    <div>
      <h1>Micro Interactions Showcase</h1>
    </div>
  )
}

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

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f0f0f;
  color: #ffffff;
  min-height: 100vh;
  padding: 2rem;
}
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
  • Start the development server by running this command in your terminal:
npm run dev
  • You should see output showing a local URL, typically http://localhost:5173.
  • Open that URL in your browser.
  • You should see a dark page with white text that reads Micro Interactions Showcase.

Seeing a blank page or errors?

Check that you deleted src/App.css and that your App.jsx does not import it. Also verify that main.jsx imports ./index.css (not a file that no longer exists).

help me debug my Vite React dev server showing a blank page

Confirm Rive editor access

In later steps, you will design two custom animations in the Rive editor. The editor runs entirely in your browser, so there is nothing to install.

  • Open your browser and navigate to rive.app.
  • Click Get Started (or Sign In if you already have an account).
  • Create a free account using Google, GitHub, or email.
  • Once signed in, confirm you can see the Rive dashboard with a New File button.

What is the Rive editor?

Rive is a design tool for building interactive animations with state machines. You draw shapes, define animation states, and export a lightweight .riv file that your React code plays back on a canvas.

Can't create an account?

Rive accounts are free. If the sign-up page is not loading, try a different browser or disable ad blockers. You do not need a paid plan for this project.

help me sign up for a Rive account

Your project is scaffolded, dependencies are installed, boilerplate is cleaned, and you have access to the Rive editor. Next up, you will build your first animated component: a spring-powered button with satisfying hover and tap feedback.

Build an Animated Button with Motion

Your project is scaffolded and the dev server is running. Now it's time to build the first component in your showcase.

Buttons are perfect for exploring micro interactions because users hover and click them constantly. In this step, you'll use Motion's spring physics to make a button feel physically responsive to hover and tap gestures.

In this step, get ready to:
  • Create an AnimatedButton component with spring-powered hover and tap animations.
  • Import and render the button inside a showcase card in App.jsx.
  • Add CSS styles for the gallery grid layout.
Create the AnimatedButton component

Motion's declarative API lets you define animations as props on special motion. elements. The whileHover and whileTap props let you define gesture-driven animations that activate automatically when the user interacts.

  • Create a new file called AnimatedButton.jsx inside the src/components/ folder.
  • Add the following code to AnimatedButton.jsx:
import { motion } from 'motion/react'

// Inline style object for the button appearance
const buttonStyle = {
  padding: '14px 28px',
  borderRadius: '12px',
  border: 'none',
  background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
  color: '#fff',
  fontSize: '1rem',
  fontWeight: 600,
  cursor: 'pointer',
  outline: 'none',
}

function AnimatedButton() {
  return (
    <motion.button
      style={buttonStyle}
      whileHover={{
        scale: 1.05,
        transition: { type: 'spring', stiffness: 400, damping: 15 },
      }}
      whileTap={{
        scale: 0.92,
        rotate: 2,
        transition: { type: 'spring', stiffness: 500, damping: 20 },
      }}
    >
      Click Me
    </motion.button>
  )
}

export default AnimatedButton

What does this code do?

motion.button is a Motion-enhanced version of a regular <button>. It accepts all the same props plus animation props like whileHover and whileTap.

  • whileHover scales the button up to 1.05x when hovered. The spring transition with stiffness 400 and damping 15 makes it feel bouncy and responsive.
  • whileTap squishes the button down to 0.92x and rotates it 2 degrees when clicked. The higher stiffness (500) makes the tap reaction feel faster and snappier.
  • The rotate on tap creates a physical "press" sensation. Without it, the button just shrinks. With it, the button feels like a real object being pushed.

What are spring physics?

Spring animations simulate real-world physics instead of using fixed durations. stiffness controls how quickly the animation moves toward its target. damping controls how much "friction" slows it down.

Low damping creates a bouncy, oscillating motion. High damping creates a smooth settle. The values in this component (400/15 for hover, 500/20 for tap) hit the sweet spot between playful and professional.

  • Save AnimatedButton.jsx.

You won't see a change yet because App.jsx doesn't render this component. The next chunk wires it up.

Render the button in App.jsx

Each component in your showcase lives inside a card with a badge indicating which tool powers it. This structure makes the showcase portfolio-ready.

  • Open src/App.jsx and replace its entire contents with the following code:
import AnimatedButton from './components/AnimatedButton'

function App() {
  return (
    <div>
      <h1>Micro Interactions Showcase</h1>
      {/* Grid container for all showcase cards */}
      <div className="showcase-grid">
        {/* Each card wraps one component with a label */}
        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Spring Button</h2>
          <AnimatedButton />
        </div>
      </div>
    </div>
  )
}

export default App

What does this code do?

  • The import pulls in your new AnimatedButton component so you can use it as a JSX element.
  • showcase-grid will become a responsive CSS grid. As you add more components in later steps, they'll flow into columns automatically.
  • The tool-badge span labels which animation library powers each card. This helps viewers understand when to reach for Motion vs. Rive.
  • Save App.jsx.
  • Check your browser at http://localhost:5173. You should see the heading "Micro Interactions Showcase" and a purple gradient "Click Me" button. The button is unstyled in its card layout for now.
  • Hover over the button. It should scale up slightly. Click it and it should squish down and rotate.

Button not animating?

  • Make sure your import says motion/react (not framer-motion which is the legacy package name).
  • Check that you're using motion.button (lowercase b), not motion.Button.
  • If you see a blank page, check the terminal for errors. A missing closing brace or parenthesis in the JSX is the most common cause.

Why isn't my Motion button animating when I hover or click?

Style the showcase gallery

The button works, but it needs a polished card layout around it. You'll add a responsive grid and dark-themed card styles.

  • Open src/index.css and add the following styles below the existing body rule:
h1 {
  text-align: center;
  margin-bottom: 2rem;
  font-size: 2rem;
}

.showcase-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

.showcase-card {
  background: #1a1a1a;
  border-radius: 16px;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.showcase-card h2 {
  font-size: 1.2rem;
  color: #e0e0e0;
}

.showcase-card .tool-badge {
  font-size: 0.75rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: #2a2a2a;
  color: #888;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

What do these styles do?

  • .showcase-grid creates a responsive grid that automatically adjusts columns based on available width. Each column is at least 320px wide.
  • .showcase-card gives each component a dark card with rounded corners, centered content, and consistent spacing.
  • .tool-badge is a subtle pill-shaped label that identifies whether the animation is powered by Motion or Rive.
  • Save index.css and check your browser.
  • You should see a dark card with a small "MOTION" badge at the top, a "Spring Button" heading, and your purple gradient button centered inside.

Card not showing up?

  • Make sure the class name in App.jsx (className="showcase-grid") matches the CSS selector exactly. Class names are case-sensitive.
  • Check that you added the new styles after the closing } of the body rule, not inside it.

Why isn't my showcase card styling appearing in the browser?

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here's what each file should look like at this point.

import { motion } from 'motion/react'

const buttonStyle = {
  padding: '14px 28px',
  borderRadius: '12px',
  border: 'none',
  background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
  color: '#fff',
  fontSize: '1rem',
  fontWeight: 600,
  cursor: 'pointer',
  outline: 'none',
}

function AnimatedButton() {
  return (
    <motion.button
      style={buttonStyle}
      whileHover={{
        scale: 1.05,
        transition: { type: 'spring', stiffness: 400, damping: 15 },
      }}
      whileTap={{
        scale: 0.92,
        rotate: 2,
        transition: { type: 'spring', stiffness: 500, damping: 20 },
      }}
    >
      Click Me
    </motion.button>
  )
}

export default AnimatedButton
import AnimatedButton from './components/AnimatedButton'

function App() {
  return (
    <div>
      <h1>Micro Interactions Showcase</h1>
      <div className="showcase-grid">
        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Spring Button</h2>
          <AnimatedButton />
        </div>
      </div>
    </div>
  )
}

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

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f0f0f;
  color: #ffffff;
  min-height: 100vh;
  padding: 2rem;
}

h1 {
  text-align: center;
  margin-bottom: 2rem;
  font-size: 2rem;
}

.showcase-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

.showcase-card {
  background: #1a1a1a;
  border-radius: 16px;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.showcase-card h2 {
  font-size: 1.2rem;
  color: #e0e0e0;
}

.showcase-card .tool-badge {
  font-size: 0.75rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: #2a2a2a;
  color: #888;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

Your first micro interaction is live. Next up, you'll build a toast notification system that animates elements in AND out of the page using AnimatePresence.

Build Toast Notifications with AnimatePresence

Your animated button responds to user gestures, but micro interactions go beyond hover and tap. Toast notifications are a perfect example of motion that communicates state changes. They slide in to confirm an action, then disappear on their own.

The tricky part is animating elements as they leave the DOM. React normally removes components instantly. In this step, you'll use AnimatePresence from Motion to orchestrate smooth exit animations before React unmounts each toast.

In this step, get ready to:
  • Create a ToastManager component with enter and exit spring animations.
  • Style toast notifications with fixed positioning in the bottom-right corner.
  • Add the toast system to the showcase gallery.
Create the ToastManager component

A toast system needs two things: state management to track which toasts are visible, and animation logic to smoothly transition them in and out. You'll build both in a single component.

  • Create a new file called ToastManager.jsx inside your src/components folder.
  • Add the following code to build the full toast notification component:
import { useState } from 'react'
import { motion, AnimatePresence } from 'motion/react'

// Module-level counter ensures each toast gets a unique ID
let toastId = 0

function ToastManager() {
  const [toasts, setToasts] = useState([])

  const addToast = () => {
    const id = toastId++
    // Cycle through messages based on toast count
    const messages = [
      'Action completed!',
      'File saved successfully.',
      'New notification arrived.',
      'Settings updated.',
    ]
    const message = messages[id % messages.length]
    setToasts((prev) => [...prev, { id, message }])
    // Auto-remove the toast after 3 seconds
    setTimeout(() => {
      setToasts((prev) => prev.filter((t) => t.id !== id))
    }, 3000)
  }

  return (
    <div>
      <button
        onClick={addToast}
        style={{
          padding: '10px 20px',
          borderRadius: '8px',
          border: '1px solid #333',
          background: '#2a2a2a',
          color: '#fff',
          cursor: 'pointer',
        }}
      >
        Trigger Toast
      </button>

      <div className="toast-container">
        <AnimatePresence mode="popLayout">
          {toasts.map((toast) => (
            <motion.div
              key={toast.id}
              className="toast"
              layout
              initial={{ opacity: 0, y: 40, scale: 0.9 }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{ opacity: 0, y: -20, scale: 0.9 }}
              transition={{ type: 'spring', stiffness: 300, damping: 25 }}
            >
              {toast.message}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </div>
  )
}

export default ToastManager

What does this code do?

The toastId counter lives outside the component so it persists across re-renders. Each time addToast fires, it grabs the next ID and picks a message from the rotating list.

The setTimeout removes each toast after 3 seconds by filtering it out of state. When React removes the element, AnimatePresence intercepts the unmount and plays the exit animation first.

The mode="popLayout" setting removes exiting toasts from document flow immediately. This lets the remaining toasts reflow into their new positions while the exit animation plays. The layout prop on each toast animates that reflow smoothly.

What are initial, animate, and exit?

These three props define the toast's animation lifecycle. initial is where the toast starts (faded, shifted down, slightly small). animate is where it settles (fully visible, in position, full size). exit is where it goes when removed (faded, shifted up, slightly small).

The spring transition makes each phase feel physical rather than robotic. A stiffness of 300 with damping of 25 gives a snappy-but-smooth feel.

  • Save ToastManager.jsx.

You won't see a change in the browser yet. The component needs CSS styles and a spot in App.jsx before it renders.

Add toast notification styles

The toast container needs fixed positioning so notifications appear in the bottom-right corner of the viewport regardless of scroll position.

  • Open src/index.css and add the following styles at the end of the file:
.toast-container {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 1000;
}

.toast {
  background: #2d2d2d;
  color: #fff;
  padding: 1rem 1.5rem;
  border-radius: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  font-size: 0.9rem;
}

What do these styles do?

The .toast-container uses position: fixed to pin itself to the viewport's bottom-right corner. The z-index: 1000 ensures toasts appear above all other content.

Each .toast gets a subtle box shadow and rounded corners to look like a floating card.

  • Save index.css.

Styles not applying?

Make sure you added the new rules after the existing .showcase-card .tool-badge block, not inside it. Each selector needs to be at the top level of the file.

help me figure out why my toast CSS styles aren't showing up

Add ToastManager to the gallery

The component is built and styled. Now you need to import it and give it a spot in the showcase grid alongside the animated button.

  • Open src/App.jsx.
  • Find this line at the top of the file:
import AnimatedButton from './components/AnimatedButton'
  • Add the ToastManager import directly below it:
import ToastManager from './components/ToastManager'
  • Next, find the closing </div> of the first showcase-card (the one containing <AnimatedButton />). Add this new card below it:
        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Toast Notifications</h2>
          <ToastManager />
        </div>

What does this add?

This creates a second card in the showcase grid with its own "Motion" badge and heading. The <ToastManager /> component renders the trigger button inside the card, while the toasts themselves appear fixed in the bottom-right corner of the page.

  • Save App.jsx and check your browser.
  • You should see a second card labeled "Toast Notifications" with a **Trigger Toast** button inside it.
  • Click the **Trigger Toast** button a few times. Each click should spawn a notification in the bottom-right corner that slides in, then fades out after 3 seconds.

Toasts not appearing?

Check that the className="toast-container" in your component matches the .toast-container selector in your CSS. A mismatch means the fixed positioning never applies.

Also verify your import path in App.jsx is './components/ToastManager'. If Vite shows a module-not-found error, the file name or path has a typo.

help me debug why my toast notifications aren't showing up

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files.

ⓧ I'd like to double check the full code

Here is the full code for each file you touched in this step.

import { useState } from 'react'
import { motion, AnimatePresence } from 'motion/react'

let toastId = 0

function ToastManager() {
  const [toasts, setToasts] = useState([])

  const addToast = () => {
    const id = toastId++
    const messages = [
      'Action completed!',
      'File saved successfully.',
      'New notification arrived.',
      'Settings updated.',
    ]
    const message = messages[id % messages.length]
    setToasts((prev) => [...prev, { id, message }])
    setTimeout(() => {
      setToasts((prev) => prev.filter((t) => t.id !== id))
    }, 3000)
  }

  return (
    <div>
      <button
        onClick={addToast}
        style={{
          padding: '10px 20px',
          borderRadius: '8px',
          border: '1px solid #333',
          background: '#2a2a2a',
          color: '#fff',
          cursor: 'pointer',
        }}
      >
        Trigger Toast
      </button>

      <div className="toast-container">
        <AnimatePresence mode="popLayout">
          {toasts.map((toast) => (
            <motion.div
              key={toast.id}
              className="toast"
              layout
              initial={{ opacity: 0, y: 40, scale: 0.9 }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{ opacity: 0, y: -20, scale: 0.9 }}
              transition={{ type: 'spring', stiffness: 300, damping: 25 }}
            >
              {toast.message}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </div>
  )
}

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

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f0f0f;
  color: #ffffff;
  min-height: 100vh;
  padding: 2rem;
}

h1 {
  text-align: center;
  margin-bottom: 2rem;
  font-size: 2rem;
}

.showcase-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

.showcase-card {
  background: #1a1a1a;
  border-radius: 16px;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.showcase-card h2 {
  font-size: 1.2rem;
  color: #e0e0e0;
}

.showcase-card .tool-badge {
  font-size: 0.75rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: #2a2a2a;
  color: #888;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.toast-container {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 1000;
}

.toast {
  background: #2d2d2d;
  color: #fff;
  padding: 1rem 1.5rem;
  border-radius: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  font-size: 0.9rem;
}
import AnimatedButton from './components/AnimatedButton'
import ToastManager from './components/ToastManager'

function App() {
  return (
    <div>
      <h1>Micro Interactions Showcase</h1>
      <div className="showcase-grid">
        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Spring Button</h2>
          <AnimatedButton />
        </div>

        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Toast Notifications</h2>
          <ToastManager />
        </div>
      </div>
    </div>
  )
}

export default App

Your showcase now has two interactive components. Next up, you'll step outside the code editor and into the Rive design tool to create a custom toggle switch animation from scratch.

Design a Toggle Switch in the Rive Editor

Your first two Motion components are live. Now you'll shift from code-driven animation to designer-authored animation using Rive.

Rive lets you design animations visually and export them as lightweight binary files that your code controls at runtime. In this step, you'll open the Rive editor in your browser, draw a toggle switch from scratch, and wire up a state machine that your React code can drive later.

In this step, get ready to:
  • Create a toggle switch artboard with a pill-shaped track and circular knob in the Rive editor.
  • Build a state machine with a boolean input that transitions between Off and On states.
  • Export the .riv file and add it to your project.
Create a new file and artboard in the Rive editor

The Rive editor is a free web app where you design animations visually. You'll create a toggle switch that looks like a pill-shaped track with a circular knob that slides left and right.

  • Open your browser and navigate to rive.app.
  • Log in to your Rive account.
  • Click the New File button on your dashboard to create a new file.
  • The editor opens with an empty stage. Set the artboard width to 200 and height to 100 in the stage options.
  • Click Create Artboard.

You should see a blank white artboard on the stage.

What is an artboard?

An artboard is the canvas for your animation. It defines the dimensions and background colour of the scene. Every Rive file has at least one artboard, and the runtime loads it by name or by default.

Now draw the two shapes that make up the toggle switch.

  • Select the Rectangle tool from the Create Tools menu in the toolbar.
  • Click and drag inside the artboard to draw a rectangle. In the Inspector panel on the right, set its width to 160 and height to 60.
  • Set the corner radius to 30 to make it pill-shaped. This value is half the height.
  • Set the fill colour to a medium grey (e.g. #555555). This is the "off" state colour of the track.
  • Centre the rectangle on the artboard by selecting it and using the alignment tools in the Inspector.
  • Select the Ellipse tool from the Create Tools menu.
  • Hold Shift while dragging inside the artboard to draw a perfect circle. Set its width and height to 50 in the Inspector.
  • Set the circle's fill colour to white (#FFFFFF).
  • Position the circle on the left side of the track. Set its X position so it sits within the pill shape with a small margin.

You should see a grey pill shape with a white circle sitting on its left side. This is the toggle in its "off" position.

Name your layers

Double-click each shape in the Hierarchy panel to rename them. Call the rectangle Track and the circle Knob. Named layers make it easier to find things when your file gets more complex.

Build a state machine with a boolean input

A state machine connects your animations together with logic. You'll create two animation states (Off and On) and a boolean input that controls which state is active.

  • Switch to Animate Mode using the mode toggle at the top of the editor.
  • In the Animations list (bottom-left area), click the + button and select Timeline to create a new animation.
  • Name this timeline Off. This is the default resting state of the toggle.
  • With the Off timeline selected, confirm the knob is positioned on the left and the track fill is grey. No keyframes are needed because this is the default pose.
  • Click + again in the Animations list and create another Timeline. Name it On.
  • With the On timeline selected, move the playhead to frame 0. Select the Knob and set a keyframe for its X position moved to the right side of the track.
  • Also keyframe the Track fill colour to a vibrant colour like green (#4CAF50) or purple (#6366f1) to show the active state.

What are timelines in Rive?

A timeline is a single animation clip. Each timeline captures keyframed changes to your shapes (position, colour, scale, etc.). The state machine decides which timeline plays and when, based on your inputs and transition rules.

Now create the state machine that connects these timelines.

  • Click the + button in the Animations list and select State Machine.
  • Rename the state machine to Toggle Machine. This name must match exactly because your React code references it.
  • The state machine graph appears. You should see an Entry node and an Exit node.
  • Right-click on the graph canvas and add the Off timeline as a state.
  • Right-click again and add the On timeline as a state.
  • Drag a transition line from the Entry node to the Off state. This sets Off as the default starting state.

Now add the boolean input that your code will control.

  • In the Inputs panel (below the state graph), click the + button and select Boolean.
  • Name the input isOn. This name must match exactly because your React code will look it up by this string.
  • Create a transition from the Off state to the On state by dragging between them.
  • Click on the transition line you just created. In the Inspector, add a condition: isOn equals true.
  • Create a transition from the On state back to the Off state.
  • Click on this reverse transition line. Add a condition: isOn equals false.

How does a state machine work?

The state machine starts at the Entry node and follows the first valid transition (in this case, to the Off state). When your React code changes the isOn boolean to true, the machine transitions from Off to On. Setting it back to false returns it to Off.

Your code never plays animations directly. It just changes input values and lets the state machine handle the rest.

Test the state machine by toggling the isOn checkbox in the Inputs panel. You should see the knob slide and the track change colour.

Knob not moving when you toggle isOn?

Make sure you added keyframes to the On timeline that move the knob's X position. If you only changed the position without setting a keyframe, Rive won't record the change.

Also check that both transition lines have the correct conditions. Click each transition line and verify the condition is set to isOn == true for Off-to-On and isOn == false for On-to-Off.

Help me debug my Rive toggle state machine transitions

Export the .riv file and add it to your project

With the state machine tested and working, you'll export the file and place it in your Vite project's public folder. Vite serves files from public/ as static assets at the root URL.

  • Click the Export button in the top-right area of the toolbar.
  • Select Download to download the .riv file to your computer.
  • Rename the downloaded file to toggle-switch.riv.
  • Move toggle-switch.riv into your project's public folder (the one at the root of your Vite project, next to src).
  • Confirm the file is in place by running:
ls public/

You should see toggle-switch.riv listed in the output.

Why the public folder?

Vite serves everything in public/ as static files at the root path. When your React code creates a Rive instance with src: '/toggle-switch.riv', the browser fetches it from this folder.

Export option not available or requires a paid plan?

Rive's runtime export feature may require a paid plan depending on your account type. If you see a paywall, check that you're using a Personal File in your workspace. Free accounts have unlimited personal files with basic export capabilities.

If you still cannot export, try downloading via the Editor menu (hamburger icon in the top-left) and selecting Export.

Help me export my .riv file from the Rive editor

Your toggle switch animation is designed and ready to load from React. Next up, you'll design a multi-state status icon with more complex state machine logic.

Design a Multi-State Status Icon in Rive

Your toggle switch proved that a single boolean can flip between two states. A status icon needs to express more than just on and off.

In this step, you will design a second Rive animation that cycles through four distinct visual states: idle, loading, success, and error. This teaches you how to use number inputs and triggers to control multi-state transitions from code.

In this step, get ready to:
  • Create a status icon artboard with four visual states in the Rive editor.
  • Build a state machine with a number input and a trigger input.
  • Export the .riv file and add it to your project.
Create the status icon artboard

Each Rive file is self-contained with its own artboard, animations, and state machine. You will create a fresh file for the status icon rather than adding to your toggle file.

  • In the Rive editor at editor.rive.app, click the + New File button to create a new file.
  • Set the artboard dimensions to 200 x 200 pixels.
  • Click Create Artboard.
  • Select the Ellipse tool (shortcut O) and draw a circle centered on the artboard, roughly 150 x 150 pixels.
  • Set the circle's fill to a neutral grey like #3a3a3a.
  • Inside the circle, draw simple icon shapes to represent each state. Use a small horizontal line for idle, a circular arrow for loading, a checkmark for success, and an X for error.
  • Place each icon shape on its own layer so you can animate their visibility independently.

Keep the artwork simple

These icons do not need to be pixel-perfect. Basic shapes (lines, paths, ellipses) are enough to teach the state machine concept. You can always refine the artwork later.

Create the timeline animations

Each visual state needs its own timeline animation. The state machine will switch between these timelines based on inputs from your React code.

  • In the Animations List panel (bottom left), click the + button and select Timeline.
  • Name this timeline idle.
  • On the idle timeline, show only the idle icon shape (horizontal line) and hide the others by setting their opacity to 0.
  • Create a second timeline named loading.
  • On the loading timeline, show only the circular arrow icon and add a looping rotation keyframe so it spins continuously.
  • Create a third timeline named success.
  • On the success timeline, show only the checkmark icon.
  • Create a fourth timeline named error.
  • On the error timeline, show only the X icon.

Why four separate timelines?

Each timeline represents a single visual state of the icon. The state machine decides which timeline to play based on the number input value your React code sends.

This is different from the toggle switch which only had two states driven by a boolean. A number input lets you express as many states as you need.

Build the state machine with number and trigger inputs

The state machine ties your four timelines together with inputs that your React code can control at runtime.

  • In the Animations List panel, click the + button and select State Machine.
  • Rename it to Status Machine by double-clicking the name.
  • In the Inputs panel on the left side of the Graph, click the + button to create a new input.
  • Select Number as the type and name it status.
  • Click + again to create a second input.
  • Select Trigger as the type and name it activate.

What is a trigger input?

A trigger is like a brief pulse. It fires once and immediately resets, unlike a boolean which stays true or false. Your React code will fire this trigger whenever the user selects a new status, telling the state machine to evaluate its transitions.

Now drag your four timeline animations onto the Graph to create states and wire up the transitions.

  • Drag the idle timeline from the Animations List onto the Graph to create a state.
  • Repeat for loading, success, and error.
  • Create a transition from the Entry state to the idle state by hovering over Entry until you see the connector, then dragging to idle.
  • Create a transition from the Any State node to idle.
  • Select this transition's direction indicator, then click the + next to Conditions.
  • Add two conditions: set status equals 0, and add the activate trigger.
  • Create a transition from Any State to loading with conditions: status equals 1 and the activate trigger.
  • Create a transition from Any State to success with conditions: status equals 2 and the activate trigger.
  • Create a transition from Any State to error with conditions: status equals 3 and the activate trigger.

How do conditions and triggers work together?

Each transition requires both the status number to equal a specific value AND the activate trigger to fire. This prevents unwanted transitions.

Without the trigger, the state machine would immediately transition whenever the number changes. The trigger gives you explicit control over when transitions occur.

  • Test your state machine by clicking the Play button in the toolbar.
  • Change the status number input to different values (0, 1, 2, 3) and click the activate trigger to confirm the icon transitions between states.

Animation not changing when you fire the trigger?

Check that each transition has both conditions: the number equals check AND the activate trigger. If you only added the number condition, the transition may fire continuously or not at all.

Also verify that you used the Any State node as the source for each transition. Transitions from specific states would only work when the machine is already in that particular state.

help me debug my Rive state machine transitions

Export the .riv file and add it to your project

With the state machine tested, export the file so your React app can load it at runtime.

  • Click the blue Export button in the top-right of the Rive editor toolbar.
  • Select For runtime to download the .riv file.
  • Rename the downloaded file to status-icon.riv.
  • Move status-icon.riv into your project's public folder, alongside toggle-switch.riv.
  • Confirm both files exist by running:
ls public/

You should see both toggle-switch.riv and status-icon.riv listed in the output.

Why the public folder?

Vite serves everything in the public folder as static assets at the root URL. Your React code will load /status-icon.riv directly from this path.

State machine and input names are automatically exported in .riv files. You do not need to manually mark them for export.

Export button greyed out?

Runtime export requires a Rive account (free tier works). Make sure you are signed in. If you see a paywall, use the Download source option on the free tier or check your account plan.

help me export my Rive file for runtime use

Your second Rive animation is ready. Next up, you will write the React components that load both .riv files and wire their state machine inputs to interactive UI controls.

Integrate Rive Animations in React

You have two Rive files sitting in your public folder with state machines ready to receive input. Now you need to load them into React components so users can interact with them in the browser.

The Rive web runtime renders .riv files onto an HTML canvas element. You'll create React components that initialize the runtime, query the state machine inputs you defined in the editor, and wire those inputs to React state.

In this step, get ready to:
  • Create a RiveToggle component that loads and controls the toggle .riv file.
  • Create a RiveStatusIcon component with multi-state button controls.
  • Add both Rive components to the showcase gallery.
Create the RiveToggle component

The Rive web runtime needs a canvas element to draw on and a reference to the loaded Rive instance to control it. You'll use React's useRef for stable references and useEffect to handle initialization and cleanup.

  • Create a new file at src/components/RiveToggle.jsx and add the following code:
import { useRef, useEffect, useState } from 'react'
import { Rive } from '@rive-app/webgl2'

function RiveToggle() {
  const canvasRef = useRef(null)
  const riveRef = useRef(null)
  const [isOn, setIsOn] = useState(false)
  const inputRef = useRef(null)

  useEffect(() => {
    const r = new Rive({
      src: '/toggle-switch.riv',
      canvas: canvasRef.current,
      autoplay: true,
      stateMachines: 'Toggle Machine',
      onLoad: () => {
        r.resizeDrawingSurfaceToCanvas()
        const inputs = r.stateMachineInputs('Toggle Machine')
        const boolInput = inputs.find((i) => i.name === 'isOn')
        inputRef.current = boolInput
      },
    })
    riveRef.current = r

    return () => {
      r.cleanup()
    }
  }, [])

  const handleClick = () => {
    const newValue = !isOn
    setIsOn(newValue)
    if (inputRef.current) {
      inputRef.current.value = newValue
    }
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        className="rive-canvas"
        width={400}
        height={200}
        onClick={handleClick}
      />
      <p style={{ textAlign: 'center', fontSize: '0.8rem', color: '#666' }}>
        {isOn ? 'ON' : 'OFF'}
      </p>
    </div>
  )
}

export default RiveToggle

What does this code do?

The useEffect creates a new Rive instance that loads toggle-switch.riv from your public folder onto the canvas. It starts the Toggle Machine state machine automatically.

Once loaded, resizeDrawingSurfaceToCanvas() ensures crisp rendering on high-DPI displays. Then stateMachineInputs() queries the state machine for the boolean input named isOn and stores it in a ref for later use.

The handleClick function flips the React state and pushes the new value into the Rive input, triggering the animation transition you built in the editor.

  • Save src/components/RiveToggle.jsx.

You won't see a change yet. The next substep adds the component to the page.

Wire the toggle to React state

The RiveToggle component is ready, but it needs CSS sizing and a spot in the gallery to render. You'll add the canvas styles and mount the component so you can test the click-to-toggle interaction.

  • In src/index.css, add this rule at the bottom of the file:
.rive-canvas {
  width: 200px;
  height: 100px;
  cursor: pointer;
}

What does this style do?

The canvas element's width and height HTML attributes set the drawing resolution (400x200 in the JSX). The CSS controls the display size (200x100). This 2x ratio keeps the rendering sharp on retina screens. The pointer cursor signals that the canvas is clickable.

  • Save src/index.css.
  • Now update src/App.jsx to import and render the RiveToggle. Find the import lines at the top of the file and add these two lines below the ToastManager import:
import RiveToggle from './components/RiveToggle'
  • Then find the closing </div> of the Toast Notifications showcase-card. Add this new card below it:
        <div className="showcase-card">
          <span className="tool-badge">Rive</span>
          <h2>Toggle Switch</h2>
          <RiveToggle />
        </div>

What does this render?

This adds a third card to the showcase grid. The "Rive" badge tells viewers this animation is designer-authored rather than code-driven. Inside, the RiveToggle component mounts its canvas and begins playing the state machine immediately.

  • Save src/App.jsx and refresh your browser.

You should see a third card labeled "Toggle Switch" with a small canvas showing your toggle in its Off state. Click the canvas to flip it to On.

Toggle not animating on click?

  • Confirm your .riv file is at public/toggle-switch.riv (not inside src/). Vite serves static assets from the public folder.
  • Check that the state machine name in the Rive editor matches Toggle Machine exactly (case-sensitive).
  • Verify the boolean input is named isOn in the Rive editor. If the name doesn't match, inputs.find() returns undefined and the click does nothing.

Help me debug why my Rive toggle isn't responding to clicks

Create the RiveStatusIcon component

The toggle uses a single boolean input. The status icon is more complex. It combines a number input (to select which state to display) with a trigger input (to kick off the transition animation).

  • Create a new file at src/components/RiveStatusIcon.jsx and add the following code:
import { useRef, useEffect, useState } from 'react'
import { Rive } from '@rive-app/webgl2'

function RiveStatusIcon() {
  const canvasRef = useRef(null)
  const riveRef = useRef(null)
  const statusInputRef = useRef(null)
  const triggerRef = useRef(null)
  const [currentState, setCurrentState] = useState('idle')

  useEffect(() => {
    const r = new Rive({
      src: '/status-icon.riv',
      canvas: canvasRef.current,
      autoplay: true,
      stateMachines: 'Status Machine',
      onLoad: () => {
        r.resizeDrawingSurfaceToCanvas()
        const inputs = r.stateMachineInputs('Status Machine')
        const numberInput = inputs.find((i) => i.name === 'status')
        const trigger = inputs.find((i) => i.name === 'activate')
        statusInputRef.current = numberInput
        triggerRef.current = trigger
      },
      onStateChange: (event) => {
        if (event.data && event.data.length > 0) {
          setCurrentState(event.data[0])
        }
      },
    })
    riveRef.current = r

    return () => {
      r.cleanup()
    }
  }, [])

  const setStatus = (value, label) => {
    if (statusInputRef.current) {
      statusInputRef.current.value = value
    }
    if (triggerRef.current) {
      triggerRef.current.fire()
    }
    setCurrentState(label)
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        className="rive-canvas-large"
        width={400}
        height={400}
      />
      <div className="status-controls">
        <button onClick={() => setStatus(0, 'idle')}>Idle</button>
        <button onClick={() => setStatus(1, 'loading')}>Loading</button>
        <button onClick={() => setStatus(2, 'success')}>Success</button>
        <button onClick={() => setStatus(3, 'error')}>Error</button>
      </div>
      <p className="state-label">State: {currentState}</p>
    </div>
  )
}

export default RiveStatusIcon

What does this code do?

This component follows the same canvas + useEffect pattern as RiveToggle, but with two key differences. First, it queries two inputs from the state machine: a number input (status) and a trigger input (activate).

The setStatus function sets the number value (0=idle, 1=loading, 2=success, 3=error) and fires the trigger to start the transition animation.

The onStateChange callback listens for state transitions inside Rive and updates a label so you can see which animation state is currently active.

  • Save src/components/RiveStatusIcon.jsx.
  • Add the styles for the status icon to the bottom of src/index.css:
.rive-canvas-large {
  width: 200px;
  height: 200px;
}

.status-controls {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: center;
}

.status-controls button {
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px solid #333;
  background: #1a1a1a;
  color: #ccc;
  cursor: pointer;
  font-size: 0.8rem;
}

.status-controls button:hover {
  background: #2a2a2a;
}

.state-label {
  font-size: 0.8rem;
  color: #666;
  margin-top: 0.5rem;
}

What do these styles do?

The .rive-canvas-large class gives the status icon a square canvas. The .status-controls flex container lays out the four state buttons in a centered, wrapping row. The .state-label shows the current animation state in muted text below the buttons.

  • Save src/index.css.
  • In src/App.jsx, add this import below the RiveToggle import:
import RiveStatusIcon from './components/RiveStatusIcon'
  • Add one more useState import at the very top of the file, above the component imports:
import { useState } from 'react'
  • Then add the Status Icon card below the Toggle Switch card you added earlier:
        <div className="showcase-card">
          <span className="tool-badge">Rive</span>
          <h2>Status Icon</h2>
          <RiveStatusIcon />
        </div>
  • Save src/App.jsx and refresh your browser.

You should see a fourth card labeled "Status Icon" with a larger canvas and four buttons below it. Click each button to see the icon transition between idle, loading, success, and error states.

Status icon not changing states?

  • Confirm your .riv file is at public/status-icon.riv (not inside src/).
  • Verify the state machine is named Status Machine, the number input is named status, and the trigger is named activate. All names are case-sensitive.
  • Open the browser console and check for WebGL errors. Browsers limit concurrent WebGL contexts to around 8-16, but with only two canvases this should not be an issue.

Help me debug my Rive status icon state machine inputs

✔️ Awesome, I've got everything!

Great. Double check you've saved all files before moving on.

ⓧ I'd like to double check the full code

Here's the complete code for each file you modified or created in this step.

import { useRef, useEffect, useState } from 'react'
import { Rive } from '@rive-app/webgl2'

function RiveToggle() {
  const canvasRef = useRef(null)
  const riveRef = useRef(null)
  const [isOn, setIsOn] = useState(false)
  const inputRef = useRef(null)

  useEffect(() => {
    const r = new Rive({
      src: '/toggle-switch.riv',
      canvas: canvasRef.current,
      autoplay: true,
      stateMachines: 'Toggle Machine',
      onLoad: () => {
        r.resizeDrawingSurfaceToCanvas()
        const inputs = r.stateMachineInputs('Toggle Machine')
        const boolInput = inputs.find((i) => i.name === 'isOn')
        inputRef.current = boolInput
      },
    })
    riveRef.current = r

    return () => {
      r.cleanup()
    }
  }, [])

  const handleClick = () => {
    const newValue = !isOn
    setIsOn(newValue)
    if (inputRef.current) {
      inputRef.current.value = newValue
    }
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        className="rive-canvas"
        width={400}
        height={200}
        onClick={handleClick}
      />
      <p style={{ textAlign: 'center', fontSize: '0.8rem', color: '#666' }}>
        {isOn ? 'ON' : 'OFF'}
      </p>
    </div>
  )
}

export default RiveToggle
import { useRef, useEffect, useState } from 'react'
import { Rive } from '@rive-app/webgl2'

function RiveStatusIcon() {
  const canvasRef = useRef(null)
  const riveRef = useRef(null)
  const statusInputRef = useRef(null)
  const triggerRef = useRef(null)
  const [currentState, setCurrentState] = useState('idle')

  useEffect(() => {
    const r = new Rive({
      src: '/status-icon.riv',
      canvas: canvasRef.current,
      autoplay: true,
      stateMachines: 'Status Machine',
      onLoad: () => {
        r.resizeDrawingSurfaceToCanvas()
        const inputs = r.stateMachineInputs('Status Machine')
        const numberInput = inputs.find((i) => i.name === 'status')
        const trigger = inputs.find((i) => i.name === 'activate')
        statusInputRef.current = numberInput
        triggerRef.current = trigger
      },
      onStateChange: (event) => {
        if (event.data && event.data.length > 0) {
          setCurrentState(event.data[0])
        }
      },
    })
    riveRef.current = r

    return () => {
      r.cleanup()
    }
  }, [])

  const setStatus = (value, label) => {
    if (statusInputRef.current) {
      statusInputRef.current.value = value
    }
    if (triggerRef.current) {
      triggerRef.current.fire()
    }
    setCurrentState(label)
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        className="rive-canvas-large"
        width={400}
        height={400}
      />
      <div className="status-controls">
        <button onClick={() => setStatus(0, 'idle')}>Idle</button>
        <button onClick={() => setStatus(1, 'loading')}>Loading</button>
        <button onClick={() => setStatus(2, 'success')}>Success</button>
        <button onClick={() => setStatus(3, 'error')}>Error</button>
      </div>
      <p className="state-label">State: {currentState}</p>
    </div>
  )
}

export default RiveStatusIcon
import { useState } from 'react'
import AnimatedButton from './components/AnimatedButton'
import ToastManager from './components/ToastManager'
import RiveToggle from './components/RiveToggle'
import RiveStatusIcon from './components/RiveStatusIcon'

function App() {
  return (
    <div>
      <h1>Micro Interactions Showcase</h1>
      <div className="showcase-grid">
        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Spring Button</h2>
          <AnimatedButton />
        </div>

        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Toast Notifications</h2>
          <ToastManager />
        </div>

        <div className="showcase-card">
          <span className="tool-badge">Rive</span>
          <h2>Toggle Switch</h2>
          <RiveToggle />
        </div>

        <div className="showcase-card">
          <span className="tool-badge">Rive</span>
          <h2>Status Icon</h2>
          <RiveStatusIcon />
        </div>
      </div>
    </div>
  )
}

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

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f0f0f;
  color: #ffffff;
  min-height: 100vh;
  padding: 2rem;
}

h1 {
  text-align: center;
  margin-bottom: 2rem;
  font-size: 2rem;
}

.showcase-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

.showcase-card {
  background: #1a1a1a;
  border-radius: 16px;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.showcase-card h2 {
  font-size: 1.2rem;
  color: #e0e0e0;
}

.showcase-card .tool-badge {
  font-size: 0.75rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: #2a2a2a;
  color: #888;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.toast-container {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 1000;
}

.toast {
  background: #2d2d2d;
  color: #fff;
  padding: 1rem 1.5rem;
  border-radius: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  font-size: 0.9rem;
}

.rive-canvas {
  width: 200px;
  height: 100px;
  cursor: pointer;
}

.rive-canvas-large {
  width: 200px;
  height: 200px;
}

.status-controls {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: center;
}

.status-controls button {
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px solid #333;
  background: #1a1a1a;
  color: #ccc;
  cursor: pointer;
  font-size: 0.8rem;
}

.status-controls button:hover {
  background: #2a2a2a;
}

.state-label {
  font-size: 0.8rem;
  color: #666;
  margin-top: 0.5rem;
}

Both Rive animations are live and responding to user input. Next up, you'll compose the full gallery layout and polish the showcase page for a complete end-to-end demo.

Compose the Showcase Gallery

You've built four distinct micro interaction components across two different animation tools. Each one lives in its own file, handles its own state, and demonstrates a unique motion design principle.

Now it's time to finalize the gallery that brings them all together and run a full end-to-end test to confirm every interaction works as expected.

In this step, get ready to:
  • Finalize the App.jsx gallery layout with all imports.
  • Verify the status icon's onStateChange callback displays the current animation state.
  • Test all four interactions end-to-end in the browser.
Finalize the gallery layout in App.jsx

Your App.jsx already composes all four components into a grid of showcase cards. Each card includes a tool-badge labeling whether it's powered by Motion or Rive, plus a descriptive heading.

Let's add one final import to complete the file.

  • In src/App.jsx, find the first line of imports at the top of the file:
import AnimatedButton from './components/AnimatedButton'
  • Add this line directly above it, so it becomes the very first import:
import { useState } from 'react'

Why import useState here?

The gallery component imports useState from React to keep the file ready for future state additions (like a global theme toggle or active-card tracking). Including it now ensures your imports are complete when you extend the showcase later.

  • Save App.jsx.
  • Refresh your browser at http://localhost:5173. You should see the full gallery grid with four dark cards, each showing a tool-badge and heading above its interactive component.

Don't see all four cards?

Make sure all four component files exist in src/components/. If a card shows a blank area instead of an interactive element, check the browser console for import errors pointing to a missing file.

Help me debug missing components in my showcase gallery.

✔️ 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

import { useState } from 'react'
import AnimatedButton from './components/AnimatedButton'
import ToastManager from './components/ToastManager'
import RiveToggle from './components/RiveToggle'
import RiveStatusIcon from './components/RiveStatusIcon'

function App() {
  return (
    <div>
      <h1>Micro Interactions Showcase</h1>
      <div className="showcase-grid">
        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Spring Button</h2>
          <AnimatedButton />
        </div>

        <div className="showcase-card">
          <span className="tool-badge">Motion</span>
          <h2>Toast Notifications</h2>
          <ToastManager />
        </div>

        <div className="showcase-card">
          <span className="tool-badge">Rive</span>
          <h2>Toggle Switch</h2>
          <RiveToggle />
        </div>

        <div className="showcase-card">
          <span className="tool-badge">Rive</span>
          <h2>Status Icon</h2>
          <RiveStatusIcon />
        </div>
      </div>
    </div>
  )
}

export default App
Verify the onStateChange callback on the status icon

The RiveStatusIcon component includes an onStateChange callback that fires whenever the Rive state machine transitions to a new animation state. This callback updates a React state variable called currentState, which displays the active state name below the icon.

  • In your browser, find the **Status Icon** card in the gallery.
  • Click the Loading button below the status icon canvas.
  • Watch the State: label update to show loading.
  • Click Success, then Error, then Idle. Confirm the label updates each time.

How does onStateChange work?

When you set the status number input and fire the activate trigger, the Rive state machine transitions to the matching animation state. The onStateChange callback receives an event whose event.data array contains the name of the new state as a string.

Your component reads that string and passes it to setCurrentState, which re-renders the state-label paragraph below the canvas. This bridges Rive's internal animation world with React's UI layer.

State label not updating?

If the label stays stuck on idle after clicking buttons, check that your Rive state machine states are named exactly as expected in the editor. The onStateChange event reports whatever names you gave the states in the Rive timeline.

Also confirm your state machine is named Status Machine (with the capital S and M). A mismatch means the runtime can't find the machine to attach the callback to.

Help me debug my Rive onStateChange callback.

Test all interactions end-to-end

With all four components composed into the gallery, run through each interaction to confirm everything works together without conflicts.

  • Hover over the Click Me button. It should scale up smoothly with spring physics.
  • Click the button. It should scale down and rotate slightly, then spring back.
  • Click Trigger Toast three times quickly. Three toasts should slide in from the bottom-right, then auto-dismiss after 3 seconds with a smooth exit animation.
  • Click the toggle switch canvas. The knob should animate from left to right and the label should switch between OFF and ON.
  • Cycle through all four status icon states using the buttons. Confirm the canvas animation changes and the state label updates each time.

What should I see?

Your page should display a responsive grid of four dark cards. Each card has a small grey badge at the top (MOTION or RIVE), a heading, and an interactive element below.

All animations should feel smooth and responsive. Spring animations should bounce naturally without stuttering. Toast notifications should stack and dismiss without jumping. Rive canvases should render crisply without pixelation.

Rive canvases look pixelated or blurry?

Make sure resizeDrawingSurfaceToCanvas() is called inside the onLoad callback in both RiveToggle.jsx and RiveStatusIcon.jsx. This method adjusts the canvas drawing surface to match your display's pixel density for crisp rendering on high-DPI screens.

Help me fix blurry Rive canvases in my React app.

Toasts appear but don't animate out?

Check that each toast motion.div has a unique key prop set to toast.id. AnimatePresence needs unique keys to track which elements are entering, present, and exiting. Without them, exit animations can't fire.

Help me debug my AnimatePresence exit animations.

Your micro interactions showcase is complete. You've composed code-driven animations (Motion) and designer-authored animations (Rive) into a single polished gallery. Next up, a secret mission that adds one more animation technique to your toolkit.

Secret mission

Add a Draggable Card with Elastic Constraints

Your showcase has spring buttons, toast notifications, and Rive animations. But what about direct manipulation? In this secret mission, you'll add a draggable card that bounces off invisible walls with rubber-band physics and momentum.

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, so there are no ongoing costs.

Resources you used:

  • Vite + React project folder on your Desktop.
  • Node modules (node_modules folder inside your project).
  • Two .riv files in your public folder.
  • Rive account (free, browser-based at rive.app).
  • Vite dev server (running process in your terminal).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep experimenting with your micro interactions.

  • Your Vite dev server stays running in the terminal.
  • Your Rive files remain in the public folder, ready to edit and re-export from the Rive editor.
  • Your Rive account stays active for future projects.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files stay on disk so you can pick up where you left off.

  • In the terminal running your dev server, press Ctrl+C to stop Vite.
  • Close VS Code if you want to free additional memory.
  • When you're ready to resume, open the project folder in VS Code and run npm run dev again.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your local project files.

  • In the terminal running your dev server, press Ctrl+C to stop Vite.
  • Delete the entire project folder from your Desktop.
  • If you want to remove your Rive files from the cloud, log into rive.app and delete the two files you created.
  • Your Rive account is free, so there is no need to delete it unless you prefer to.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete micro interactions showcase from scratch, proving you can implement both code-driven and designer-authored animations in a real React project.

You've learned how to:

  • Built interactive buttons with spring physics using Motion's whileHover and whileTap gesture animations.
  • Created a toast notification system with smooth enter/exit transitions using AnimatePresence and layout animations.
  • Designed custom Rive animations with state machines and integrated them into React using the vanilla web runtime with canvas, useRef, and useEffect.
  • Secret Mission: Added a draggable card with elastic constraints, momentum physics, and visual feedback using Motion's drag API.

Ready to quiz yourself?