02구역별 3D 틸트 (CSS만)
카드를 4등분한 보이지 않는 구역 중 마우스가 올라간 구역에 따라 :has() 선택자가 다른 기울기 값을 골라, JS 좌표 추적 없이도 카드가 그쪽으로 기운다.
쓰임기능 소개 카드, JS 없이 틸트가 필요한 포트폴리오 카드
:has() 선택자rotateX/rotateYperspective

<main class="stage">
<article class="card is-demo" tabindex="0">
<div class="card__zones" aria-hidden="true">
<span class="zone zone--tl"></span>
<span class="zone zone--tr"></span>
<span class="zone zone--bl"></span>
<span class="zone zone--br"></span>
</div>
<div class="card__inner">
<h3 class="card__title">신규 릴리즈</h3>
<p class="card__meta">Q3 로드맵 보기</p>
</div>
</article>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 기울어지는 시간
$easing: $ease-spring;
$color: $subject-cream; // 안쪽 패널 배경
@include stage($stage-yellow);
.stage { padding: clamp(8px, 2.4vw, 16px); }
.card {
position: relative;
width: min(150px, 48vw);
aspect-ratio: 4 / 3;
perspective: 600px;
outline: none;
cursor: pointer;
}
.card__zones {
position: absolute; inset: 0;
z-index: 2;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
border-radius: $r-card;
overflow: hidden;
}
.zone { display: block; }
.card__inner {
height: 100%;
padding: $sp-3;
box-sizing: border-box;
border-radius: $r-card;
background: $color;
box-shadow: $shadow-press;
display: flex;
flex-direction: column;
justify-content: center;
transform-style: preserve-3d;
transition: transform $duration $easing;
}
.card__title { margin: 0 0 $sp-1; font-weight: 700; font-size: clamp(11px, 3.2vw, 13px); line-height: 1.3; font-family: inherit; color: $stage-ink; }
.card__meta { margin: 0; font-weight: 500; font-size: clamp(10px, 2.8vw, 11px); line-height: 1.3; font-family: inherit; color: rgba($stage-ink, .65); }
// 구역 4개 중 어디에 커서가 있는지에 따라 :has() 가 다른 기울기를 고른다 — JS 좌표 추적이 없다
.card:has(.zone--tl:hover) .card__inner,
.card:focus-visible .card__inner { transform: rotateX(8deg) rotateY(-10deg); }
.card:has(.zone--tr:hover) .card__inner { transform: rotateX(8deg) rotateY(10deg); }
.card:has(.zone--bl:hover) .card__inner { transform: rotateX(-8deg) rotateY(-10deg); }
.card:has(.zone--br:hover) .card__inner { transform: rotateX(-8deg) rotateY(10deg); }
.card.is-demo .card__inner { animation: tilt-loop $dur-loop $easing infinite; }
@keyframes tilt-loop {
0%, 10% { transform: rotateX(0) rotateY(0); }
25% { transform: rotateX(8deg) rotateY(-10deg); }
50% { transform: rotateX(8deg) rotateY(10deg); }
75% { transform: rotateX(-8deg) rotateY(-10deg); }
90%, 100% { transform: rotateX(0) rotateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.card__inner { transition: none; }
.card.is-demo .card__inner { animation: none; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
padding: clamp(8px, 2.4vw, 16px);
}
.card {
position: relative;
width: min(150px, 48vw);
aspect-ratio: 4/3;
perspective: 600px;
outline: none;
cursor: pointer;
}
.card__zones {
position: absolute;
inset: 0;
z-index: 2;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
border-radius: 16px;
overflow: hidden;
}
.zone {
display: block;
}
.card__inner {
height: 100%;
padding: 12px;
box-sizing: border-box;
border-radius: 16px;
background: #fff7e6;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
display: flex;
flex-direction: column;
justify-content: center;
transform-style: preserve-3d;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.card__title {
margin: 0 0 4px;
font-weight: 700;
font-size: clamp(11px, 3.2vw, 13px);
line-height: 1.3;
font-family: inherit;
color: #17141a;
}
.card__meta {
margin: 0;
font-weight: 500;
font-size: clamp(10px, 2.8vw, 11px);
line-height: 1.3;
font-family: inherit;
color: rgba(23, 20, 26, 0.65);
}
.card:has(.zone--tl:hover) .card__inner,
.card:focus-visible .card__inner {
transform: rotateX(8deg) rotateY(-10deg);
}
.card:has(.zone--tr:hover) .card__inner {
transform: rotateX(8deg) rotateY(10deg);
}
.card:has(.zone--bl:hover) .card__inner {
transform: rotateX(-8deg) rotateY(-10deg);
}
.card:has(.zone--br:hover) .card__inner {
transform: rotateX(-8deg) rotateY(10deg);
}
.card.is-demo .card__inner {
animation: tilt-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes tilt-loop {
0%, 10% {
transform: rotateX(0) rotateY(0);
}
25% {
transform: rotateX(8deg) rotateY(-10deg);
}
50% {
transform: rotateX(8deg) rotateY(10deg);
}
75% {
transform: rotateX(-8deg) rotateY(-10deg);
}
90%, 100% {
transform: rotateX(0) rotateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.card__inner {
transition: none;
}
.card.is-demo .card__inner {
animation: none;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./TiltZonesCard.module.scss";
export type TiltZonesCardProps = {
title: string;
meta: string;
/** 기울어지는 시간 (기본 300ms) */
duration?: number;
easing?: string;
/** 안쪽 패널 배경 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** 구역 4개 중 마우스가 올라간 구역에 따라 :has() 가 기울기를 고른다 — JS 좌표 추적이 없다 */
export default function TiltZonesCard({
title,
meta,
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#fff7e6",
}: TiltZonesCardProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<article className={styles.card} style={vars} tabIndex={0}>
<div className={styles.zones} aria-hidden="true">
<span className={`${styles.zone} ${styles.zoneTl}`} />
<span className={`${styles.zone} ${styles.zoneTr}`} />
<span className={`${styles.zone} ${styles.zoneBl}`} />
<span className={`${styles.zone} ${styles.zoneBr}`} />
</div>
<div className={styles.inner}>
<h3 className={styles.title}>{title}</h3>
<p className={styles.meta}>{meta}</p>
</div>
</article>
);
}02 구역별 3D 틸트 (CSS만) — 측정 누적면적 2.811% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 카드 효과 9가지 — 호버 인터랙션 모음
이 데모가 속한 글
CSS 카드 효과 9가지 — 호버 인터랙션 모음CSS 카드 효과는 마우스를 올리거나 카드를 선택했을 때 상품·포스트 카드가 살짝 움직이면서 정보를 더 보여주는 짧은 시각 변화입니다. 상품·포스트 카드에 바로 붙일 수 있는 아홉 가지를 실제로 렌더링해서 모았고, 이 글의 데모 전부 보기


