01無限スクロール写真ウォール
小さな正方形の写真が横に途切れず流れ続け、帯をまるごと複製してあるので、1周して最初に戻る継ぎ目がまったく見えません。
使いどころInstagramフィードをまとめて見せるランディング背景、ロゴの代わりに作品写真を流す紹介セクション
translateX(-50%) 무한 루프track 통째 복제margin-right 간격

<main class="stage">
<div class="wall-viewport" aria-label="終わりなく流れる写真ギャラリー">
<ul class="wall-track is-demo">
<li class="wall-tile wall-tile--pink"></li>
<li class="wall-tile wall-tile--mint"></li>
<li class="wall-tile wall-tile--lilac"></li>
<li class="wall-tile wall-tile--sky"></li>
<li class="wall-tile wall-tile--pink2"></li>
<li class="wall-tile wall-tile--pink" aria-hidden="true"></li>
<li class="wall-tile wall-tile--mint" aria-hidden="true"></li>
<li class="wall-tile wall-tile--lilac" aria-hidden="true"></li>
<li class="wall-tile wall-tile--sky" aria-hidden="true"></li>
<li class="wall-tile wall-tile--pink2" aria-hidden="true"></li>
</ul>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 한 바퀴 도는 시간
$easing: linear;
$color: $subject-pink; // 대표 타일 색
@include stage($stage-paper);
.wall-viewport {
width: min(280px, 84vw);
overflow: hidden;
border-radius: $r-card;
box-shadow: $shadow-press;
}
.wall-track {
display: flex;
list-style: none;
margin: 0;
padding: 0;
width: max-content;
}
// 간격은 margin-right 로만 준다 (BD15) — flex gap 을 쓰면 -50% 가 gap/2 만큼 모자라 튄다
.wall-tile {
flex: 0 0 auto;
width: 56px;
height: 42px;
margin-right: $sp-2;
border-radius: $r-sm;
}
.wall-tile--pink { background: linear-gradient(155deg, $color 0%, $subject-cream 100%); }
.wall-tile--mint { background: linear-gradient(155deg, $subject-mint 0%, $subject-cream 100%); }
.wall-tile--lilac { background: linear-gradient(155deg, $subject-lilac 0%, $subject-cream 100%); }
.wall-tile--sky { background: linear-gradient(155deg, $subject-sky 0%, $subject-cream 100%); }
.wall-tile--pink2 { background: linear-gradient(205deg, $subject-cream 0%, $color 100%); }
// 트랙을 통째로 복제(위 10칸 = 5칸 × 2벌)해 두면 -50% 지점이 처음과 픽셀 단위로 같아 이어붙인 자리가 안 보인다
.wall-track.is-demo { animation: wall-loop $duration $easing infinite; }
@keyframes wall-loop {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.wall-track.is-demo { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wall-viewport {
width: min(280px, 84vw);
overflow: hidden;
border-radius: 16px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.wall-track {
display: flex;
list-style: none;
margin: 0;
padding: 0;
width: max-content;
}
.wall-tile {
flex: 0 0 auto;
width: 56px;
height: 42px;
margin-right: 8px;
border-radius: 8px;
}
.wall-tile--pink {
background: linear-gradient(155deg, #ff9ebb 0%, #fff7e6 100%);
}
.wall-tile--mint {
background: linear-gradient(155deg, #4cd4a6 0%, #fff7e6 100%);
}
.wall-tile--lilac {
background: linear-gradient(155deg, #b9a0ff 0%, #fff7e6 100%);
}
.wall-tile--sky {
background: linear-gradient(155deg, #6ec8ff 0%, #fff7e6 100%);
}
.wall-tile--pink2 {
background: linear-gradient(205deg, #fff7e6 0%, #ff9ebb 100%);
}
.wall-track.is-demo {
animation: wall-loop 2s linear infinite;
}
@keyframes wall-loop {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
@media (prefers-reduced-motion: reduce) {
.wall-track.is-demo {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./InfiniteScrollPhotoWall.module.scss";
export type InfiniteScrollPhotoWallProps = {
/** 한 바퀴 도는 시간 (기본 2000ms) */
duration?: number;
easing?: string;
/** 대표 타일 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const ORDER = ["a", "b", "c", "d", "e", "a", "b", "c", "d", "e"] as const;
/** 작은 사진들이 가로로 끝없이 흐르고, 트랙을 통째로 복제해 이어붙인 자리가 안 보인다 */
export default function InfiniteScrollPhotoWall({
duration = 2000,
easing = "linear",
color = "#ff9ebb",
}: InfiniteScrollPhotoWallProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.viewport} style={vars} aria-label="A gallery of photos scrolling endlessly">
<ul className={styles.track}>
{ORDER.map((tone, i) => (
<li key={i} className={`${styles.tile} ${styles["tile-" + tone]}`} aria-hidden={i >= 5} />
))}
</ul>
</div>
);
}01 無限スクロール写真ウォール — 計測 累積面積 8.037% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · ポートフォリオ ギャラリー UI 9選 — ドラッグからモーフ切替まで
このデモが属する記事
ポートフォリオ ギャラリー UI 9選 — ドラッグからモーフ切替までポートフォリオ ギャラリーとは、写真や作品を格子状に並べ、ホバーやクリックで拡大・絞り込み・並び替え・切り替えが起こる画面のことです。Pinterestで保存したあのポートフォリオギャラリーを、9通りの方式に分けてそのままコードでお届けしま この記事のデモをすべて見る


