Dirck Mulder
Components||3 min read

Build an Animated List in React

Add staggered entrance animations to any list so items appear one by one with satisfying timing.

You have a list of ten items and they all just appear at once. Nobody reads them in order, nobody notices them arriving. Add a staggered entrance and suddenly the list feels like it is telling you something.

The final result

A scrollable list where each item animates in with a scale and opacity transition when it enters the viewport. Items react to hover and click, and keyboard navigation is fully supported.

Setting up

Install the dependencies:

bash
npm install motion

Import what you need:

tsx
import { useRef, useState, useEffect, useCallback } from 'react';
import { motion, useInView } from 'motion/react';

Building the component

We split this into two parts: an AnimatedItem wrapper and the AnimatedList container.

The AnimatedItem

Each item uses useInView to detect when it is at least halfway visible. The animation triggers on entry and reverses on exit because once: false is set.

tsx
const AnimatedItem: React.FC<AnimatedItemProps> = ({
  children,
  delay = 0,
  index,
  onMouseEnter,
  onClick,
}) => {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { amount: 0.5, once: false });
  return (
    <motion.div
      ref={ref}
      data-index={index}
      onMouseEnter={onMouseEnter}
      onClick={onClick}
      initial={{ scale: 0.7, opacity: 0 }}
      animate={inView ? { scale: 1, opacity: 1 } : { scale: 0.7, opacity: 0 }}
      transition={{ duration: 0.2, delay }}
      className='mb-4 cursor-pointer'
    >
      {children}
    </motion.div>
  );
};

Scroll-aware gradients

The container tracks scroll position to fade in top and bottom gradients so the list never looks like it cuts off abruptly:

tsx
const handleScroll = (e: UIEvent<HTMLDivElement>) => {
  const { scrollTop, scrollHeight, clientHeight } =
    e.target as HTMLDivElement;
  setTopGradientOpacity(Math.min(scrollTop / 50, 1));
  const bottomDistance = scrollHeight - (scrollTop + clientHeight);
  setBottomGradientOpacity(
    scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1)
  );
};

Keyboard navigation

Arrow keys and Tab move through items. When using keyboard nav, the selected item scrolls into view with a bit of extra margin:

tsx
useEffect(() => {
  if (!enableArrowNavigation) return;
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {
      e.preventDefault();
      setKeyboardNav(true);
      setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));
    } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {
      e.preventDefault();
      setKeyboardNav(true);
      setSelectedIndex(prev => Math.max(prev - 1, 0));
    }
  };
  window.addEventListener('keydown', handleKeyDown);
  return () => window.removeEventListener('keydown', handleKeyDown);
}, [items, selectedIndex, onItemSelect, enableArrowNavigation]);

How to use it

tsx
<AnimatedList
  items={['Notification 1', 'Notification 2', 'Notification 3']}
  onItemSelect={(item, index) => console.log(item, index)}
  showGradients={true}
  enableArrowNavigation={true}
  displayScrollbar={true}
  initialSelectedIndex={-1}
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | items | string[] | 15 placeholder items | The list content | | onItemSelect | function | - | Fires on click or Enter | | showGradients | boolean | true | Top and bottom fade overlays | | enableArrowNavigation | boolean | true | Keyboard support | | displayScrollbar | boolean | true | Show or hide the scrollbar |

Key takeaways

  • useInView with once: false lets items animate both in and out as you scroll, keeping the list feeling alive at any position.
  • The gradient overlays are cosmetic but do real work: they communicate that the list continues without needing a scrollbar in view at all times.
  • Splitting the component into AnimatedItem and AnimatedList keeps each piece testable in isolation.