07첫 장 강조 배치
첫 번째 사진에만 grid-column: span 2와 grid-row: span 2를 줘서 나머지보다 네 배 크게 띄우고, 그 옆으로 작은 사진들이 채워지는 잡지 레이아웃을 순수 CSS 선택자 하나로 만듭니다.
쓰임대표 사진 하나를 강조하는 블로그 헤더 갤러리, 신상품 하나를 크게 보여주는 상품 목록
:nth-of-typegrid-column: 1 / 3grid-row: 1 / 3

<main class="stage">
<ul class="gallery is-demo" aria-label="사진 갤러리">
<li class="tile tile--featured" tabindex="0"></li>
<li class="tile tile--small" tabindex="0"></li>
<li class="tile tile--small" tabindex="0"></li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 강조 타일이 커지는 시간
$easing: $ease-spring;
$color: $subject-cream; // 타일 그러데이션
@include stage($stage-orange);
.stage { padding: clamp(8px, 2.4vw, 16px); }
.gallery {
display: grid;
grid-template-columns: repeat(3, 56px);
grid-template-rows: repeat(2, 56px);
gap: $sp-2;
list-style: none;
margin: 0;
padding: 0;
width: 184px;
max-width: 100%;
}
.tile {
border-radius: $r-sm;
outline: none;
cursor: pointer;
box-shadow: $shadow-press;
transform: scale(1);
transition: transform $duration $easing, box-shadow $duration $easing;
}
.tile--featured {
grid-column: 1 / 3;
grid-row: 1 / 3;
background: linear-gradient(150deg, $color 0%, $stage-orange 100%);
}
.tile--small:nth-of-type(2) { background: linear-gradient(60deg, $stage-ink 0%, $color 100%); }
.tile--small:nth-of-type(3) { background: linear-gradient(200deg, $color 0%, $stage-ink 100%); }
.tile:hover, .tile:focus-visible, .tile:active {
transform: scale(1.04);
box-shadow: $shadow-raised;
}
.gallery.is-demo .tile--featured { animation: pop-loop-lg $dur-loop $easing infinite; }
.gallery.is-demo .tile--small { animation: pop-loop-sm $dur-loop $easing infinite; }
.gallery.is-demo .tile--small:nth-of-type(3) { animation-delay: -0.3s; }
@keyframes pop-loop-lg {
0%, 8% { transform: scale(1); box-shadow: $shadow-press; }
30%, 50% { transform: scale(1.03); box-shadow: $shadow-raised; }
70%, 100% { transform: scale(1); box-shadow: $shadow-press; }
}
@keyframes pop-loop-sm {
0%, 40% { transform: scale(1); box-shadow: $shadow-press; }
60%, 80% { transform: scale(1.06); box-shadow: $shadow-raised; }
100% { transform: scale(1); box-shadow: $shadow-press; }
}
@media (hover: none) {
.tile:active { transform: scale(1.04); box-shadow: $shadow-raised; }
}
@media (prefers-reduced-motion: reduce) {
.tile { transition: none; }
.gallery.is-demo .tile--featured, .gallery.is-demo .tile--small { animation: 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);
}
.stage {
padding: clamp(8px, 2.4vw, 16px);
}
.gallery {
display: grid;
grid-template-columns: repeat(3, 56px);
grid-template-rows: repeat(2, 56px);
gap: 8px;
list-style: none;
margin: 0;
padding: 0;
width: 184px;
max-width: 100%;
}
.tile {
border-radius: 8px;
outline: none;
cursor: pointer;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
transform: scale(1);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tile--featured {
grid-column: 1/3;
grid-row: 1/3;
background: linear-gradient(150deg, #fff7e6 0%, #ff4d1f 100%);
}
.tile--small:nth-of-type(2) {
background: linear-gradient(60deg, #17141a 0%, #fff7e6 100%);
}
.tile--small:nth-of-type(3) {
background: linear-gradient(200deg, #fff7e6 0%, #17141a 100%);
}
.tile:hover, .tile:focus-visible, .tile:active {
transform: scale(1.04);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.gallery.is-demo .tile--featured {
animation: pop-loop-lg 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.gallery.is-demo .tile--small {
animation: pop-loop-sm 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.gallery.is-demo .tile--small:nth-of-type(3) {
animation-delay: -0.3s;
}
@keyframes pop-loop-lg {
0%, 8% {
transform: scale(1);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
30%, 50% {
transform: scale(1.03);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
70%, 100% {
transform: scale(1);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
}
@keyframes pop-loop-sm {
0%, 40% {
transform: scale(1);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
60%, 80% {
transform: scale(1.06);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
100% {
transform: scale(1);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
}
@media (hover: none) {
.tile:active {
transform: scale(1.04);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
}
@media (prefers-reduced-motion: reduce) {
.tile {
transition: none;
}
.gallery.is-demo .tile--featured, .gallery.is-demo .tile--small {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./FeaturedGallery.module.scss";
export type FeaturedGalleryProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** 첫 번째 사진에만 grid-column/grid-row span 2 를 줘서 네 배 크게 띄우는 잡지형 레이아웃 */
export default function FeaturedGallery({
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#fff7e6",
}: FeaturedGalleryProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<ul className={styles.gallery} style={vars} aria-label="Photo gallery">
<li className={styles.featured} tabIndex={0} />
<li className={styles.small} tabIndex={0} />
<li className={styles.small} tabIndex={0} />
</ul>
);
}07 첫 장 강조 배치 — 측정 누적면적 7.026% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 이미지 갤러리 CSS 9종 — 메이슨리부터 라이트박스까지
이 데모가 속한 글
이미지 갤러리 CSS 9종 — 메이슨리부터 라이트박스까지이미지 갤러리 CSS는 사진 여러 장을 그리드나 워터폴 모양으로 배치하고 호버·클릭에 반응하게 만드는 레이아웃 기법입니다. 핀터레스트에 저장해 둔 그 갤러리처럼 예쁘지만, 막상 코드로 옮기려면 그리드 수식과 크롭 처 이 글의 데모 전부 보기


