empty-states

برای وضعیت‌های خالی، بدون نتیجه، آفلاین و خطا، موشن‌های ظریف و هدفمند بسازید.

این Skill الگوهای انیمیشن برای وضعیت‌های empty، null و zero-data را ارائه می‌دهد؛ از نمایش نخستین تجربه و نتیجه‌نداشتن جست‌وجو تا حالت آفلاین، خطا و حذف همه محتوا. نمونه‌های آماده برای CSS، GSAP، Framer Motion و Lottie شامل ورود تدریجی، شناورشدن، pulse، stagger و جایگزینی نرم محتوا هستند.

با این الگوها وضعیت خالی فقط یک صفحه بی‌محتوا نیست و کاربر مسیر بعدی را بهتر می‌فهمد. راهنما همچنین به prefers-reduced-motion، عملکرد، زمان‌بندی و توقف انیمیشن خارج از viewport توجه می‌کند تا تجربه‌ای روان و دسترس‌پذیر بسازید.

چه چیزی می‌گیرید

  • الگوهای موشن برای first-time، no results، cleared، error و offline state
  • کدهای آماده CSS، GSAP و Framer Motion برای ورود، خروج و stagger
  • الگوهای gentle float، breathing pulse، rotation و fade + scale
  • نمونه جایگزینی نرم empty state با فهرست واقعی محتوا
  • راهنمای Lottie، رعایت reduced motion و بهینه‌سازی اجرای انیمیشن

برای چه کاری خوب است

  • طراحان محصول و توسعه‌دهندگان UI برای فهرست‌ها، داشبوردها و جست‌وجو
  • اپلیکیشن‌هایی که حالت‌های بدون داده، آفلاین یا خطا باید راهنمایانه و انسانی باشند
  • پروژه‌های React، Framer Motion، GSAP، CSS یا Lottie که به موشن‌های آماده نیاز دارند

برای چه کاری نیست

  • ساخت انیمیشن‌های پیچیده سینمایی، تبلیغاتی یا موشن‌گرافیک مستقل
  • صفحه‌هایی که به انیمیشن دائمی و جلب توجه شدید نیاز دارند
  • وقتی یک empty state ساده و بدون حرکت برای محصول کافی است

دربارهٔ این اسکیل

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

دانلود ZIP