AnimationsIntermediateApril 8, 2026
Clip-Path Polygon Card Reveal
Image cards are collapsed to a single center point using clip-path polygon and expand to full size as they enter the viewport. Combined with scale animation. Triggered by IntersectionObserver.
View Full Demo →Preview
Source
index.jsx
"use client";
import { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";
import styles from "./styles.module.css";
function RevealCard({ title, category, image, href }) {
const ref = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.2 }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<a ref={ref} href={href} className={styles.card}>
<motion.div
className={styles.imageWrap}
initial={{ clipPath: "polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)", scale: 0.8 }}
animate={
isVisible
? { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", scale: 1 }
: {}
}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
style={{ willChange: "clip-path, transform" }}
>
<div
className={styles.image}
style={{ backgroundImage: `url(${image})`, backgroundColor: "#1a1a1a" }}
/>
</motion.div>
<div className={styles.info}>
<h3 className={styles.title}>{title}</h3>
<span className={styles.category}>{category}</span>
</div>
</a>
);
}
export default function ClipPathPolygonCardReveal({ cards = [] }) {
return (
<div className={styles.grid}>
{cards.map((card) => (
<RevealCard key={card.title} {...card} />
))}
</div>
);
}
styles.module.css
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
padding: 2rem;
}
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
text-decoration: none;
color: #1a1a1a;
}
.imageWrap {
aspect-ratio: 3 / 4;
border-radius: 6px;
overflow: hidden;
}
.image {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
}
.info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.title {
font-size: 0.9375rem;
font-weight: 500;
letter-spacing: -0.01em;
}
.category {
font-size: 0.8125rem;
color: #9b9b9b;
}
@media (max-width: 700px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
Dependencies
framer-motion