06호버 캡션 그리드
여러 장이 늘어선 그리드에서 마우스를 올린 칸만 캡션 바가 translateY로 올라오고 opacity로 나타나며, 나머지 칸은 그대로 있습니다.
쓰임이벤트 사진 갤러리
translateYopacity300ms

<main class="stage">
<div class="hc-grid is-demo">
<div class="hc-tile hc-tile--a"><span class="hc-cap">Interior</span></div>
<div class="hc-tile hc-tile--b"><span class="hc-cap">Outdoor</span></div>
<div class="hc-tile hc-tile--c"><span class="hc-cap">Detail</span></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base;
$easing: $ease-out;
$color: $subject-ink;
@include stage($stage-yellow);
.hc-grid {
width: min(260px, 78vw);
aspect-ratio: 8 / 5;
display: flex;
gap: $sp-2;
}
.hc-tile {
position: relative;
flex: 1 1 0;
border-radius: $r-sm;
overflow: hidden;
}
.hc-tile--a { background: radial-gradient(60px circle at 30% 30%, rgba(255,255,255,.55), transparent 60%), linear-gradient(150deg, $subject-blue, $subject-ink); }
.hc-tile--b { background: radial-gradient(60px circle at 70% 40%, rgba(255,255,255,.55), transparent 60%), linear-gradient(150deg, $subject-cream, $subject-blue); }
.hc-tile--c { background: radial-gradient(60px circle at 50% 60%, rgba(255,255,255,.55), transparent 60%), linear-gradient(150deg, $subject-ink, $subject-cream); }
.hc-cap {
position: absolute; left: 0; right: 0; bottom: 0;
padding: $sp-2;
background: rgba($color, .78);
color: $subject-cream;
font: 600 11px/1 -apple-system, "Segoe UI", sans-serif;
transform: translateY(100%);
opacity: 0;
}
// 실사용: 진짜 마우스 호버 — 지금 가리킨 칸만 캡션이 올라온다, 나머지는 그대로
.hc-cap { transition: transform $duration $easing, opacity $duration $easing; }
.hc-tile:hover .hc-cap { transform: translateY(0); opacity: 1; }
// 데모 루프: 세 칸을 순서대로 도는 캡션 (동시가 아니라 한 번에 하나씩)
@keyframes hc-cap-cycle {
0% { transform: translateY(100%); opacity: 0; }
6% { transform: translateY(0); opacity: 1; }
26% { transform: translateY(0); opacity: 1; }
33% { transform: translateY(100%); opacity: 0; }
100% { transform: translateY(100%); opacity: 0; }
}
.hc-grid.is-demo .hc-tile--a .hc-cap { animation: hc-cap-cycle $dur-loop $easing infinite; animation-delay: 0s; }
.hc-grid.is-demo .hc-tile--b .hc-cap { animation: hc-cap-cycle $dur-loop $easing infinite; animation-delay: .66s; }
.hc-grid.is-demo .hc-tile--c .hc-cap { animation: hc-cap-cycle $dur-loop $easing infinite; animation-delay: 1.32s; }
@media (prefers-reduced-motion: reduce) {
.hc-tile:hover .hc-cap,
.hc-grid.is-demo .hc-tile .hc-cap {
animation: none !important; transition: none !important; transform: translateY(0) !important; opacity: 1 !important;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.hc-grid {
width: min(260px, 78vw);
aspect-ratio: 8/5;
display: flex;
gap: 8px;
}
.hc-tile {
position: relative;
flex: 1 1 0;
border-radius: 8px;
overflow: hidden;
}
.hc-tile--a {
background: radial-gradient(60px circle at 30% 30%, rgba(255, 255, 255, 0.55), transparent 60%), linear-gradient(150deg, #2f6df6, #17141a);
}
.hc-tile--b {
background: radial-gradient(60px circle at 70% 40%, rgba(255, 255, 255, 0.55), transparent 60%), linear-gradient(150deg, #fff7e6, #2f6df6);
}
.hc-tile--c {
background: radial-gradient(60px circle at 50% 60%, rgba(255, 255, 255, 0.55), transparent 60%), linear-gradient(150deg, #17141a, #fff7e6);
}
.hc-cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 8px;
background: rgba(23, 20, 26, 0.78);
color: #fff7e6;
font: 600 11px/1 -apple-system, "Segoe UI", sans-serif;
transform: translateY(100%);
opacity: 0;
}
.hc-cap {
transition: transform 300ms ease-out, opacity 300ms ease-out;
}
.hc-tile:hover .hc-cap {
transform: translateY(0);
opacity: 1;
}
@keyframes hc-cap-cycle {
0% {
transform: translateY(100%);
opacity: 0;
}
6% {
transform: translateY(0);
opacity: 1;
}
26% {
transform: translateY(0);
opacity: 1;
}
33% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(100%);
opacity: 0;
}
}
.hc-grid.is-demo .hc-tile--a .hc-cap {
animation: hc-cap-cycle 2s ease-out infinite;
animation-delay: 0s;
}
.hc-grid.is-demo .hc-tile--b .hc-cap {
animation: hc-cap-cycle 2s ease-out infinite;
animation-delay: 0.66s;
}
.hc-grid.is-demo .hc-tile--c .hc-cap {
animation: hc-cap-cycle 2s ease-out infinite;
animation-delay: 1.32s;
}
@media (prefers-reduced-motion: reduce) {
.hc-tile:hover .hc-cap,
.hc-grid.is-demo .hc-tile .hc-cap {
animation: none !important;
transition: none !important;
transform: translateY(0) !important;
opacity: 1 !important;
}
}import { CSSProperties } from "react";
import styles from "./HoverCaptionGrid.module.scss";
export interface HoverCaptionGridProps {
duration?: string;
easing?: string;
color?: string;
}
const TILES = [
{ id: "a", label: "Interior" },
{ id: "b", label: "Outdoor" },
{ id: "c", label: "Detail" },
] as const;
export default function HoverCaptionGrid({
duration = "300ms",
easing = "ease-out",
color = "#17141a",
}: HoverCaptionGridProps) {
const capStyle: CSSProperties = { transitionDuration: duration, transitionTimingFunction: easing, background: `rgba(23, 20, 26, .78)` };
return (
<div className={styles.grid}>
{TILES.map((t) => (
<div key={t.id} className={`${styles.tile} ${styles["tile" + t.id.toUpperCase()]}`}>
<span className={styles.cap} style={capStyle}>{t.label}</span>
</div>
))}
</div>
);
}06 호버 캡션 그리드 — 측정 누적면적 4.412% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 이미지 슬라이드 애니메이션 9종
이 데모가 속한 글
CSS 이미지 슬라이드 애니메이션 9종사진 위에 마우스를 올리면 커튼처럼 열리거나 원이 점점 커지며 드러나는 걸 본 적 있으신가요? 이런 CSS 이미지 슬라이드 애니메이션을 한 줄 복붙으로 쓸 수 있게 아홉 가지로 모았고, 그중 하나는 자바스크립트 없이 이 글의 데모 전부 보기


