How to Add Animated Icons to Your React App
A step-by-step guide to integrating Rive and Lottie animated icons into a React application. Includes code examples, performance tips, and common pitfalls.
Adding animated icons to a React app is straightforward once you understand the available libraries and patterns. This guide walks through integrating both Rive and Lottie icons, with practical code examples and tips for keeping your implementation performant.
Prerequisites
This guide assumes you have a React project (React 18+, Next.js 14+, or a Vite-based setup). You will need animated icon files — you can download .riv and .lottie files from Unicorn Icons.
Option 1: Rive Animated Icons in React
Rive's React library provides hooks and components for rendering .riv files with full state machine support.
Installation
bashcopynpm install @rive-app/react-canvas
Basic Usage
The simplest way to display a Rive animation is with the useRive hook:
tsxcopyimport { useRive } from "@rive-app/react-canvas"; export function CheckmarkIcon() { const { RiveComponent } = useRive({ src: "/icons/checkmark.riv", stateMachines: "State Machine 1", autoplay: true, }); return <RiveComponent style={{ width: 48, height: 48 }} />; }
Controlling State Machines
To drive a state machine from your application code, use the useStateMachineInput hook to get references to the state machine inputs:
tsxcopyimport { useRive, useStateMachineInput } from "@rive-app/react-canvas"; export function SubmitButton({ isLoading, isSuccess }) { const { RiveComponent, rive } = useRive({ src: "/icons/submit-button.riv", stateMachines: "ButtonState", autoplay: true, }); const loadingInput = useStateMachineInput(rive, "ButtonState", "loading"); const successInput = useStateMachineInput(rive, "ButtonState", "success"); // Drive the state machine from props React.useEffect(() => { if (loadingInput) loadingInput.value = isLoading; }, [isLoading, loadingInput]); React.useEffect(() => { if (successInput) successInput.value = isSuccess; }, [isSuccess, successInput]); return ( <button type="submit"> <RiveComponent style={{ width: 24, height: 24 }} /> Submit </button> ); }
Performance: Shared Canvas Renderer
When rendering multiple Rive animations on the same page, use the @rive-app/react-canvas-lite package and share a single WebGL context across instances to minimize GPU memory usage:
bashcopynpm install @rive-app/react-canvas-lite
Then wrap your app in the RiveProvider component to enable context sharing.
Option 2: Lottie Animated Icons in React
For Lottie icons, @lottiefiles/dotlottie-react is the recommended library in 2026 as it supports the newer .lottie format natively.
Installation
bashcopynpm install @lottiefiles/dotlottie-react
Basic Usage
tsxcopyimport { DotLottieReact } from "@lottiefiles/dotlottie-react"; export function LoadingSpinner() { return ( <DotLottieReact src="/icons/spinner.lottie" loop autoplay style={{ width: 48, height: 48 }} /> ); }
Playback Control
To control playback from your application (e.g., pause when a task completes):
tsxcopyimport { DotLottieReact } from "@lottiefiles/dotlottie-react"; import { useState, useCallback } from "react"; export function TaskIcon({ isComplete }) { const [dotLottie, setDotLottie] = useState(null); const dotLottieRefCallback = useCallback((ref) => { setDotLottie(ref); }, []); React.useEffect(() => { if (dotLottie) { if (isComplete) { dotLottie.pause(); } else { dotLottie.play(); } } }, [isComplete, dotLottie]); return ( <DotLottieReact src="/icons/task.lottie" dotLottieRefCallback={dotLottieRefCallback} loop autoplay style={{ width: 24, height: 24 }} /> ); }
Handling Reduced Motion
Always respect the user's prefers-reduced-motion preference. In React, you can use a custom hook:
tsxcopyfunction usePrefersReducedMotion() { const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); useEffect(() => { const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); setPrefersReducedMotion(mq.matches); const handler = (e) => setPrefersReducedMotion(e.matches); mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, []); return prefersReducedMotion; } // Usage: function AnimatedIcon() { const reducedMotion = usePrefersReducedMotion(); if (reducedMotion) return <img src="/icons/static-icon.svg" alt="" />; return <RiveComponent style={{ width: 48, height: 48 }} />; }
Lazy Loading Icons
For icons below the fold, defer loading using React.lazy or IntersectionObserver to avoid loading animation runtimes until they are needed:
tsxcopyconst AnimatedIcon = React.lazy(() => import("./AnimatedIcon")); // Wrap in Suspense with a static fallback <Suspense fallback={<img src="/icons/static.svg" alt="" />}> <AnimatedIcon /> </Suspense>
Common Pitfalls
- Loading the runtime on every page — only import Rive or Lottie in components that actually use animations. Tree-shaking and dynamic imports prevent unnecessary bundle bloat.
- Forgetting to set explicit dimensions — Rive and Lottie canvases without explicit width/height will default to their artboard dimensions, which may not match your layout. Always set
style={{ width, height }}. - Not unloading on unmount — both libraries handle cleanup automatically when components unmount, but ensure you're not holding references to rive/lottie instances in closures that outlive the component.
Next Steps
Browse the Unicorn Icons library to find animated icons for your project. For platform-specific guides, see React + Rive and React + Lottie.