agentleFS
soilmasssoilmass/motion-design-agent/skills/empty-states/SKILL.md

empty-states

Skill7 starsChanged 10 months agoMITView on GitHub

Animation patterns for empty, null, and zero-data states. Use when designing animations for empty lists, no search results, first-time empty views, or content placeholders.

5 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Files
SKILL.md

Empty State Animations

Motion patterns for empty, null, and zero-data UI states.

Empty State Types

TypeContextAnimation Style
First-timeNew user, no data yetWelcoming, guiding
No resultsSearch/filter returned emptyHelpful, suggestive
ClearedUser deleted all itemsAcknowledging, light
ErrorFailed to load dataReassuring, retry-focused
OfflineNo network connectionInformative, patient

Illustration Animation

Gentle Float

/* Subtle floating motion */
@keyframes gentle-float {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

.empty-illustration {
  animation: gentle-float 3s ease-in-out infinite;
}

/* Reduced motion: static */
@media (prefers-reduced-motion: reduce) {
  .empty-illustration {
    animation: none;
  }
}

Breathing Pulse

@keyframes breathe {
  0%, 100% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.02);
    opacity: 0.9;
  }
}

.empty-icon {
  animation: breathe 2.5s ease-in-out infinite;
}

Subtle Rotation

// For circular or balanced illustrations
gsap.to('.empty-illustration', {
  rotation: 360,
  duration: 20,
  repeat: -1,
  ease: 'none'
});

Entrance Animation

Fade + Scale

// Primary entrance for empty states
const emptyStateEntrance = {
  initial: { opacity: 0, scale: 0.95 },
  animate: { opacity: 1, scale: 1 },
  transition: {
    duration: 0.4,
    ease: [0.05, 0.7, 0.1, 1]
  }
};

// Staggered content
const containerVariants = {
  animate: {
    transition: {
      staggerChildren: 0.1
    }
  }
};

const itemVariants = {
  initial: { opacity: 0, y: 20 },
  animate: { opacity: 1, y: 0 }
};

Illustration First

function EmptyState({ illustration, title, description, action }) {
  return (
    <motion.div variants={containerVariants} initial="initial" animate="animate">
      <motion.div
        variants={itemVariants}
        transition={{ duration: 0.5 }}
      >
        {illustration}
      </motion.div>

      <motion.h3
        variants={itemVariants}
        transition={{ duration: 0.3, delay: 0.2 }}
      >
        {title}
      </motion.h3>

      <motion.p
        variants={itemVariants}
        transition={{ duration: 0.3, delay: 0.3 }}
      >
        {description}
      </motion.p>

      <motion.button
        variants={itemVariants}
        transition={{ duration: 0.3, delay: 0.4 }}
        whileHover={{ scale: 1.02 }}
        whileTap={{ scale: 0.98 }}
      >
        {action}
      </motion.button>
    </motion.div>
  );
}

Context-Specific Patterns

No Search Results

// Emphasize search bar, suggest alternatives
const noResultsAnimation = {
  illustration: {
    initial: { opacity: 0, scale: 0.9 },
    animate: {
      opacity: 1,
      scale: 1,
      transition: { duration: 0.4 }
    }
  },
  searchHighlight: {
    animate: {
      boxShadow: [
        '0 0 0 0 rgba(59, 130, 246, 0)',
        '0 0 0 4px rgba(59, 130, 246, 0.3)',
        '0 0 0 0 rgba(59, 130, 246, 0)'
      ],
      transition: {
        duration: 1.5,
        delay: 0.5,
        repeat: 2
      }
    }
  }
};

First-Time Empty

// Welcoming, guide to action
const firstTimeAnimation = {
  container: {
    animate: {
      transition: { staggerChildren: 0.15 }
    }
  },
  illustration: {
    initial: { opacity: 0, y: -20 },
    animate: {
      opacity: 1,
      y: 0,
      transition: {
        type: 'spring',
        damping: 15
      }
    }
  },
  ctaButton: {
    animate: {
      scale: [1, 1.02, 1],
      transition: {
        delay: 1,
        duration: 0.5,
        repeat: 2,
        repeatDelay: 2
      }
    }
  }
};

Error State

// Reassuring, focus on retry
const errorAnimation = {
  icon: {
    animate: {
      rotate: [0, -5, 5, 0],
      transition: {
        duration: 0.4,
        delay: 0.3
      }
    }
  },
  retryButton: {
    whileHover: {
      scale: 1.02,
      transition: { duration: 0.2 }
    },
    whileTap: { scale: 0.98 }
  }
};

Offline State

// Patient, informative
const offlineAnimation = {
  cloudIcon: {
    animate: {
      opacity: [1, 0.5, 1],
      transition: {
        duration: 2,
        repeat: Infinity
      }
    }
  },
  connectionLine: {
    animate: {
      strokeDashoffset: [0, 20],
      transition: {
        duration: 1,
        repeat: Infinity,
        ease: 'linear'
      }
    }
  }
};

Transition to Content

Content Arrives

function ListWithEmpty({ items }) {
  return (
    <AnimatePresence mode="wait">
      {items.length === 0 ? (
        <motion.div
          key="empty"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0, scale: 0.95 }}
          transition={{ duration: 0.3 }}
        >
          <EmptyState />
        </motion.div>
      ) : (
        <motion.div
          key="content"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          transition={{ duration: 0.3 }}
        >
          <ItemList items={items} />
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Smooth Replacement

// Empty state fades out as content fades in
const exitTransition = {
  exit: {
    opacity: 0,
    y: -20,
    transition: { duration: 0.2 }
  }
};

const enterTransition = {
  initial: { opacity: 0, y: 20 },
  animate: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.3,
      delay: 0.1  // Slight delay after empty exits
    }
  }
};

Lottie Integration

import Lottie from 'lottie-react';
import emptyAnimation from './empty-state.json';

function EmptyStateWithLottie() {
  return (
    <div className="empty-state">
      <Lottie
        animationData={emptyAnimation}
        loop={true}
        autoplay={true}
        style={{ width: 200, height: 200 }}
      />
      <h3>No items yet</h3>
      <p>Create your first item to get started</p>
    </div>
  );
}

Performance Considerations

// Pause animation when not visible
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
  const observer = new IntersectionObserver(
    ([entry]) => setIsVisible(entry.isIntersecting)
  );
  observer.observe(emptyStateRef.current);
  return () => observer.disconnect();
}, []);

// Only animate when visible
<Lottie
  animationData={animation}
  loop={isVisible}
  autoplay={isVisible}
/>

Timing Guidelines

ElementDurationNotes
Illustration entrance400-500msSlightly longer for visual impact
Text entrance200-300msFast, readable
Button entrance200msQuick, actionable
Stagger delay100-150msBetween elements
Loop animation2-3s cycleGentle, not distracting

Related Skills

  • Dependencies: easing, duration, entrance-patterns
  • See also: loading-states, error-states, onboarding
  • Patterns: Float, breathe, pulse, stagger entrance

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.