04메이슨리 순차 페이드
높낮이가 다른 사진 타일이 벽돌처럼 쌓인 그리드가 화면에 들어오면, IntersectionObserver가 타일마다 순서대로 opacity와 translateY를 켭니다.
쓰임블로그 썸네일 그리드
columnstransition-delayJS 7줄

<main class="stage">
<div class="mf-grid is-demo">
<div class="mf-tile mf-tile--a"></div>
<div class="mf-tile mf-tile--b"></div>
<div class="mf-tile mf-tile--c"></div>
<div class="mf-tile mf-tile--d"></div>
<div class="mf-tile mf-tile--e"></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop;
$easing: $ease-out;
$stagger: 140ms;
@include stage($stage-paper);
.mf-grid {
width: min(260px, 78vw);
columns: 2;
column-gap: $sp-2;
}
.mf-tile {
break-inside: avoid;
margin-bottom: $sp-2;
border-radius: $r-sm;
box-shadow: $shadow-press;
// 진짜 등장 전 상태 — transition 경로와 keyframes 경로가 같은 시작점을 공유한다
opacity: 0;
transform: translateY($sp-4);
}
.mf-tile--a { height: 52px; background: radial-gradient(26px circle at 30% 30%, rgba(255,255,255,.6), transparent 60%), linear-gradient(150deg, $subject-blue, $subject-ink); }
.mf-tile--b { height: 36px; background: radial-gradient(22px circle at 70% 60%, rgba(255,255,255,.6), transparent 60%), linear-gradient(150deg, $subject-cream, $subject-blue); }
.mf-tile--c { height: 44px; background: radial-gradient(24px circle at 40% 70%, rgba(255,255,255,.6), transparent 60%), linear-gradient(150deg, $subject-ink, $subject-cream); }
.mf-tile--d { height: 56px; background: radial-gradient(26px circle at 60% 30%, rgba(255,255,255,.6), transparent 60%), linear-gradient(150deg, $subject-blue, $subject-cream); }
.mf-tile--e { height: 32px; background: radial-gradient(18px circle at 50% 50%, rgba(255,255,255,.6), transparent 60%), linear-gradient(150deg, $subject-cream, $subject-ink); }
// 실사용: 진짜 IntersectionObserver 가 뷰포트에 들어올 때 .in 을 붙인다(위 <script>). 타일마다 delay 만 다르다
.mf-tile { transition: opacity 400ms $easing, transform 400ms $easing; }
.mf-tile.in { opacity: 1; transform: translateY(0); }
.mf-tile--a.in { transition-delay: 0; }
.mf-tile--b.in { transition-delay: $stagger; }
.mf-tile--c.in { transition-delay: $stagger * 2; }
.mf-tile--d.in { transition-delay: $stagger * 3; }
.mf-tile--e.in { transition-delay: $stagger * 4; }
// 데모 루프: is-demo 는 transition 경로를 쓰지 않는다 — 같은 순차 등장을 keyframes 로 무한 재생(캡처용)
@keyframes mf-tile-in {
0% { opacity: 0; transform: translateY($sp-4); }
20% { opacity: 1; transform: translateY(0); }
100% { opacity: 1; transform: translateY(0); }
}
.mf-grid.is-demo .mf-tile { animation: mf-tile-in $duration $easing infinite; animation-fill-mode: both; }
.mf-grid.is-demo .mf-tile--a { animation-delay: 0ms; }
.mf-grid.is-demo .mf-tile--b { animation-delay: $stagger; }
.mf-grid.is-demo .mf-tile--c { animation-delay: $stagger * 2; }
.mf-grid.is-demo .mf-tile--d { animation-delay: $stagger * 3; }
.mf-grid.is-demo .mf-tile--e { animation-delay: $stagger * 4; }
@media (prefers-reduced-motion: reduce) {
.mf-tile, .mf-tile.in, .mf-grid.is-demo .mf-tile {
animation: none !important; transition: none !important; opacity: 1 !important; transform: none !important;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.mf-grid {
width: min(260px, 78vw);
columns: 2;
column-gap: 8px;
}
.mf-tile {
break-inside: avoid;
margin-bottom: 8px;
border-radius: 8px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
opacity: 0;
transform: translateY(16px);
}
.mf-tile--a {
height: 52px;
background: radial-gradient(26px circle at 30% 30%, rgba(255, 255, 255, 0.6), transparent 60%), linear-gradient(150deg, #2f6df6, #17141a);
}
.mf-tile--b {
height: 36px;
background: radial-gradient(22px circle at 70% 60%, rgba(255, 255, 255, 0.6), transparent 60%), linear-gradient(150deg, #fff7e6, #2f6df6);
}
.mf-tile--c {
height: 44px;
background: radial-gradient(24px circle at 40% 70%, rgba(255, 255, 255, 0.6), transparent 60%), linear-gradient(150deg, #17141a, #fff7e6);
}
.mf-tile--d {
height: 56px;
background: radial-gradient(26px circle at 60% 30%, rgba(255, 255, 255, 0.6), transparent 60%), linear-gradient(150deg, #2f6df6, #fff7e6);
}
.mf-tile--e {
height: 32px;
background: radial-gradient(18px circle at 50% 50%, rgba(255, 255, 255, 0.6), transparent 60%), linear-gradient(150deg, #fff7e6, #17141a);
}
.mf-tile {
transition: opacity 400ms ease-out, transform 400ms ease-out;
}
.mf-tile.in {
opacity: 1;
transform: translateY(0);
}
.mf-tile--a.in {
transition-delay: 0;
}
.mf-tile--b.in {
transition-delay: 140ms;
}
.mf-tile--c.in {
transition-delay: 280ms;
}
.mf-tile--d.in {
transition-delay: 420ms;
}
.mf-tile--e.in {
transition-delay: 560ms;
}
@keyframes mf-tile-in {
0% {
opacity: 0;
transform: translateY(16px);
}
20% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.mf-grid.is-demo .mf-tile {
animation: mf-tile-in 2s ease-out infinite;
animation-fill-mode: both;
}
.mf-grid.is-demo .mf-tile--a {
animation-delay: 0ms;
}
.mf-grid.is-demo .mf-tile--b {
animation-delay: 140ms;
}
.mf-grid.is-demo .mf-tile--c {
animation-delay: 280ms;
}
.mf-grid.is-demo .mf-tile--d {
animation-delay: 420ms;
}
.mf-grid.is-demo .mf-tile--e {
animation-delay: 560ms;
}
@media (prefers-reduced-motion: reduce) {
.mf-tile, .mf-tile.in, .mf-grid.is-demo .mf-tile {
animation: none !important;
transition: none !important;
opacity: 1 !important;
transform: none !important;
}
}var tiles = document.querySelectorAll('.mf-tile');
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } });
}, { threshold: 0.3 });
tiles.forEach(function (t) { io.observe(t); });import { CSSProperties, useEffect, useRef, useState } from "react";
import styles from "./MasonryFadeStagger.module.scss";
export interface MasonryFadeStaggerProps {
duration?: string;
easing?: string;
stagger?: number;
}
const TILES = ["a", "b", "c", "d", "e"] as const;
export default function MasonryFadeStagger({
duration = "400ms",
easing = "ease-out",
stagger = 140,
}: MasonryFadeStaggerProps) {
const ref = useRef<HTMLDivElement>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => entries.forEach((e) => e.isIntersecting && setInView(true)),
{ threshold: 0.3 }
);
io.observe(el);
return () => io.disconnect();
}, []);
return (
<div ref={ref} className={styles.grid}>
{TILES.map((t, i) => {
const style: CSSProperties = {
transitionDuration: duration,
transitionTimingFunction: easing,
transitionDelay: inView ? `${i * stagger}ms` : "0ms",
};
return (
<div
key={t}
className={`${styles.tile} ${styles["tile" + t.toUpperCase()]} ${inView ? styles.in : ""}`}
style={style}
/>
);
})}
</div>
);
}04 메이슨리 순차 페이드 — 측정 누적면적 25.641% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 이미지 슬라이드 애니메이션 9종
이 데모가 속한 글
CSS 이미지 슬라이드 애니메이션 9종사진 위에 마우스를 올리면 커튼처럼 열리거나 원이 점점 커지며 드러나는 걸 본 적 있으신가요? 이런 CSS 이미지 슬라이드 애니메이션을 한 줄 복붙으로 쓸 수 있게 아홉 가지로 모았고, 그중 하나는 자바스크립트 없이 이 글의 데모 전부 보기


