07Parallax
Nền phía sau di chuyển chậm hơn nội dung phía trước, tạo chiều sâu.
Dùng choNền hero, ngăn cách giữa các section
0.5배0.3배두 겹

<main class="stage is-demo">
<div class="parallax">
<div class="parallax__back" aria-hidden="true"></div>
<div class="parallax__front">
<h2 class="parallax__title">Tạo cảm giác chiều sâu</h2>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개. 변형은 이것만 바꾼다 (값은 토큰에서 받는다) ──────
$back-shift: 20px; // 뒤 배경 이동량 (0.3배 느낌)
$front-shift: 40px; // 앞 내용 이동량 (0.5배 느낌)
$color: $subject-cream; // 뒤 배경 원 색 (orange 무대라 cream)
// ── 데모 무대 (zip 에서 지워도 된다) ──────────────────────────
html, body { margin: 0; overflow: hidden; }
.stage {
position: relative;
min-height: 100vh;
margin: 0;
overflow: hidden;
background: $stage-orange;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.parallax {
position: absolute;
inset: 0;
display: grid;
place-items: center;
&__back {
position: absolute;
inset: -20% -20%;
background:
radial-gradient($color 0 60px, transparent 61px) 20% 30%,
radial-gradient($color 0 40px, transparent 41px) 75% 65%;
background-repeat: no-repeat;
opacity: .55;
// ── 실제 사용 코드: 뒤 배경은 스크롤보다 느리게(0.3배) 움직인다 ──
@supports (animation-timeline: scroll()) {
animation: drift-back linear both;
animation-timeline: scroll(root);
}
@supports not (animation-timeline: scroll()) {
transform: none;
}
}
&__front {
position: relative;
z-index: 1;
// ── 실제 사용 코드: 앞 내용은 뒤보다 빠르게(0.5배) 움직인다 ──
@supports (animation-timeline: scroll()) {
animation: drift-front linear both;
animation-timeline: scroll(root);
}
@supports not (animation-timeline: scroll()) {
transform: none;
}
}
&__title {
margin: 0;
padding: 14px 22px;
border-radius: $r-pill;
background: $stage-ink;
color: #fff;
font-weight: 700;
font-size: clamp(16px, 5vw, 20px);
line-height: 1;
font-family: inherit;
}
}
@keyframes drift-back { from { transform: translateY(0); } to { transform: translateY(-#{$back-shift}); } }
@keyframes drift-front { from { transform: translateY(0); } to { transform: translateY(-#{$front-shift}); } }
// ── 데모(.is-demo): 스크롤 대신 2초 왕복 루프로 흉내 ────────
.stage.is-demo .parallax__back {
animation: drift-back-loop $dur-loop ease-in-out infinite;
}
.stage.is-demo .parallax__front {
animation: drift-front-loop $dur-loop ease-in-out infinite;
}
@keyframes drift-back-loop {
0%, 100% { transform: translateY($back-shift); }
50% { transform: translateY(-$back-shift); }
}
@keyframes drift-front-loop {
0%, 100% { transform: translateY($front-shift); }
50% { transform: translateY(-$front-shift); }
}
// ── 접근성: 움직임 줄이기 설정이면 층을 그대로 고정 ────────────
@media (prefers-reduced-motion: reduce) {
.parallax__back, .parallax__front,
.stage.is-demo .parallax__back, .stage.is-demo .parallax__front {
animation: none !important;
transform: none;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
position: relative;
min-height: 100vh;
margin: 0;
overflow: hidden;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.parallax {
position: absolute;
inset: 0;
display: grid;
place-items: center;
}
.parallax__back {
position: absolute;
inset: -20% -20%;
background: radial-gradient(#fff7e6 0 60px, transparent 61px) 20% 30%, radial-gradient(#fff7e6 0 40px, transparent 41px) 75% 65%;
background-repeat: no-repeat;
opacity: 0.55;
}
@supports (animation-timeline: scroll()) {
.parallax__back {
animation: drift-back linear both;
animation-timeline: scroll(root);
}
}
@supports not (animation-timeline: scroll()) {
.parallax__back {
transform: none;
}
}
.parallax__front {
position: relative;
z-index: 1;
}
@supports (animation-timeline: scroll()) {
.parallax__front {
animation: drift-front linear both;
animation-timeline: scroll(root);
}
}
@supports not (animation-timeline: scroll()) {
.parallax__front {
transform: none;
}
}
.parallax__title {
margin: 0;
padding: 14px 22px;
border-radius: 999px;
background: #17141a;
color: #fff;
font-weight: 700;
font-size: clamp(16px, 5vw, 20px);
line-height: 1;
font-family: inherit;
}
@keyframes drift-back {
from {
transform: translateY(0);
}
to {
transform: translateY(-20px);
}
}
@keyframes drift-front {
from {
transform: translateY(0);
}
to {
transform: translateY(-40px);
}
}
.stage.is-demo .parallax__back {
animation: drift-back-loop 2s ease-in-out infinite;
}
.stage.is-demo .parallax__front {
animation: drift-front-loop 2s ease-in-out infinite;
}
@keyframes drift-back-loop {
0%, 100% {
transform: translateY(20px);
}
50% {
transform: translateY(-20px);
}
}
@keyframes drift-front-loop {
0%, 100% {
transform: translateY(40px);
}
50% {
transform: translateY(-40px);
}
}
@media (prefers-reduced-motion: reduce) {
.parallax__back, .parallax__front,
.stage.is-demo .parallax__back, .stage.is-demo .parallax__front {
animation: none !important;
transform: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./ParallaxLayers.module.scss";
export type ParallaxLayersProps = {
children: ReactNode;
/** 뒤 배경 이동량 (기본 20px, 0.3배 느낌) */
backShift?: number;
/** 앞 내용 이동량 (기본 40px, 0.5배 느낌) */
frontShift?: number;
/** 뒤 배경 원 색 */
color?: string;
className?: string;
};
type Vars = CSSProperties & { "--back-shift"?: string; "--front-shift"?: string; "--color"?: string };
export default function ParallaxLayers({
children,
backShift = 20,
frontShift = 40,
color = "#fff7e6",
className = "",
}: ParallaxLayersProps) {
const vars: Vars = {
"--back-shift": `${backShift}px`,
"--front-shift": `${frontShift}px`,
"--color": color,
};
return (
<div className={`${styles.parallax} ${className}`} style={vars}>
<div className={styles.back} aria-hidden="true" />
<div className={styles.front}>{children}</div>
</div>
);
}07 Parallax — Diện tích đo được 13.925% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 hiệu ứng css scroll animation không cần JS
Bài viết chứa demo này
9 hiệu ứng css scroll animation không cần JSCss scroll animation là animation gắn liền với vị trí cuộn trang, chạy bằng animation-timeline của CSS thay cho một list Xem tất cả demo trong bài này


