07검은 잉크 덩어리로 뒤집히는 인용구
인용 문장이 잉크색 블록 위 흰 글자로 한 번에 뒤집히고, 블록 가장자리가 clip-path 로 닦이며 각을 이룹니다.
쓰임포트폴리오 선언문, 제품 철학
clip-pathsteps(1, end)color

<main class="stage">
<figure class="qi is-demo" aria-label="잉크 블록으로 뒤집히는 인용구">
<blockquote class="qi__body">
<span class="qi__l1">나는 어제의 화면을 고치지 않고,</span>
<span class="qi__l2">내일의 화면을 더 잘 만든다.</span>
</blockquote>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
// 포트폴리오 선언문용 인용구 — 잉크색 블록이 clip-path inset 으로 좌에서 우로 닦이며 나타나고,
// 그 위의 흰 문장이 steps(1, end) 컷으로 한 번에 뒤집힌다(잉크 배경 위 흰 글자 — 대비 18.24:1).
// 266-09 함정: steps 타이밍과 clip-path 를 한 keyframes 에 섞으면 보간이 죽는다 — 블록(ease 위프)과
// 문장(steps 컷)을 애니메이션 둘로 분리했다. 복귀: 문장 컷 → 블록 위프로 우에서 좌로 닦아 나감(입장 16% vs 퇴장 18%).
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-ink;
@include stage($stage-orange);
.qi {
box-sizing: border-box;
width: min(320px, 92vw);
margin: 0;
padding: $sp-5 $sp-6;
border-radius: $r-card;
background: $color;
color: $stage-paper;
font-family: inherit;
}
.qi * { box-sizing: border-box; }
.qi__body {
margin: 0;
display: grid;
gap: $sp-2;
}
.qi__l1,
.qi__l2 {
display: block;
font-size: 14px;
font-weight: 700;
line-height: 1.6;
color: inherit;
}
// ── 미리보기 루프 ─────────────────
.qi.is-demo {
animation-name: qiWipe;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.qi.is-demo .qi__l1,
.qi.is-demo .qi__l2 {
animation-name: qiCut1;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.qi.is-demo .qi__l2 { animation-name: qiCut2; }
// 잉크 블록: 0~16% 좌→우 위프로 닦여 나타남(빠르게 — 문장 컷과 겹치는 프레임이 최대 변화)
@keyframes qiWipe {
0% { clip-path: inset(0 100% 0 0 round 16px); }
16%, 74% { clip-path: inset(0 0 0 0 round 16px); }
92%, 100% { clip-path: inset(0 100% 0 0 round 16px); }
}
// 문장 1: 블록이 다 찬 직후 한 프레임 컷
@keyframes qiCut1 {
0%, 17% { opacity: 0; }
21%, 70% { opacity: 1; }
74%, 100% { opacity: 0; }
}
// 문장 2: 한 프레임 뒤
@keyframes qiCut2 {
0%, 19% { opacity: 0; }
23%, 68% { opacity: 1; }
72%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.qi.is-demo,
.qi.is-demo .qi__l1,
.qi.is-demo .qi__l2 {
animation: none !important;
}
}
@media (max-width: 340px) {
.qi { padding: $sp-4 $sp-5; }
.qi__l1, .qi__l2 { font-size: 13px; }
}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);
}
.qi {
box-sizing: border-box;
width: min(320px, 92vw);
margin: 0;
padding: 20px 24px;
border-radius: 16px;
background: #17141a;
color: #ffffff;
font-family: inherit;
}
.qi * {
box-sizing: border-box;
}
.qi__body {
margin: 0;
display: grid;
gap: 8px;
}
.qi__l1,
.qi__l2 {
display: block;
font-size: 14px;
font-weight: 700;
line-height: 1.6;
color: inherit;
}
.qi.is-demo {
animation-name: qiWipe;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.qi.is-demo .qi__l1,
.qi.is-demo .qi__l2 {
animation-name: qiCut1;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.qi.is-demo .qi__l2 {
animation-name: qiCut2;
}
@keyframes qiWipe {
0% {
clip-path: inset(0 100% 0 0 round 16px);
}
16%, 74% {
clip-path: inset(0 0 0 0 round 16px);
}
92%, 100% {
clip-path: inset(0 100% 0 0 round 16px);
}
}
@keyframes qiCut1 {
0%, 17% {
opacity: 0;
}
21%, 70% {
opacity: 1;
}
74%, 100% {
opacity: 0;
}
}
@keyframes qiCut2 {
0%, 19% {
opacity: 0;
}
23%, 68% {
opacity: 1;
}
72%, 100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.qi.is-demo,
.qi.is-demo .qi__l1,
.qi.is-demo .qi__l2 {
animation: none !important;
}
}
@media (max-width: 340px) {
.qi {
padding: 16px 20px;
}
.qi__l1, .qi__l2 {
font-size: 13px;
}
}// QuoteInkInvert — vanilla 판(run/282/demo/07-quote-ink-invert) 의 React 포트.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./QuoteInkInvert.module.scss";
export type QuoteInkInvertProps = {
line1?: string;
line2?: string;
groupLabel?: string;
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function QuoteInkInvert({
line1 = "나는 어제의 화면을 고치지 않고,",
line2 = "내일의 화면을 더 잘 만든다.",
groupLabel = "잉크 블록으로 뒤집히는 인용구",
demo = true,
duration = 2000,
easing = "ease-out",
color = "#17141a",
}: QuoteInkInvertProps) {
const [live, setLive] = useState(demo);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function onEnter() {
setLive(false);
}
const cls = [styles.qi, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<figure className={cls} style={vars} aria-label={groupLabel} onMouseEnter={onEnter}>
<blockquote className={styles.body}>
<span className={styles.l1}>{line1}</span>
<span className={styles.l2}>{line2}</span>
</blockquote>
</figure>
);
}07 검은 잉크 덩어리로 뒤집히는 인용구 — 측정 누적면적 20.317% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 인용구 디자인 CSS 9종 — 블로그에 바로 붙이는 인용구 박스 코드
이 데모가 속한 글
인용구 디자인 CSS 9종 — 블로그에 바로 붙이는 인용구 박스 코드인용구 디자인은 남의 문장을 내 글 사이에 예쁘게 앉히는 인용구 박스를 말합니다. 이 페이지에는 그 박스 아홉 벌이 실제로 돌아가는 채로 들어 있고, 격자 아래 코드를 통째로 복사해 블로그에 바로 붙이는 데 씁니다. 이 글의 데모 전부 보기

