UI Components Easy
Animated List
A list component where items animate in with staggered entrance effects — slide and fade from alternating directions for a dynamic reveal.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(0.75rem, 3vw, 2rem);
}
.list-wrapper {
width: min(520px, 100%);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.list-title {
font-size: 1.375rem;
font-weight: 700;
color: #f1f5f9;
}
.list-subtitle {
font-size: 0.875rem;
color: #64748b;
margin-bottom: 1rem;
}
/* ── List ── */
.animated-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* ── Item ── */
.animated-list-item {
display: flex;
align-items: center;
gap: clamp(0.5rem, 2vw, 0.875rem);
padding: clamp(0.625rem, 2vw, 0.875rem) clamp(0.75rem, 2vw, 1rem);
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 0.875rem;
opacity: 0;
transform: translateX(-30px);
transition: background 0.2s ease;
}
.animated-list-item[data-direction="right"] {
transform: translateX(30px);
}
/* ── Visible state ── */
.animated-list-item.visible {
animation: slide-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards;
animation-delay: calc(var(--i, 0) * 0.08s);
}
.animated-list-item[data-direction="right"].visible {
animation-name: slide-in-right;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slide-in-right {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animated-list-item:hover {
background: rgba(255, 255, 255, 0.06);
}
/* ── Icon ── */
.item-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 0.625rem;
display: grid;
place-items: center;
background: rgba(34, 197, 94, 0.12);
color: #4ade80;
}
.item-icon--blue {
background: rgba(59, 130, 246, 0.12);
color: #60a5fa;
}
.item-icon--amber {
background: rgba(245, 158, 11, 0.12);
color: #fbbf24;
}
.item-icon--purple {
background: rgba(168, 85, 247, 0.12);
color: #c084fc;
}
.item-icon--green {
background: rgba(34, 197, 94, 0.12);
color: #4ade80;
}
.item-icon--rose {
background: rgba(244, 63, 94, 0.12);
color: #fb7185;
}
/* ── Content ── */
.item-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.item-content strong {
font-size: 0.875rem;
font-weight: 600;
color: #e2e8f0;
}
.item-content span {
font-size: 0.8rem;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-time {
flex-shrink: 0;
font-size: 0.75rem;
color: #475569;
}
/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
.animated-list-item {
opacity: 1;
transform: none;
}
.animated-list-item.visible {
animation: none;
}
}(function () {
"use strict";
const list = document.getElementById("animated-list");
if (!list) return;
const items = list.querySelectorAll(".animated-list-item");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.1,
rootMargin: "0px 0px -40px 0px",
}
);
items.forEach((item) => observer.observe(item));
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Animated List</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="list-wrapper">
<h2 class="list-title">Notifications</h2>
<p class="list-subtitle">Scroll down or watch them appear</p>
<ul class="animated-list" id="animated-list">
<li class="animated-list-item" style="--i: 0" data-direction="left">
<div class="item-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="M22 4 12 14.01l-3-3"/></svg>
</div>
<div class="item-content">
<strong>Build successful</strong>
<span>Your project compiled without errors</span>
</div>
<span class="item-time">2m ago</span>
</li>
<li class="animated-list-item" style="--i: 1" data-direction="right">
<div class="item-icon item-icon--blue">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<div class="item-content">
<strong>New comment</strong>
<span>Alex left feedback on your PR</span>
</div>
<span class="item-time">5m ago</span>
</li>
<li class="animated-list-item" style="--i: 2" data-direction="left">
<div class="item-icon item-icon--amber">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="item-content">
<strong>Disk usage warning</strong>
<span>Storage is at 85% capacity</span>
</div>
<span class="item-time">12m ago</span>
</li>
<li class="animated-list-item" style="--i: 3" data-direction="right">
<div class="item-icon item-icon--purple">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
</div>
<div class="item-content">
<strong>New team member</strong>
<span>Jordan joined the engineering team</span>
</div>
<span class="item-time">1h ago</span>
</li>
<li class="animated-list-item" style="--i: 4" data-direction="left">
<div class="item-icon item-icon--green">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<div class="item-content">
<strong>Security update</strong>
<span>All dependencies patched successfully</span>
</div>
<span class="item-time">3h ago</span>
</li>
<li class="animated-list-item" style="--i: 5" data-direction="right">
<div class="item-icon item-icon--rose">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</div>
<div class="item-content">
<strong>Milestone reached</strong>
<span>Project hit 10,000 stars on GitHub</span>
</div>
<span class="item-time">1d ago</span>
</li>
</ul>
</div>
<script src="script.js"></script>
</body>
</html>import { useEffect, useRef, useState, useCallback } from "react";
interface AnimatedListItem {
id: string;
icon?: React.ReactNode;
title: string;
description: string;
time?: string;
iconColor?: string;
iconBg?: string;
}
interface AnimatedListProps {
items?: AnimatedListItem[];
staggerDelay?: number;
}
const defaultItems: AnimatedListItem[] = [
{
id: "1",
title: "Build successful",
description: "Your project compiled without errors",
time: "2m ago",
iconColor: "#4ade80",
iconBg: "rgba(34,197,94,0.12)",
},
{
id: "2",
title: "New comment",
description: "Alex left feedback on your PR",
time: "5m ago",
iconColor: "#60a5fa",
iconBg: "rgba(59,130,246,0.12)",
},
{
id: "3",
title: "Disk usage warning",
description: "Storage is at 85% capacity",
time: "12m ago",
iconColor: "#fbbf24",
iconBg: "rgba(245,158,11,0.12)",
},
{
id: "4",
title: "New team member",
description: "Jordan joined the engineering team",
time: "1h ago",
iconColor: "#c084fc",
iconBg: "rgba(168,85,247,0.12)",
},
{
id: "5",
title: "Security update",
description: "All dependencies patched successfully",
time: "3h ago",
iconColor: "#4ade80",
iconBg: "rgba(34,197,94,0.12)",
},
{
id: "6",
title: "Milestone reached",
description: "Project hit 10,000 stars on GitHub",
time: "1d ago",
iconColor: "#fb7185",
iconBg: "rgba(244,63,94,0.12)",
},
];
function ListItem({
item,
index,
staggerDelay,
direction,
}: {
item: AnimatedListItem;
index: number;
staggerDelay: number;
direction: "left" | "right";
}) {
const ref = useRef<HTMLLIElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.unobserve(el);
}
},
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
const translateFrom = direction === "left" ? "-30px" : "30px";
return (
<li
ref={ref}
style={{
display: "flex",
alignItems: "center",
gap: "0.875rem",
padding: "0.875rem 1rem",
background: "rgba(255,255,255,0.03)",
border: "1px solid rgba(255,255,255,0.07)",
borderRadius: "0.875rem",
opacity: visible ? 1 : 0,
transform: visible ? "translateX(0)" : `translateX(${translateFrom})`,
transition: `opacity 0.5s cubic-bezier(0.22,1,0.36,1) ${index * staggerDelay}ms, transform 0.5s cubic-bezier(0.22,1,0.36,1) ${index * staggerDelay}ms`,
}}
>
<div
style={{
flexShrink: 0,
width: 40,
height: 40,
borderRadius: "0.625rem",
display: "grid",
placeItems: "center",
background: item.iconBg || "rgba(34,197,94,0.12)",
color: item.iconColor || "#4ade80",
}}
>
{item.icon || (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<path d="M22 4 12 14.01l-3-3" />
</svg>
)}
</div>
<div
style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: "0.125rem" }}
>
<strong style={{ fontSize: "0.875rem", fontWeight: 600, color: "#e2e8f0" }}>
{item.title}
</strong>
<span
style={{
fontSize: "0.8rem",
color: "#64748b",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{item.description}
</span>
</div>
{item.time && (
<span style={{ flexShrink: 0, fontSize: "0.75rem", color: "#475569" }}>{item.time}</span>
)}
</li>
);
}
export default function AnimatedList({
items = defaultItems,
staggerDelay = 80,
}: AnimatedListProps) {
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "grid",
placeItems: "center",
padding: "2rem",
fontFamily: "system-ui, -apple-system, sans-serif",
color: "#f1f5f9",
}}
>
<div
style={{
width: "min(520px, 100%)",
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
<h2 style={{ fontSize: "1.375rem", fontWeight: 700, color: "#f1f5f9" }}>Notifications</h2>
<p style={{ fontSize: "0.875rem", color: "#64748b", marginBottom: "1rem" }}>
Watch the items animate in
</p>
<ul
style={{
listStyle: "none",
display: "flex",
flexDirection: "column",
gap: "0.5rem",
padding: 0,
}}
>
{items.map((item, i) => (
<ListItem
key={item.id}
item={item}
index={i}
staggerDelay={staggerDelay}
direction={i % 2 === 0 ? "left" : "right"}
/>
))}
</ul>
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
staggerDelay: { type: Number, default: 80 },
});
const items = [
{
id: "1",
title: "Build successful",
description: "Your project compiled without errors",
time: "2m ago",
iconColor: "#4ade80",
iconBg: "rgba(34,197,94,0.12)",
},
{
id: "2",
title: "New comment",
description: "Alex left feedback on your PR",
time: "5m ago",
iconColor: "#60a5fa",
iconBg: "rgba(59,130,246,0.12)",
},
{
id: "3",
title: "Disk usage warning",
description: "Storage is at 85% capacity",
time: "12m ago",
iconColor: "#fbbf24",
iconBg: "rgba(245,158,11,0.12)",
},
{
id: "4",
title: "New team member",
description: "Jordan joined the engineering team",
time: "1h ago",
iconColor: "#c084fc",
iconBg: "rgba(168,85,247,0.12)",
},
{
id: "5",
title: "Security update",
description: "All dependencies patched successfully",
time: "3h ago",
iconColor: "#4ade80",
iconBg: "rgba(34,197,94,0.12)",
},
{
id: "6",
title: "Milestone reached",
description: "Project hit 10,000 stars on GitHub",
time: "1d ago",
iconColor: "#fb7185",
iconBg: "rgba(244,63,94,0.12)",
},
];
const itemEls = ref([]);
const visibleSet = ref(new Set());
let observer;
function getItemStyle(index) {
const visible = visibleSet.value.has(index);
const direction = index % 2 === 0 ? "left" : "right";
const translateFrom = direction === "left" ? "-30px" : "30px";
return {
display: "flex",
alignItems: "center",
gap: "0.875rem",
padding: "0.875rem 1rem",
background: "rgba(255,255,255,0.03)",
border: "1px solid rgba(255,255,255,0.07)",
borderRadius: "0.875rem",
opacity: visible ? 1 : 0,
transform: visible ? "translateX(0)" : `translateX(${translateFrom})`,
transition: `opacity 0.5s cubic-bezier(0.22,1,0.36,1) ${index * props.staggerDelay}ms, transform 0.5s cubic-bezier(0.22,1,0.36,1) ${index * props.staggerDelay}ms`,
};
}
function setItemEl(el, index) {
if (el) {
itemEls.value[index] = el;
}
}
onMounted(() => {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const idx = itemEls.value.indexOf(entry.target);
if (idx !== -1) {
visibleSet.value = new Set([...visibleSet.value, idx]);
observer.unobserve(entry.target);
}
}
});
},
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" }
);
itemEls.value.forEach((el) => {
if (el) observer.observe(el);
});
});
onUnmounted(() => {
if (observer) observer.disconnect();
});
</script>
<template>
<div
style="min-height: 100vh; background: #0a0a0a; display: grid; place-items: center; padding: 2rem; font-family: system-ui, -apple-system, sans-serif; color: #f1f5f9;"
>
<div style="width: min(520px, 100%); display: flex; flex-direction: column; gap: 0.5rem;">
<h2 style="font-size: 1.375rem; font-weight: 700; color: #f1f5f9;">Notifications</h2>
<p style="font-size: 0.875rem; color: #64748b; margin-bottom: 1rem;">Watch the items animate in</p>
<ul style="list-style: none; display: flex; flex-direction: column; gap: 0.5rem; padding: 0;">
<li
v-for="(item, i) in items"
:key="item.id"
:ref="(el) => setItemEl(el, i)"
:style="getItemStyle(i)"
>
<div
:style="{
flexShrink: 0,
width: '40px',
height: '40px',
borderRadius: '0.625rem',
display: 'grid',
placeItems: 'center',
background: item.iconBg,
color: item.iconColor,
}"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<path d="M22 4 12 14.01l-3-3" />
</svg>
</div>
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.125rem;">
<strong style="font-size: 0.875rem; font-weight: 600; color: #e2e8f0;">{{ item.title }}</strong>
<span style="font-size: 0.8rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ item.description }}</span>
</div>
<span v-if="item.time" style="flex-shrink: 0; font-size: 0.75rem; color: #475569;">{{ item.time }}</span>
</li>
</ul>
</div>
</div>
</template><script>
import { onMount } from "svelte";
export let staggerDelay = 80;
const items = [
{
id: "1",
title: "Build successful",
description: "Your project compiled without errors",
time: "2m ago",
iconColor: "#4ade80",
iconBg: "rgba(34,197,94,0.12)",
},
{
id: "2",
title: "New comment",
description: "Alex left feedback on your PR",
time: "5m ago",
iconColor: "#60a5fa",
iconBg: "rgba(59,130,246,0.12)",
},
{
id: "3",
title: "Disk usage warning",
description: "Storage is at 85% capacity",
time: "12m ago",
iconColor: "#fbbf24",
iconBg: "rgba(245,158,11,0.12)",
},
{
id: "4",
title: "New team member",
description: "Jordan joined the engineering team",
time: "1h ago",
iconColor: "#c084fc",
iconBg: "rgba(168,85,247,0.12)",
},
{
id: "5",
title: "Security update",
description: "All dependencies patched successfully",
time: "3h ago",
iconColor: "#4ade80",
iconBg: "rgba(34,197,94,0.12)",
},
{
id: "6",
title: "Milestone reached",
description: "Project hit 10,000 stars on GitHub",
time: "1d ago",
iconColor: "#fb7185",
iconBg: "rgba(244,63,94,0.12)",
},
];
let visible = items.map(() => false);
let itemEls = [];
onMount(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const idx = itemEls.indexOf(entry.target);
if (idx !== -1) {
visible[idx] = true;
visible = visible;
observer.unobserve(entry.target);
}
}
});
},
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" }
);
itemEls.forEach((el) => {
if (el) observer.observe(el);
});
return () => observer.disconnect();
});
</script>
<div
style="min-height: 100vh; background: #0a0a0a; display: grid; place-items: center; padding: 2rem; font-family: system-ui, -apple-system, sans-serif; color: #f1f5f9;"
>
<div style="width: min(520px, 100%); display: flex; flex-direction: column; gap: 0.5rem;">
<h2 style="font-size: 1.375rem; font-weight: 700; color: #f1f5f9;">Notifications</h2>
<p style="font-size: 0.875rem; color: #64748b; margin-bottom: 1rem;">Watch the items animate in</p>
<ul style="list-style: none; display: flex; flex-direction: column; gap: 0.5rem; padding: 0;">
{#each items as item, i}
<li
bind:this={itemEls[i]}
style="display: flex; align-items: center; gap: 0.875rem; padding: 0.875rem 1rem; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07); border-radius: 0.875rem; opacity: {visible[i] ? 1 : 0}; transform: translateX({visible[i] ? '0' : (i % 2 === 0 ? '-30px' : '30px')}); transition: opacity 0.5s cubic-bezier(0.22,1,0.36,1) {i * staggerDelay}ms, transform 0.5s cubic-bezier(0.22,1,0.36,1) {i * staggerDelay}ms;"
>
<div
style="flex-shrink: 0; width: 40px; height: 40px; border-radius: 0.625rem; display: grid; place-items: center; background: {item.iconBg}; color: {item.iconColor};"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<path d="M22 4 12 14.01l-3-3" />
</svg>
</div>
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.125rem;">
<strong style="font-size: 0.875rem; font-weight: 600; color: #e2e8f0;">{item.title}</strong>
<span style="font-size: 0.8rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{item.description}</span>
</div>
{#if item.time}
<span style="flex-shrink: 0; font-size: 0.75rem; color: #475569;">{item.time}</span>
{/if}
</li>
{/each}
</ul>
</div>
</div>Animated List
A list where items stagger in with slide + fade animations. Each item enters from a different direction based on its index, creating a lively cascade effect when the list scrolls into view.
How it works
- Items start with
opacity: 0and a transform offset (left, right, or bottom). - An
IntersectionObserverwatches each item and adds a.visibleclass when it enters the viewport. - CSS
@keyframeshandle the slide-in, with each item’sanimation-delayset via a CSS custom property--i.
Features
- Staggered entrance with configurable delay
- Items slide from alternating directions (left, right, bottom)
- IntersectionObserver for scroll-triggered animation
- Respects
prefers-reduced-motion