05Gallery lật thẻ
4 ô lưới lật lần lượt, mỗi ô trễ 0,15 giây so với ô trước, để lộ nhãn danh mục phía sau.
Dùng choLưới danh mục trong gallery media, giới thiệu widget dashboard
backface-visibilityanimation-delay 시차grid

<main class="stage">
<ul class="gallery is-demo">
<li class="tile"><div class="tile__inner">
<div class="tile__face tile__face--front" aria-hidden="true"></div>
<div class="tile__face tile__face--back">Ảnh</div>
</div></li>
<li class="tile"><div class="tile__inner">
<div class="tile__face tile__face--front" aria-hidden="true"></div>
<div class="tile__face tile__face--back">Video</div>
</div></li>
<li class="tile"><div class="tile__inner">
<div class="tile__face tile__face--front" aria-hidden="true"></div>
<div class="tile__face tile__face--back">Tài liệu</div>
</div></li>
<li class="tile"><div class="tile__inner">
<div class="tile__face tile__face--front" aria-hidden="true"></div>
<div class="tile__face tile__face--back">Nhạc</div>
</div></li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────
$duration: $dur-base; // 타일 하나가 뒤집히는 시간
$easing: $ease-out;
$color: $subject-blue-deep; // 뒷면(카테고리) 배경 (크림 글자가 얹히므로 진한 파랑)
@include stage($stage-ink);
.gallery {
display: grid;
grid-template-columns: repeat(2, 74px);
gap: $sp-2;
list-style: none;
margin: 0;
padding: 0;
perspective: 700px; // 격자 전체가 같은 시점을 공유
}
.tile { width: 74px; height: 74px; }
.tile__inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
}
.tile__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: $r-sm;
backface-visibility: hidden; // 뒤로 넘어간 면 글자가 반전돼 비치지 않게
}
.tile__face--front { background: $subject-cream; }
.tile__face--back { background: $color; color: $subject-cream; font-weight: 700; font-size: 12px; line-height: 1; font-family: inherit; transform: rotateY(180deg); }
// 4칸이 0.15초씩 시차를 두고 같은 동작을 한다 — 물결처럼 뒤집힌다
.gallery.is-demo .tile:nth-child(1) .tile__inner { animation: flip $dur-loop $easing infinite; }
.gallery.is-demo .tile:nth-child(2) .tile__inner { animation: flip $dur-loop $easing infinite; animation-delay: .15s; }
.gallery.is-demo .tile:nth-child(3) .tile__inner { animation: flip $dur-loop $easing infinite; animation-delay: .3s; }
.gallery.is-demo .tile:nth-child(4) .tile__inner { animation: flip $dur-loop $easing infinite; animation-delay: .45s; }
@keyframes flip {
0%, 15% { transform: rotateY(0deg); }
50% { transform: rotateY(180deg); }
85%, 100% { transform: rotateY(360deg); } // 360 = 0 이라 다음 루프로 튀지 않고 이어진다
}
// ── 접근성: 움직임 줄이기 설정이면 뒷면(정보)에서 고정 ────────────
@media (prefers-reduced-motion: reduce) {
.gallery.is-demo .tile__inner { animation: none; transform: rotateY(180deg); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.gallery {
display: grid;
grid-template-columns: repeat(2, 74px);
gap: 8px;
list-style: none;
margin: 0;
padding: 0;
perspective: 700px;
}
.tile {
width: 74px;
height: 74px;
}
.tile__inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
}
.tile__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 8px;
backface-visibility: hidden;
}
.tile__face--front {
background: #fff7e6;
}
.tile__face--back {
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
transform: rotateY(180deg);
}
.gallery.is-demo .tile:nth-child(1) .tile__inner {
animation: flip 2s ease-out infinite;
}
.gallery.is-demo .tile:nth-child(2) .tile__inner {
animation: flip 2s ease-out infinite;
animation-delay: 0.15s;
}
.gallery.is-demo .tile:nth-child(3) .tile__inner {
animation: flip 2s ease-out infinite;
animation-delay: 0.3s;
}
.gallery.is-demo .tile:nth-child(4) .tile__inner {
animation: flip 2s ease-out infinite;
animation-delay: 0.45s;
}
@keyframes flip {
0%, 15% {
transform: rotateY(0deg);
}
50% {
transform: rotateY(180deg);
}
85%, 100% {
transform: rotateY(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.gallery.is-demo .tile__inner {
animation: none;
transform: rotateY(180deg);
}
}import type { CSSProperties } from "react";
import styles from "./FlipGallery.module.scss";
export type FlipGalleryProps = {
duration?: number;
easing?: string;
/** 뒷면(카테고리) 배경 */
color?: string;
tiles?: string[];
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string; "--delay"?: string };
export default function FlipGallery({
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
tiles = ["사진", "동영상", "문서", "음악"],
}: FlipGalleryProps) {
const base: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<ul className={styles.gallery}>
{tiles.map((t, i) => (
<li key={t} className={styles.tile}>
<div className={styles.inner} style={{ ...base, "--delay": `${i * 0.15}s` } as Vars}>
<div className={`${styles.face} ${styles.front}`} aria-hidden="true" />
<div className={`${styles.face} ${styles.back}`}>{t}</div>
</div>
</li>
))}
</ul>
);
}05 Gallery lật thẻ — Diện tích đo được 15.097% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 CSS 3D Cube Effects — dán 1 dòng là chạy
Bài viết chứa demo này
9 CSS 3D Cube Effects — dán 1 dòng là chạyCSS 3D cube là nhóm hiệu ứng khối lập phương xoay, carousel hình trụ và card nghiêng khi hover, dựng bằng perspective và Xem tất cả demo trong bài này


