03Pull quote settling into the middle of the text
The large line caught between paragraphs drops in as two rows a beat apart, claiming the magazine pull-quote spot.
Best forMid-page landing emphasis, newsletters
translateYanimation-delayopacity

<main class="stage">
<section class="qp is-demo" aria-label="Pull quote settling mid-article">
<p class="qp__ctx qp__ctx--pre">Readers come down the page one line at a time.</p>
<blockquote class="qp__pull" >
<span class="qp__a">Speed</span>
<span class="qp__b">is a feature.</span>
</blockquote>
<p class="qp__ctx qp__ctx--post">One big line in that flow lets them breathe.</p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 매거진 pull quote — 본문 사이에 낀 큰 문장 두 줄이 animation-delay 로 한 박자씩 차이 나게 내려앉는다.
// 지연은 무한 루프에서도 위상차로 유지되고, animation-fill-mode: backwards 로 지연 구간에 첫 프레임(숨김)을 채운다
// (252-08 함정: fill 이 없으면 완성 상태가 0프레임에 번쩍한다).
$duration: $dur-loop;
$easing: $ease-spring;
$color: $stage-paper;
@include stage($stage-orange);
.qp {
box-sizing: border-box;
width: min(320px, 92vw);
margin: 0;
display: grid;
gap: $sp-4;
text-align: center;
font-family: inherit;
}
.qp * { box-sizing: border-box; }
.qp__ctx {
margin: 0;
font-size: 11px;
font-weight: 400;
line-height: 1.6;
color: $subject-ink;
}
.qp__pull {
margin: 0;
display: grid;
gap: $sp-1;
}
.qp__a,
.qp__b {
display: block;
font-size: 22px;
font-weight: 800;
line-height: 1.35;
color: $color;
letter-spacing: .04em;
}
// ── 미리보기 루프 ─────────────────
.qp.is-demo .qp__a,
.qp.is-demo .qp__b {
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.qp.is-demo .qp__a { animation-name: qpDropA; animation-delay: 60ms; }
.qp.is-demo .qp__b { animation-name: qpDropB; animation-delay: 160ms; }
// 맥락 줄(앞·뒤 지문)은 steps 컷 — 페이드 중간값이 poster 에 반투명 유령으로 남는다(237 함정)
.qp.is-demo .qp__ctx {
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.qp.is-demo .qp__ctx--pre { animation-name: qpCtxPre; }
.qp.is-demo .qp__ctx--post { animation-name: qpCtxPost; }
// 큰 문장 두 줄은 opacity 램프 없이 translateY 로만 내려앉는다(처음부터 불투명 — 텍스트 페이드의
// 중간값이 poster 에 유령으로 남는다). 두 줄이 겹쳐 내려오는 입장(60~400ms)이 최대 변화 프레임.
// 복귀는 다시 위로 올라갔다가 줄마다 다른 지점(86%·90%)에 멈춘다 — 마지막 샘플(95.8%) 전에 끝난다.
@keyframes qpDropA {
0%, 2% { opacity: 1; transform: translateY(18px); }
16%, 80% { opacity: 1; transform: translateY(0); }
86%, 100% { opacity: 1; transform: translateY(18px); }
}
@keyframes qpDropB {
0%, 6% { opacity: 1; transform: translateY(18px); }
22%, 76% { opacity: 1; transform: translateY(0); }
90%, 100% { opacity: 1; transform: translateY(18px); }
}
// 앞 지문: 큰 문장이 자리 잡기 전 컷, 큰 문장보다 먼저 물러난다
@keyframes qpCtxPre {
0%, 8% { opacity: 0; }
9%, 56% { opacity: 1; }
57%, 100% { opacity: 0; }
}
// 뒷 지문: 큰 문장이 다 내려앉은 뒤 컷, 앞 지문 다음에 물러난다
@keyframes qpCtxPost {
0%, 26% { opacity: 0; }
27%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.qp.is-demo .qp__a,
.qp.is-demo .qp__b,
.qp.is-demo .qp__ctx {
animation: none !important;
}
}
@media (max-width: 340px) {
.qp { gap: $sp-3; }
.qp__a, .qp__b { font-size: 20px; }
.qp__ctx { font-size: 10px; }
}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);
}
.qp {
box-sizing: border-box;
width: min(320px, 92vw);
margin: 0;
display: grid;
gap: 16px;
text-align: center;
font-family: inherit;
}
.qp * {
box-sizing: border-box;
}
.qp__ctx {
margin: 0;
font-size: 11px;
font-weight: 400;
line-height: 1.6;
color: #17141a;
}
.qp__pull {
margin: 0;
display: grid;
gap: 4px;
}
.qp__a,
.qp__b {
display: block;
font-size: 22px;
font-weight: 800;
line-height: 1.35;
color: #ffffff;
letter-spacing: 0.04em;
}
.qp.is-demo .qp__a,
.qp.is-demo .qp__b {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.qp.is-demo .qp__a {
animation-name: qpDropA;
animation-delay: 60ms;
}
.qp.is-demo .qp__b {
animation-name: qpDropB;
animation-delay: 160ms;
}
.qp.is-demo .qp__ctx {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.qp.is-demo .qp__ctx--pre {
animation-name: qpCtxPre;
}
.qp.is-demo .qp__ctx--post {
animation-name: qpCtxPost;
}
@keyframes qpDropA {
0%, 2% {
opacity: 1;
transform: translateY(18px);
}
16%, 80% {
opacity: 1;
transform: translateY(0);
}
86%, 100% {
opacity: 1;
transform: translateY(18px);
}
}
@keyframes qpDropB {
0%, 6% {
opacity: 1;
transform: translateY(18px);
}
22%, 76% {
opacity: 1;
transform: translateY(0);
}
90%, 100% {
opacity: 1;
transform: translateY(18px);
}
}
@keyframes qpCtxPre {
0%, 8% {
opacity: 0;
}
9%, 56% {
opacity: 1;
}
57%, 100% {
opacity: 0;
}
}
@keyframes qpCtxPost {
0%, 26% {
opacity: 0;
}
27%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.qp.is-demo .qp__a,
.qp.is-demo .qp__b,
.qp.is-demo .qp__ctx {
animation: none !important;
}
}
@media (max-width: 340px) {
.qp {
gap: 12px;
}
.qp__a, .qp__b {
font-size: 20px;
}
.qp__ctx {
font-size: 10px;
}
}// QuotePullCenter — vanilla 판(run/282/demo/03-quote-pull-center) 의 React 포트.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./QuotePullCenter.module.scss";
export type QuotePullCenterProps = {
preContext?: string;
pullA?: string;
pullB?: string;
postContext?: string;
groupLabel?: string;
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function QuotePullCenter({
preContext = "문서를 읽는 사람은 위에서 아래로 한 줄씩 내려온다.",
pullA = "속도는",
pullB = "기능이다.",
postContext = "그 흐름 속에서 큰 문장 하나가 숨을 고르게 한다.",
groupLabel = "본문 한가운데 내려앉는 강조 문장",
demo = true,
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#ffffff",
}: QuotePullCenterProps) {
const [live, setLive] = useState(demo);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function onEnter() {
setLive(false);
}
const cls = [styles.qp, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<section className={cls} style={vars} aria-label={groupLabel} onMouseEnter={onEnter}>
<p className={[styles.ctx, styles.ctxPre].join(" ")}>{preContext}</p>
<blockquote className={styles.pull}>
<span className={styles.a}>{pullA}</span>
<span className={styles.b}>{pullB}</span>
</blockquote>
<p className={[styles.ctx, styles.ctxPost].join(" ")}>{postContext}</p>
</section>
);
}03 Pull quote settling into the middle of the text — Measured area 4.517% · moving frames 12/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


