05Quote framed by marks sliding inward
Oversized marks at both ends slide inward and tighten the frame, and the line lifts once in the middle.
Best forInterview articles, research reports
translateXopacity::before

<main class="stage">
<figure class="qs is-demo" aria-label="Quote framed by marks sliding inward">
<blockquote class="qs__body">Choose the wrong question,<br>and even a good answer is useless.</blockquote>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
// 인터뷰용 인용구 — ::before/::after 로 얹은 커다란 따옴표 글리프가 바깥에서 안으로 미끄러져
// 들어와 문장을 틀 안에 가두고, 문장은 가운데서 한 번 떠오른다.
// 따옴표는 아이콘이 아니라 타이포그래피 글리프(세리프) — 문장보다 크게 벌려 두는 게 핵심.
$duration: $dur-loop;
$easing: $ease-spring;
$color: $stage-paper;
@include stage($stage-ink);
.qs {
box-sizing: border-box;
width: min(320px, 92vw);
margin: 0;
padding: $sp-6 $sp-8;
font-family: inherit;
}
.qs * { box-sizing: border-box; }
.qs__body {
position: relative;
z-index: 1;
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.7;
text-align: center;
color: $color;
}
// 양 끝 따옴표 — 문장보다 위·아래로 크게 벌어져 있는 글리프
.qs__body::before,
.qs__body::after {
position: absolute;
z-index: -1;
font-family: Georgia, "Times New Roman", serif;
font-size: 52px;
font-weight: 700;
line-height: 1;
color: $subject-lilac;
opacity: .9;
pointer-events: none;
}
.qs__body::before { content: "\201C"; top: -28px; left: -20px; }
.qs__body::after { content: "\201D"; bottom: -28px; right: -20px; }
// ── 미리보기 루프 ─────────────────
.qs.is-demo .qs__body {
animation-name: qsLift;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.qs.is-demo .qs__body::before {
animation-name: qsMarkBefore;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.qs.is-demo .qs__body::after {
animation-name: qsMarkAfter;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
// 문장·따옴표 모두 opacity 램프를 쓰지 않는다 — 텍스트 페이드의 중간값이 poster 에
// 반투명 유령으로 남는다(237 계열 함정). 문장은 translateY 로, 따옴표는 슬라이드+스케일로
// 처음부터 불투명하게 움직인다. 복귀: 문장은 되돌아 내려가고, 따옴표는 바깥으로 슬라이드.
@keyframes qsLift {
0%, 20% { opacity: 1; transform: translateY(10px); }
32%, 68% { opacity: 1; transform: translateY(0); }
74%, 100% { opacity: 1; transform: translateY(10px); }
}
// 여는 따옴표: 왼바깥 → 제자리 → 다시 바깥으로(불투명 유지)
@keyframes qsMarkBefore {
0% { opacity: .9; transform: translateX(-30px) scale(.7); }
28%, 68% { opacity: .9; transform: translateX(0) scale(1); }
92%, 100% { opacity: .9; transform: translateX(-30px) scale(.7); }
}
// 닫는 따옴표: 오른바깥 → 제자리 → 다시 바깥으로(같은 시간, 반대 방향 — 두 글리프가
// 동시에 움직이는 입장 프레임이 최대 변화)
@keyframes qsMarkAfter {
0% { opacity: .9; transform: translateX(30px) scale(.7); }
28%, 68% { opacity: .9; transform: translateX(0) scale(1); }
92%, 100% { opacity: .9; transform: translateX(30px) scale(.7); }
}
@media (prefers-reduced-motion: reduce) {
.qs.is-demo .qs__body,
.qs.is-demo .qs__body::before,
.qs.is-demo .qs__body::after {
animation: none !important;
}
}
@media (max-width: 340px) {
.qs { padding: $sp-5 $sp-6; }
.qs__body { font-size: 14px; }
.qs__body::before, .qs__body::after { font-size: 44px; }
.qs__body::before { top: -24px; left: -16px; }
.qs__body::after { bottom: -24px; right: -16px; }
}@charset "UTF-8";
html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.qs {
box-sizing: border-box;
width: min(320px, 92vw);
margin: 0;
padding: 24px 32px;
font-family: inherit;
}
.qs * {
box-sizing: border-box;
}
.qs__body {
position: relative;
z-index: 1;
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.7;
text-align: center;
color: #ffffff;
}
.qs__body::before,
.qs__body::after {
position: absolute;
z-index: -1;
font-family: Georgia, "Times New Roman", serif;
font-size: 52px;
font-weight: 700;
line-height: 1;
color: #b9a0ff;
opacity: 0.9;
pointer-events: none;
}
.qs__body::before {
content: "“";
top: -28px;
left: -20px;
}
.qs__body::after {
content: "”";
bottom: -28px;
right: -20px;
}
.qs.is-demo .qs__body {
animation-name: qsLift;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.qs.is-demo .qs__body::before {
animation-name: qsMarkBefore;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.qs.is-demo .qs__body::after {
animation-name: qsMarkAfter;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@keyframes qsLift {
0%, 20% {
opacity: 1;
transform: translateY(10px);
}
32%, 68% {
opacity: 1;
transform: translateY(0);
}
74%, 100% {
opacity: 1;
transform: translateY(10px);
}
}
@keyframes qsMarkBefore {
0% {
opacity: 0.9;
transform: translateX(-30px) scale(0.7);
}
28%, 68% {
opacity: 0.9;
transform: translateX(0) scale(1);
}
92%, 100% {
opacity: 0.9;
transform: translateX(-30px) scale(0.7);
}
}
@keyframes qsMarkAfter {
0% {
opacity: 0.9;
transform: translateX(30px) scale(0.7);
}
28%, 68% {
opacity: 0.9;
transform: translateX(0) scale(1);
}
92%, 100% {
opacity: 0.9;
transform: translateX(30px) scale(0.7);
}
}
@media (prefers-reduced-motion: reduce) {
.qs.is-demo .qs__body,
.qs.is-demo .qs__body::before,
.qs.is-demo .qs__body::after {
animation: none !important;
}
}
@media (max-width: 340px) {
.qs {
padding: 20px 24px;
}
.qs__body {
font-size: 14px;
}
.qs__body::before, .qs__body::after {
font-size: 44px;
}
.qs__body::before {
top: -24px;
left: -16px;
}
.qs__body::after {
bottom: -24px;
right: -16px;
}
}// QuoteBracketSlide — vanilla 판(run/282/demo/05-quote-bracket-slide) 의 React 포트.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./QuoteBracketSlide.module.scss";
export type QuoteBracketSlideProps = {
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 QuoteBracketSlide({
line1 = "물음을 잘못 골랐다면",
line2 = "좋은 답도 소용이 없다.",
groupLabel = "양 끝 따옴표가 닫히는 인용구",
demo = true,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#ffffff",
}: QuoteBracketSlideProps) {
const [live, setLive] = useState(demo);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function onEnter() {
setLive(false);
}
const cls = [styles.qs, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<figure className={cls} style={vars} aria-label={groupLabel} onMouseEnter={onEnter}>
<blockquote className={styles.body}>{line1}<br />{line2}</blockquote>
</figure>
);
}05 Quote framed by marks sliding inward — Measured area 4.822% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · Blockquote CSS: 9 Quote Box Designs (Copy-Paste)
The post this demo lives in
Blockquote CSS: 9 Quote Box Designs (Copy-Paste)Blockquote CSS places another writer's sentence inside your own text. Nine boxes run live below, every snippet Copy-Past See all demos in this post

