03텍스트가 이미지 위로 밀림
이미지는 그대로 두고 글자만 스크롤보다 빠르게 올라가 이미지 위로 겹쳐 지나간다.
쓰임매거진형 히어로, 포토 에세이 제목
1.6배mix-blend-modetranslateY

<main class="stage is-demo">
<div class="textimg">
<div class="textimg__image" aria-hidden="true"></div>
<h2 class="textimg__text">지나가는 제목</h2>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개. 변형은 이것만 바꾼다 ──────────────────────────
$text-shift: 70px; // 글자 이동량 (이미지보다 훨씬 빠르게)
$image-shift: 14px; // 이미지 이동량 (거의 고정)
$color: $subject-cream; // 글자 색 (orange 무대라 cream)
@include stage($stage-orange);
.textimg {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
overflow: hidden;
&__image {
position: absolute;
inset: 14% 10%;
border-radius: $r-card;
background: rgba($subject-ink, .34);
// ── 실제 사용 코드: 이미지는 거의 고정, 글자만 훨씬 빠르게 지나간다 ──
@supports (animation-timeline: scroll()) {
animation: image-drift linear both;
animation-timeline: scroll(root);
}
@supports not (animation-timeline: scroll()) { transform: none; }
}
&__text {
position: relative;
z-index: 1;
margin: 0;
color: $color;
font: 800 clamp(28px, 11vw, 44px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
mix-blend-mode: difference;
@supports (animation-timeline: scroll()) {
animation: text-drift linear both;
animation-timeline: scroll(root);
}
@supports not (animation-timeline: scroll()) { transform: none; }
}
}
@keyframes image-drift { from { transform: translateY(0); } to { transform: translateY(-#{$image-shift}); } }
@keyframes text-drift { from { transform: translateY(60px); } to { transform: translateY(-#{$text-shift}); } }
// ── 데모(.is-demo): 왕복 루프로 흉내 ────────
.stage.is-demo .textimg__image { animation: image-loop $dur-loop ease-in-out infinite; }
.stage.is-demo .textimg__text { animation: text-loop $dur-loop ease-in-out infinite; }
@keyframes image-loop { 0%, 100% { transform: translateY($image-shift); } 50% { transform: translateY(-$image-shift); } }
@keyframes text-loop { 0%, 100% { transform: translateY(46px); } 50% { transform: translateY(-46px); } }
@media (prefers-reduced-motion: reduce) {
.textimg__image, .textimg__text,
.stage.is-demo .textimg__image, .stage.is-demo .textimg__text {
animation: none !important;
transform: none;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.textimg {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
overflow: hidden;
}
.textimg__image {
position: absolute;
inset: 14% 10%;
border-radius: 16px;
background: rgba(23, 20, 26, 0.34);
}
@supports (animation-timeline: scroll()) {
.textimg__image {
animation: image-drift linear both;
animation-timeline: scroll(root);
}
}
@supports not (animation-timeline: scroll()) {
.textimg__image {
transform: none;
}
}
.textimg__text {
position: relative;
z-index: 1;
margin: 0;
color: #fff7e6;
font: 800 clamp(28px, 11vw, 44px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
mix-blend-mode: difference;
}
@supports (animation-timeline: scroll()) {
.textimg__text {
animation: text-drift linear both;
animation-timeline: scroll(root);
}
}
@supports not (animation-timeline: scroll()) {
.textimg__text {
transform: none;
}
}
@keyframes image-drift {
from {
transform: translateY(0);
}
to {
transform: translateY(-14px);
}
}
@keyframes text-drift {
from {
transform: translateY(60px);
}
to {
transform: translateY(-70px);
}
}
.stage.is-demo .textimg__image {
animation: image-loop 2s ease-in-out infinite;
}
.stage.is-demo .textimg__text {
animation: text-loop 2s ease-in-out infinite;
}
@keyframes image-loop {
0%, 100% {
transform: translateY(14px);
}
50% {
transform: translateY(-14px);
}
}
@keyframes text-loop {
0%, 100% {
transform: translateY(46px);
}
50% {
transform: translateY(-46px);
}
}
@media (prefers-reduced-motion: reduce) {
.textimg__image, .textimg__text,
.stage.is-demo .textimg__image, .stage.is-demo .textimg__text {
animation: none !important;
transform: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./TextOverImage.module.scss";
export type TextOverImageProps = {
children: ReactNode;
/** 글자 이동량 (기본 70px, 이미지보다 훨씬 빠르게) */
textShift?: number;
/** 이미지 이동량 (기본 14px, 거의 고정) */
imageShift?: number;
/** 글자 색 */
color?: string;
className?: string;
};
type Vars = CSSProperties & {
"--text-shift"?: string;
"--image-shift"?: string;
"--color"?: string;
};
export default function TextOverImage({
children,
textShift = 70,
imageShift = 14,
color = "#fff7e6",
className = "",
}: TextOverImageProps) {
const vars: Vars = {
"--text-shift": `${textShift}px`,
"--image-shift": `${imageShift}px`,
"--color": color,
};
return (
<div className={`${styles.wrap} ${className}`} style={vars}>
<div className={styles.image} aria-hidden="true" />
<h2 className={styles.text}>{children}</h2>
</div>
);
}03 텍스트가 이미지 위로 밀림 — 측정 누적면적 31.153% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 패럴랙스 스크롤 9종 — AOS·GSAP 없이, JS 0줄
이 데모가 속한 글
CSS 패럴랙스 스크롤 9종 — AOS·GSAP 없이, JS 0줄CSS 패럴랙스 스크롤 아홉 개를 모았습니다. 레이어마다 이동 속도를 다르게 주는 원리 하나로 배경이 내용보다 느리게 움직이는 착시를 만듭니다. 격자에서는 아홉 개가 2초 간격으로 자동 반복되니 먼저 눈으로 훑고 마 이 글의 데모 전부 보기


