093Dカバーフロー
中央の写真は正面に大きく立ち、両隣の写真は斜めに傾いて小さく退いており、アルバムカバーを手でめくっているように見える。好きな写真をクリックすると正面中央に来る。
使いどころポートフォリオ・アルバムカバーめくり、ブランドショーケース
perspectiverotateY + translateZ:checked

<main class="stage">
<div class="cf is-demo">
<input type="radio" name="cf-tab" id="cf-1" class="cf__radio" checked>
<input type="radio" name="cf-tab" id="cf-2" class="cf__radio">
<input type="radio" name="cf-tab" id="cf-3" class="cf__radio">
<div class="cf__stage">
<label class="cf__slide cf__slide--1" for="cf-1" aria-label="スライド1"></label>
<label class="cf__slide cf__slide--2" for="cf-2" aria-label="スライド2"></label>
<label class="cf__slide cf__slide--3" for="cf-3" aria-label="スライド3"></label>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.cf { width: min(400px, 92vw); }
.cf__radio { position: absolute; opacity: 0; pointer-events: none; }
.cf__stage {
position: relative;
height: clamp(140px, 44vw, 200px);
perspective: 720px;
}
.cf__slide {
position: absolute;
top: 0; left: 50%;
width: 46%;
height: 100%;
margin-left: -23%;
border-radius: $r-card;
cursor: pointer;
transform-style: preserve-3d;
transition: transform $duration $easing, opacity $duration $easing;
}
.cf__slide--1 { background: linear-gradient(160deg, $color, #d6ddeb); }
.cf__slide--2 { background: linear-gradient(160deg, $stage-orange, #ebdbd6); }
.cf__slide--3 { background: linear-gradient(160deg, #c2ae82, $stage-yellow); }
.cf__slide::before {
content: ""; position: absolute; top: $sp-3; right: $sp-3;
width: $sp-6; height: $sp-6; border-radius: 50%;
background: rgba(255, 255, 255, .85);
}
.cf__slide::after {
content: ""; position: absolute; left: -10%; right: -10%; bottom: -6%; height: 46%;
background: rgba(23, 20, 26, .26);
clip-path: polygon(0% 100%, 22% 40%, 42% 66%, 60% 26%, 78% 60%, 100% 34%, 100% 100%);
}
// ── 실제 동작: 고른 라디오의 슬라이드가 정면 중앙으로, 나머지 둘은 좌우로 기울어 물러난다 ──
$center: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
$prev: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(.74);
$next: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(.74);
#cf-1:checked ~ .cf__stage .cf__slide--1 { transform: $center; z-index: 2; }
#cf-1:checked ~ .cf__stage .cf__slide--2 { transform: $next; z-index: 1; opacity: .7; }
#cf-1:checked ~ .cf__stage .cf__slide--3 { transform: $prev; z-index: 1; opacity: .7; }
#cf-2:checked ~ .cf__stage .cf__slide--2 { transform: $center; z-index: 2; }
#cf-2:checked ~ .cf__stage .cf__slide--3 { transform: $next; z-index: 1; opacity: .7; }
#cf-2:checked ~ .cf__stage .cf__slide--1 { transform: $prev; z-index: 1; opacity: .7; }
#cf-3:checked ~ .cf__stage .cf__slide--3 { transform: $center; z-index: 2; }
#cf-3:checked ~ .cf__stage .cf__slide--1 { transform: $next; z-index: 1; opacity: .7; }
#cf-3:checked ~ .cf__stage .cf__slide--2 { transform: $prev; z-index: 1; opacity: .7; }
// ── 데모 루프: 클릭 전까지 세 장이 스스로 중앙 자리를 돌아가며 맡는다 ──
.cf.is-demo .cf__slide--1 { animation: cf-1 $dur-loop $easing infinite; }
.cf.is-demo .cf__slide--2 { animation: cf-2 $dur-loop $easing infinite; }
.cf.is-demo .cf__slide--3 { animation: cf-3 $dur-loop $easing infinite; }
@keyframes cf-1 {
0%, 4% { transform: $center; opacity: 1; }
29%, 62% { transform: $next; opacity: .7; }
66%, 96% { transform: $prev; opacity: .7; }
100% { transform: $center; opacity: 1; }
}
@keyframes cf-2 {
0%, 29% { transform: $prev; opacity: .7; }
33%, 62% { transform: $center; opacity: 1; }
66%, 96% { transform: $next; opacity: .7; }
100% { transform: $prev; opacity: .7; }
}
@keyframes cf-3 {
0%, 29% { transform: $next; opacity: .7; }
33%, 62% { transform: $prev; opacity: .7; }
66%, 96% { transform: $center; opacity: 1; }
100% { transform: $next; opacity: .7; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.cf__slide { transition: none; }
.cf.is-demo .cf__slide--1,
.cf.is-demo .cf__slide--2,
.cf.is-demo .cf__slide--3 { animation: none !important; }
}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);
}
.cf {
width: min(400px, 92vw);
}
.cf__radio {
position: absolute;
opacity: 0;
pointer-events: none;
}
.cf__stage {
position: relative;
height: clamp(140px, 44vw, 200px);
perspective: 720px;
}
.cf__slide {
position: absolute;
top: 0;
left: 50%;
width: 46%;
height: 100%;
margin-left: -23%;
border-radius: 16px;
cursor: pointer;
transform-style: preserve-3d;
transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.cf__slide--1 {
background: linear-gradient(160deg, #2f6df6, #d6ddeb);
}
.cf__slide--2 {
background: linear-gradient(160deg, #ff4d1f, #ebdbd6);
}
.cf__slide--3 {
background: linear-gradient(160deg, #c2ae82, #ffd23f);
}
.cf__slide::before {
content: "";
position: absolute;
top: 12px;
right: 12px;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.85);
}
.cf__slide::after {
content: "";
position: absolute;
left: -10%;
right: -10%;
bottom: -6%;
height: 46%;
background: rgba(23, 20, 26, 0.26);
clip-path: polygon(0% 100%, 22% 40%, 42% 66%, 60% 26%, 78% 60%, 100% 34%, 100% 100%);
}
#cf-1:checked ~ .cf__stage .cf__slide--1 {
transform: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
z-index: 2;
}
#cf-1:checked ~ .cf__stage .cf__slide--2 {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(0.74);
z-index: 1;
opacity: 0.7;
}
#cf-1:checked ~ .cf__stage .cf__slide--3 {
transform: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(0.74);
z-index: 1;
opacity: 0.7;
}
#cf-2:checked ~ .cf__stage .cf__slide--2 {
transform: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
z-index: 2;
}
#cf-2:checked ~ .cf__stage .cf__slide--3 {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(0.74);
z-index: 1;
opacity: 0.7;
}
#cf-2:checked ~ .cf__stage .cf__slide--1 {
transform: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(0.74);
z-index: 1;
opacity: 0.7;
}
#cf-3:checked ~ .cf__stage .cf__slide--3 {
transform: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
z-index: 2;
}
#cf-3:checked ~ .cf__stage .cf__slide--1 {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(0.74);
z-index: 1;
opacity: 0.7;
}
#cf-3:checked ~ .cf__stage .cf__slide--2 {
transform: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(0.74);
z-index: 1;
opacity: 0.7;
}
.cf.is-demo .cf__slide--1 {
animation: cf-1 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.cf.is-demo .cf__slide--2 {
animation: cf-2 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.cf.is-demo .cf__slide--3 {
animation: cf-3 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes cf-1 {
0%, 4% {
transform: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
opacity: 1;
}
29%, 62% {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(0.74);
opacity: 0.7;
}
66%, 96% {
transform: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(0.74);
opacity: 0.7;
}
100% {
transform: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
opacity: 1;
}
}
@keyframes cf-2 {
0%, 29% {
transform: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(0.74);
opacity: 0.7;
}
33%, 62% {
transform: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
opacity: 1;
}
66%, 96% {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(0.74);
opacity: 0.7;
}
100% {
transform: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(0.74);
opacity: 0.7;
}
}
@keyframes cf-3 {
0%, 29% {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(0.74);
opacity: 0.7;
}
33%, 62% {
transform: translateX(-108%) translateZ(-120px) rotateY(38deg) scale(0.74);
opacity: 0.7;
}
66%, 96% {
transform: translateX(-50%) translateZ(0) rotateY(0deg) scale(1);
opacity: 1;
}
100% {
transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(0.74);
opacity: 0.7;
}
}
@media (prefers-reduced-motion: reduce) {
.cf__slide {
transition: none;
}
.cf.is-demo .cf__slide--1,
.cf.is-demo .cf__slide--2,
.cf.is-demo .cf__slide--3 {
animation: none !important;
}
}// 실제 동작은 :checked 만으로 끝난다(0줄). 이 3줄은 미리보기 자동 재생만 멈춘다.
document.querySelectorAll('.cf__radio').forEach(function (r) {
r.addEventListener('pointerdown', function () { document.querySelector('.cf').classList.remove('is-demo'); });
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./Coverflow3d.module.scss";
export type Coverflow3dProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
function roleOf(i: number, active: number, n: number): "center" | "prev" | "next" {
if (i === active) return "center";
return (i - active + n) % n === 1 ? "next" : "prev";
}
export default function Coverflow3d({ duration = 600, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: Coverflow3dProps) {
const [active, setActive] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const n = 3;
return (
<div className={styles.cf} style={vars}>
<div className={styles.stageBox}>
{[0, 1, 2].map((i) => (
<button
key={i}
type="button"
aria-label={`슬라이드 ${i + 1}`}
className={`${styles.slide} ${styles["slide" + (i + 1)]} ${styles[roleOf(i, active, n)]}`}
onClick={() => setActive(i)}
/>
))}
</div>
</div>
);
}09 3Dカバーフロー — 計測 累積面積 36.764% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · css 画像スライダー9選 — コピペで使えるカルーセル
このデモが属する記事
css 画像スライダー9選 — コピペで使えるカルーセル通販サイトの商品写真や会社サイトのトップ画像で、指でなぞったりボタンを押したりすると次の一枚に切り替わる仕組みを画像スライダーと呼びます。ここで紹介するcss 画像スライダーはCSSだけ・JS不要で、9種類のやり方をそのままコピペできる形に この記事のデモをすべて見る


