Storybook is the standard tool for documenting React (and Vue/Svelte) components. Adding Lottie animations to your component library requires a few specific patterns to make stories interactive and reusable. This guide covers every approach.
Before You Start
Verify your animation files at IconKing before adding them to your design system:
- Confirm exact colors match your design tokens
- Verify timing and loop behavior
- Convert
.jsonâ.lottiefor 75% smaller files in your Storybook build
Installation
npm install lottie-react
# or
npm install @lottiefiles/dotlottie-react
Basic Story Setup
The simplest Storybook story for a Lottie animation component:
// src/components/LottieIcon/LottieIcon.tsx
import Lottie from 'lottie-react';
interface LottieIconProps {
animationData: object;
size?: number;
loop?: boolean;
autoplay?: boolean;
}
export function LottieIcon({
animationData,
size = 48,
loop = false,
autoplay = true,
}: LottieIconProps) {
return (
<div style={{ width: size, height: size }}>
<Lottie animationData={animationData} loop={loop} autoplay={autoplay} />
</div>
);
}
// src/components/LottieIcon/LottieIcon.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { LottieIcon } from './LottieIcon';
import heartAnim from '../../animations/heart.json';
import checkAnim from '../../animations/checkmark.json';
import loadingAnim from '../../animations/loading.json';
const meta: Meta<typeof LottieIcon> = {
title: 'Animations/LottieIcon',
component: LottieIcon,
parameters: {
layout: 'centered',
},
argTypes: {
size: {
control: { type: 'range', min: 24, max: 200, step: 8 },
},
loop: {
control: 'boolean',
},
autoplay: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<typeof LottieIcon>;
export const Heart: Story = {
args: {
animationData: heartAnim,
size: 64,
loop: true,
autoplay: true,
},
};
export const Checkmark: Story = {
args: {
animationData: checkAnim,
size: 64,
loop: false,
autoplay: true,
},
};
export const Loading: Story = {
args: {
animationData: loadingAnim,
size: 48,
loop: true,
autoplay: true,
},
};
Controlled Animation Story
Add play/pause controls using Storybook's useArgs hook:
// src/components/AnimatedButton/AnimatedButton.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';
import { AnimatedButton } from './AnimatedButton';
const meta: Meta<typeof AnimatedButton> = {
title: 'Animations/AnimatedButton',
component: AnimatedButton,
parameters: {
layout: 'centered',
},
};
export default meta;
export const Interactive: StoryObj<typeof AnimatedButton> = {
args: {
playing: false,
label: 'Click to animate',
},
render: function Render(args) {
const [{ playing }, updateArgs] = useArgs();
return (
<AnimatedButton
{...args}
playing={playing}
onClick={() => updateArgs({ playing: !playing })}
/>
);
},
};
Animation Gallery Story
Document all animations in your library on a single story page:
// src/stories/AnimationGallery.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import Lottie from 'lottie-react';
// Import all animation files
import heartAnim from '../animations/heart.json';
import checkAnim from '../animations/checkmark.json';
import loadingAnim from '../animations/loading.json';
import errorAnim from '../animations/error.json';
import successAnim from '../animations/success.json';
import arrowAnim from '../animations/arrow.json';
const animations = [
{ name: 'Heart', data: heartAnim },
{ name: 'Checkmark', data: checkAnim },
{ name: 'Loading', data: loadingAnim },
{ name: 'Error', data: errorAnim },
{ name: 'Success', data: successAnim },
{ name: 'Arrow', data: arrowAnim },
];
function AnimationGallery({ size = 64, loop = true, autoplay = true }) {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24, padding: 24 }}>
{animations.map(({ name, data }) => (
<div key={name} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<div style={{ width: size, height: size }}>
<Lottie animationData={data} loop={loop} autoplay={autoplay} />
</div>
<span style={{ fontSize: 12, color: '#666' }}>{name}</span>
</div>
))}
</div>
);
}
const meta: Meta<typeof AnimationGallery> = {
title: 'Animations/Gallery',
component: AnimationGallery,
parameters: {
layout: 'padded',
},
argTypes: {
size: { control: { type: 'range', min: 32, max: 128, step: 8 } },
loop: { control: 'boolean' },
autoplay: { control: 'boolean' },
},
};
export default meta;
export const Default: StoryObj<typeof AnimationGallery> = {
args: {
size: 64,
loop: true,
autoplay: true,
},
};
Play Function for Animation Timing Tests
Storybook's play function lets you write interaction tests for animations:
import type { Meta, StoryObj } from '@storybook/react';
import { expect, within, userEvent, waitFor } from '@storybook/test';
import { StatusAnimation } from './StatusAnimation';
const meta: Meta<typeof StatusAnimation> = {
title: 'Animations/StatusAnimation',
component: StatusAnimation,
};
export default meta;
export const ShowsSuccess: StoryObj<typeof StatusAnimation> = {
args: { status: 'loading' },
play: async ({ canvasElement, args, updateArgs }) => {
const canvas = within(canvasElement);
// Verify loading state renders
const container = canvas.getByRole('status');
await expect(container).toHaveAttribute('aria-label', 'Loading, please wait');
// Simulate state change to success
updateArgs({ status: 'success' });
await waitFor(() => {
expect(container).toHaveAttribute('aria-label', 'Successfully completed');
});
},
};
Decorator for Animation Background
Add a consistent background for animation stories:
// .storybook/preview.tsx
import type { Preview } from '@storybook/react';
const preview: Preview = {
decorators: [
(Story, context) => {
// Apply dark or light background based on the story's background parameter
const bg = context.parameters.animationBg ?? 'white';
return (
<div style={{ background: bg, padding: 24, borderRadius: 8 }}>
<Story />
</div>
);
},
],
};
export default preview;
// In a story that needs a dark background (e.g., white animations)
export const OnDark: Story = {
parameters: {
animationBg: '#1a1a2e',
},
args: {
animationData: whiteIconAnim,
},
};
Static Snapshot Story (CI-Safe)
For visual regression testing in CI, render a static frame instead of the live animation:
export const StaticSnapshot: Story = {
args: {
animationData: heroAnim,
loop: false,
autoplay: false,
},
parameters: {
// Chromatic-specific: pause at first frame for snapshot
chromatic: { pauseAnimationAtEnd: true },
},
};
Storybook + MDX Docs Page
Create a documentation page describing your animation system:
<!-- src/stories/AnimationSystem.mdx -->
import { Canvas, Meta, Story } from '@storybook/blocks';
import * as LottieIconStories from '../components/LottieIcon/LottieIcon.stories';
<Meta title="Design System/Animation System" />
# Animation System
All animations are Lottie files, previewed and converted at [IconKing](https://iconking.net).
## Icon Animations
Used for interactive states (hover, press, success) on buttons and form elements.
<Canvas of={LottieIconStories.Heart} />
## Usage Guidelines
- Use `.lottie` format â 75% smaller than `.json`
- Respect `prefers-reduced-motion` â don't autoplay for users who prefer reduced motion
- Decorative animations must have `aria-hidden="true"`
- Meaningful animations need `role="img"` + `aria-label`
Summary
- Use
argTypeswithcontrol: 'range'for the size slider â makes sizing instantly testable - Build a Gallery story to document all animations in one place
- Use Storybook's
playfunction to test state transitions (loading â success â error) - Add
chromatic: { pauseAnimationAtEnd: true }for stable visual regression snapshots - Preview and convert all animation files at IconKing before adding them to your component library
Top comments (0)