DEV Community

Fazal Shah
Fazal Shah

Posted on

Documenting Lottie Animations in Storybook

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 → .lottie for 75% smaller files in your Storybook build

Installation

npm install lottie-react
# or
npm install @lottiefiles/dotlottie-react
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
// 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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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 })}
      />
    );
  },
};
Enter fullscreen mode Exit fullscreen mode

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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');
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// In a story that needs a dark background (e.g., white animations)
export const OnDark: Story = {
  parameters: {
    animationBg: '#1a1a2e',
  },
  args: {
    animationData: whiteIconAnim,
  },
};
Enter fullscreen mode Exit fullscreen mode

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 },
  },
};
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

Summary

  1. Use argTypes with control: 'range' for the size slider — makes sizing instantly testable
  2. Build a Gallery story to document all animations in one place
  3. Use Storybook's play function to test state transitions (loading → success → error)
  4. Add chromatic: { pauseAnimationAtEnd: true } for stable visual regression snapshots
  5. Preview and convert all animation files at IconKing before adding them to your component library

Top comments (0)