03Chữ trôi qua ảnh
Ảnh gần như đứng yên, chỉ chữ di chuyển nhanh hơn tốc độ cuộn rồi trượt qua bên trên ảnh.
Dùng choHero kiểu tạp chí, tiêu đề photo essay
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">Tiêu đề trôi qua</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 Chữ trôi qua ảnh — Diện tích đo được 31.153% · 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 parallax scrolling, không cần JS
Bài viết chứa demo này
9 hiệu ứng css parallax scrolling, không cần JSCSS parallax scrolling là cách tạo cảm giác chiều sâu khi cuộn trang, bằng cách cho các lớp — nền, chữ, thẻ — di chuyển Xem tất cả demo trong bài này


