07분위기를 먼저 보이는 이벤트 카드
날짜 숫자가 카드 한쪽을 크게 차지하고 제목 줄이 아래에서 올라오며, 카드 배경의 색 띠가 천천히 기울어 행사 분위기를 낸다.
쓰임공연·전시 일정 목록, 모임 안내
rotatemix-blend-modeanimation-delay

<main class="stage">
<svg class="em__defs" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><symbol id="emPin" viewBox="0 0 256 256"><g fill="currentColor"><path d="M128 24a80 80 0 0 0-80 80c0 72 80 128 80 128s80-56 80-128a80 80 0 0 0-80-80m0 112a32 32 0 1 1 32-32a32 32 0 0 1-32 32" opacity=".2"/><path d="M128 64a40 40 0 1 0 40 40a40 40 0 0 0-40-40m0 64a24 24 0 1 1 24-24a24 24 0 0 1-24 24m0-112a88.1 88.1 0 0 0-88 88c0 31.4 14.51 64.68 42 96.25a254.2 254.2 0 0 0 41.45 38.3a8 8 0 0 0 9.18 0a254.2 254.2 0 0 0 41.37-38.3c27.45-31.57 42-64.85 42-96.25a88.1 88.1 0 0 0-88-88m0 206c-16.53-13-72-60.75-72-118a72 72 0 0 1 144 0c0 57.23-55.47 105-72 118"/></g></symbol></svg>
<ul class="em is-demo" id="em">
<li class="em__card">
<span class="em__mood em__mood--a" aria-hidden="true"></span>
<div class="em__day">
<b class="em__num">12</b>
<span class="em__mon">10월</span>
</div>
<div class="em__body">
<div class="em__line"><h3 class="em__title">밤의 재즈 소품집</h3></div>
<p class="em__place"><svg class="em__ic" aria-hidden="true"><use href="#emPin"/></svg>연남 골목홀</p>
<a class="em__link" href="#jazz">밤의 재즈 소품집 예매</a>
</div>
</li>
<li class="em__card">
<span class="em__mood em__mood--b" aria-hidden="true"></span>
<div class="em__day">
<b class="em__num">19</b>
<span class="em__mon">10월</span>
</div>
<div class="em__body">
<div class="em__line"><h3 class="em__title">종이 공예 워크숍</h3></div>
<p class="em__place"><svg class="em__ic" aria-hidden="true"><use href="#emPin"/></svg>성수 작업실</p>
<a class="em__link" href="#paper">종이 공예 워크숍 예매</a>
</div>
</li>
<li class="em__card">
<span class="em__mood em__mood--c" aria-hidden="true"></span>
<div class="em__day">
<b class="em__num">26</b>
<span class="em__mon">10월</span>
</div>
<div class="em__body">
<div class="em__line"><h3 class="em__title">가을 야외 상영회</h3></div>
<p class="em__place"><svg class="em__ic" aria-hidden="true"><use href="#emPin"/></svg>한강 잔디마당</p>
<a class="em__link" href="#screen">가을 야외 상영회 예매</a>
</div>
</li>
</ul>
</main>@use "sass:math";
@use "../../../../site/static/demo-tokens" as *;
// 분위기를 먼저 보이는 이벤트 카드 — 날짜가 카드 한쪽을 크게 차지하고, 제목 줄은 아래에서 올라오고,
// 색 띠 하나가 카드 안에서 천천히 기운다. 띠는 이미지가 아니라 mix-blend-mode: multiply 로
// 카드 면과 곱해진 색이라 카드 배경이 바뀌면 띠의 색도 따라 바뀐다.
//
// 세 가지가 맞물려야 이 배치가 성립한다
// rotate — 띠를 기울인다. transform 단축이 아닌 낱개 속성이라 translate·scale 과 안 싸운다
// mix-blend-mode — 띠가 카드 면과 곱해진다. 카드에 isolation: isolate 가 없으면 무대(주황)까지 뚫고 섞인다
// animation-delay — 카드마다 띠와 제목이 자기 순서에 움직인다 (fill-mode: backwards 를 늘 함께)
//
// 글자는 전부 잉크 한 색이다. 띠가 지나가면 면색이 크림 → 곱해진 색으로 바뀌는데,
// 가장 어두워지는 조합(크림 x 주황 = #ff4b1c)에서도 잉크 글자가 5.50:1 이라 AA 를 지킨다.
// 흐린 글자(알파)를 쓰면 이 계산이 띠 위에서 무너진다 — 그래서 굵기와 크기로만 층을 나눴다.
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-blue;
// 2초 루프 안에서 "제목이 올라오는" 구간. $duration 을 절반으로 줄이면 구간도 절반이 된다
$enter: math.div($duration, $dur-loop) * 100%;
$tilt: 9deg;
@include stage($stage-orange);
.em__defs { position: absolute; width: 0; height: 0; }
.em {
display: grid;
gap: $sp-2;
box-sizing: border-box; width: min(100%, 432px);
margin: 0; padding: 0; list-style: none;
}
.em__card {
position: relative;
// 카드가 자기 합성 그룹이 된다 — 이게 없으면 띠가 카드 면이 아니라 무대(주황)와 곱해진다
isolation: isolate;
overflow: hidden;
box-sizing: border-box;
display: grid;
// 날짜 칸은 내용 폭, 나머지 전부가 본문 — 날짜가 카드 한쪽을 크게 차지한다
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: $sp-3;
padding: $sp-2 $sp-3;
border-radius: $r-card;
border-left: 3px solid $color;
background: $subject-cream;
}
// ── 분위기 띠. 카드보다 크게 깔아 두고 기울이므로 모서리에 빈자리가 생기지 않는다
.em__mood {
position: absolute; inset: -60% -20%;
z-index: 0;
mix-blend-mode: multiply;
rotate: -#{$tilt};
}
// 곱셈이라 밝은 색만 쓴다 — 어두운 색을 곱하면 그 위 글자가 읽히지 않는다
.em__mood--a { background: linear-gradient(104deg, rgba($stage-yellow, 0) 22%, $stage-yellow 48%, $stage-orange 96%); }
.em__mood--b { background: linear-gradient(104deg, rgba($subject-mint, 0) 22%, $subject-mint 48%, $subject-sky 96%); }
.em__mood--c { background: linear-gradient(104deg, rgba($subject-lilac, 0) 22%, $subject-lilac 48%, $subject-pink 96%); }
.em__day, .em__body { position: relative; z-index: 1; }
.em__day { display: grid; justify-items: center; gap: 0; }
.em__num {
color: $subject-ink;
font-weight: 800; font-size: clamp(20px, 6.4vw, 28px); line-height: 1.05;
font-variant-numeric: tabular-nums;
}
.em__mon {
color: $subject-ink;
font-weight: 700; font-size: clamp(9px, 2.6vw, 11px); line-height: 1.3;
}
.em__body { display: grid; gap: $sp-1; }
// 제목 줄은 잘라 두고 그 안에서 올라온다 — 카드 밖으로 새지 않는다
.em__line { overflow: hidden; }
.em__title {
margin: 0;
color: $subject-ink;
font-weight: 700; font-size: clamp(11px, 3.2vw, 14px); line-height: 1.25;
}
.em__place {
display: flex; align-items: center; gap: $sp-1;
margin: 0;
color: $subject-ink;
font-weight: 500; font-size: clamp(9px, 2.6vw, 11px); line-height: 1.3;
}
.em__ic { flex: 0 0 auto; width: 12px; height: 12px; color: $color; }
// 카드 전체가 아니라 링크가 무슨 행사인지 말한다 (WCAG 2.4.4 link purpose)
.em__link {
justify-self: start;
color: $subject-ink;
font-weight: 700; font-size: clamp(9px, 2.6vw, 11px); line-height: 1.3;
text-decoration: underline; text-underline-offset: 2px;
}
.em__link:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; border-radius: $r-xs; }
// ── 격자 미리보기 루프 (2초). 띠는 내내 기울고, 제목은 카드 순서대로 올라온다
.em.is-demo .em__mood {
animation-name: emTilt;
animation-duration: $dur-loop;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.em.is-demo .em__card:nth-child(2) .em__mood { animation-delay: .18s; }
.em.is-demo .em__card:nth-child(3) .em__mood { animation-delay: .36s; }
.em.is-demo .em__title {
animation-name: emRise;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
// 지연 구간에 요소의 작성 상태(= 도착 상태)가 0프레임에 번쩍이는 걸 막는다
animation-fill-mode: backwards;
}
.em.is-demo .em__card:nth-child(2) .em__title { animation-delay: .18s; }
.em.is-demo .em__card:nth-child(3) .em__title { animation-delay: .36s; }
@keyframes emTilt {
0% { rotate: -#{$tilt}; }
50% { rotate: $tilt; }
100% { rotate: -#{$tilt}; }
}
// 복귀(다시 내려감)는 88% 안에 끝난다 — 24프레임의 마지막 샘플이 95.8% 지점이다
@keyframes emRise {
0% { transform: translateY(120%); }
#{$enter} { transform: translateY(0); }
74% { transform: translateY(0); }
88%, 100% { transform: translateY(120%); }
}
// ── 좁은 화면: 카드 셋을 그대로 쌓는다. 날짜와 글자만 줄인다
@media (max-width: 340px) {
.em { gap: $sp-1; }
.em__card { gap: $sp-2; padding-top: $sp-1; padding-bottom: $sp-1; padding-left: $sp-2; padding-right: $sp-2; }
.em__body { gap: 0; }
.em__ic { width: 10px; height: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.em.is-demo .em__mood,
.em.is-demo .em__title {
animation: none !important;
transform: none !important;
}
}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);
}
.em__defs {
position: absolute;
width: 0;
height: 0;
}
.em {
display: grid;
gap: 8px;
box-sizing: border-box;
width: min(100%, 432px);
margin: 0;
padding: 0;
list-style: none;
}
.em__card {
position: relative;
isolation: isolate;
overflow: hidden;
box-sizing: border-box;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 12px;
padding: 8px 12px;
border-radius: 16px;
border-left: 3px solid #2f6df6;
background: #fff7e6;
}
.em__mood {
position: absolute;
inset: -60% -20%;
z-index: 0;
mix-blend-mode: multiply;
rotate: -9deg;
}
.em__mood--a {
background: linear-gradient(104deg, rgba(255, 210, 63, 0) 22%, #ffd23f 48%, #ff4d1f 96%);
}
.em__mood--b {
background: linear-gradient(104deg, rgba(76, 212, 166, 0) 22%, #4cd4a6 48%, #6ec8ff 96%);
}
.em__mood--c {
background: linear-gradient(104deg, rgba(185, 160, 255, 0) 22%, #b9a0ff 48%, #ff9ebb 96%);
}
.em__day, .em__body {
position: relative;
z-index: 1;
}
.em__day {
display: grid;
justify-items: center;
gap: 0;
}
.em__num {
color: #17141a;
font-weight: 800;
font-size: clamp(20px, 6.4vw, 28px);
line-height: 1.05;
font-variant-numeric: tabular-nums;
}
.em__mon {
color: #17141a;
font-weight: 700;
font-size: clamp(9px, 2.6vw, 11px);
line-height: 1.3;
}
.em__body {
display: grid;
gap: 4px;
}
.em__line {
overflow: hidden;
}
.em__title {
margin: 0;
color: #17141a;
font-weight: 700;
font-size: clamp(11px, 3.2vw, 14px);
line-height: 1.25;
}
.em__place {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
color: #17141a;
font-weight: 500;
font-size: clamp(9px, 2.6vw, 11px);
line-height: 1.3;
}
.em__ic {
flex: 0 0 auto;
width: 12px;
height: 12px;
color: #2f6df6;
}
.em__link {
justify-self: start;
color: #17141a;
font-weight: 700;
font-size: clamp(9px, 2.6vw, 11px);
line-height: 1.3;
text-decoration: underline;
text-underline-offset: 2px;
}
.em__link:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
border-radius: 4px;
}
.em.is-demo .em__mood {
animation-name: emTilt;
animation-duration: 2s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.em.is-demo .em__card:nth-child(2) .em__mood {
animation-delay: 0.18s;
}
.em.is-demo .em__card:nth-child(3) .em__mood {
animation-delay: 0.36s;
}
.em.is-demo .em__title {
animation-name: emRise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.em.is-demo .em__card:nth-child(2) .em__title {
animation-delay: 0.18s;
}
.em.is-demo .em__card:nth-child(3) .em__title {
animation-delay: 0.36s;
}
@keyframes emTilt {
0% {
rotate: -9deg;
}
50% {
rotate: 9deg;
}
100% {
rotate: -9deg;
}
}
@keyframes emRise {
0% {
transform: translateY(120%);
}
30% {
transform: translateY(0);
}
74% {
transform: translateY(0);
}
88%, 100% {
transform: translateY(120%);
}
}
@media (max-width: 340px) {
.em {
gap: 4px;
}
.em__card {
gap: 8px;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 8px;
padding-right: 8px;
}
.em__body {
gap: 0;
}
.em__ic {
width: 10px;
height: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.em.is-demo .em__mood,
.em.is-demo .em__title {
animation: none !important;
transform: none !important;
}
}// 사람이 만지면 미리보기 루프를 뗀다 — 뗀 뒤에도 색 띠는 기울어진 자리에 그대로 선다
var eventMood = document.getElementById('em');
['pointerdown', 'focusin'].forEach(function (evt) {
eventMood.addEventListener(evt, function () { eventMood.classList.remove('is-demo'); });
});// EventMoodCard — vanilla 판(run/081/demo/07-content-card-event-mood/index.html) 의 React 포트.
// 색 띠는 rotate 로 기울고 mix-blend-mode: multiply 로 카드 면과 곱해진다 — 카드에 isolation: isolate 가
// 없으면 무대(주황)까지 뚫고 섞인다. 제목 줄은 animation-delay 로 카드 순서에 올라온다.
// @keyframes 본문은 vanilla 와 같은 글자다 (값을 다시 타이핑하지 않는다).
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./EventMoodCard.module.scss";
export type EventMoodCardProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
type Event = { num: string; mon: string; title: string; place: string; link: string; href: string; mood: string };
// 링크 글자는 조각으로 잇지 않는다 — 언어팩 사전의 키가 문장 전체여야 en·ja 판이 반쪽으로 안 나간다
const EVENTS: Event[] = [
{ num: "12", mon: "10월", title: "밤의 재즈 소품집", place: "연남 골목홀", link: "밤의 재즈 소품집 예매", href: "#jazz", mood: "moodA" },
{ num: "19", mon: "10월", title: "종이 공예 워크숍", place: "성수 작업실", link: "종이 공예 워크숍 예매", href: "#paper", mood: "moodB" },
{ num: "26", mon: "10월", title: "가을 야외 상영회", place: "한강 잔디마당", link: "가을 야외 상영회 예매", href: "#screen", mood: "moodC" },
];
function MapPin() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M128 24a80 80 0 0 0-80 80c0 72 80 128 80 128s80-56 80-128a80 80 0 0 0-80-80m0 112a32 32 0 1 1 32-32a32 32 0 0 1-32 32" opacity=".2" />
<path d="M128 64a40 40 0 1 0 40 40a40 40 0 0 0-40-40m0 64a24 24 0 1 1 24-24a24 24 0 0 1-24 24m0-112a88.1 88.1 0 0 0-88 88c0 31.4 14.51 64.68 42 96.25a254.2 254.2 0 0 0 41.45 38.3a8 8 0 0 0 9.18 0a254.2 254.2 0 0 0 41.37-38.3c27.45-31.57 42-64.85 42-96.25a88.1 88.1 0 0 0-88-88m0 206c-16.53-13-72-60.75-72-118a72 72 0 0 1 144 0c0 57.23-55.47 105-72 118" />
</g>
</svg>
);
}
export default function EventMoodCard({
duration = 600,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#2f6df6",
}: EventMoodCardProps) {
const [preview, setPreview] = useState(true);
const vars: Vars = {
"--duration": duration + "ms",
"--easing": easing,
"--color": color,
};
const stop = () => setPreview(false);
return (
<main className={styles.stage}>
<ul
className={styles.em + (preview ? " " + styles.isDemo : "")}
style={vars}
onPointerDown={stop}
onFocus={stop}
>
{EVENTS.map((ev) => (
<li className={styles.card} key={ev.href}>
<span className={styles.mood + " " + styles[ev.mood]} aria-hidden="true" />
<div className={styles.day}>
<b className={styles.num}>{ev.num}</b>
<span className={styles.mon}>{ev.mon}</span>
</div>
<div className={styles.body}>
<div className={styles.line}>
<h3 className={styles.title}>{ev.title}</h3>
</div>
<p className={styles.place}>
<span className={styles.ic} aria-hidden="true"><MapPin /></span>
{ev.place}
</p>
<a className={styles.link} href={ev.href}>{ev.link}</a>
</div>
</li>
))}
</ul>
</main>
);
}07 분위기를 먼저 보이는 이벤트 카드 — 측정 누적면적 7.618% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 카드 디자인 9종 — 도시·팀원·바로가기부터
이 데모가 속한 글
CSS 카드 디자인 9종 — 도시·팀원·바로가기부터CSS 카드 디자인은 화면에 내용을 담아 나열하는 가장 기본적인 그릇을 짜는 일입니다. 클릭은 카드 전체가 아니라 링크에 맡기는 게 이 편의 원칙이고, 도시 소개·팀원·바로가기·자기소개 탭·후기·제품 스펙·이벤트·인 이 글의 데모 전부 보기


