인용구 디자인 CSS 9종 — 블로그에 바로 붙이는 인용구 박스 코드
인용구 디자인은 남의 문장을 내 글 사이에 예쁘게 앉히는 인용구 박스를 말합니다. 이 페이지에는 그 박스 아홉 벌이 실제로 돌아가는 채로 들어 있고, 격자 아래 코드를 통째로 복사해 블로그에 바로 붙이는 데 씁니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 왼쪽 막대가 자라며 열리는 인용구
- 02 큰따옴표가 통 튀어나오는 인용구
- 03 본문 한가운데로 내려앉는 pull quote
- 04 배경이 좌에서 우로 차오르는 인용 박스
- 05 양 끝 따옴표가 안으로 미끄러지는 인용구
- 06 말한 사람이 한 박자 늦게 드러나는 출처 줄
- 07 검은 잉크 덩어리로 뒤집히는 인용구
- 08 등 뒤에 커다란 따옴표가 떠오르는 인용구
- 09 손을 얹으면 다음 문장으로 바뀌는 인용구
아홉 개는 예쁜 순서가 아니라 인용구가 글에서 일하는 순서로 놓았습니다. 독자가 제일 먼저 만나는 기본형인 왼쪽 막대(01)에서 시작해, 장식이 문장보다 앞서 나오고(02), 문장이 본문을 가로지르며(03), 상자가 색으로 차오르고(04), 틀이 양 끝에서 조여들고(05), 말한 사람이 한 박자 늦게 마저 드러나고(06), 색이 아예 뒤집히며 무게가 실리고(07), 배경이 깊어지다가(08), 마지막에야 손이 닿으면 살아 움직입니다(09). 아홉 벌이 같은 색·간격·이징 토큰을 나눠 쓰므로 한 글에 몇 개를 섞어도 톤이 어긋나지 않습니다.
01왼쪽 막대가 자라며 열리는 인용구
블로그 본문의 기본 인용구입니다. 왼쪽의 굵은 막대가 scaleY 로 위에서 아래로 차게 자라 자리를 만들면 문장 둘과 출처가 한 박자씩 차이 나게 내려앉습니다. 막대와 문장이 동시에 움직이는 구간의 누적 변화 면적은 화면의 3.683% 로 측정됐고, 매거진 기사에 그대로 쓸 수 있는 무난한 첫 벌입니다.
// 막대 — 위에서 아래로 자란다(scaleY). 컷이 아니라 스케일이라 면적이 프레임마다 나뉘어 들어온다
.qb__bar {
border-radius: $r-pill;
background: $color;
}
.qb.is-demo .qb__bar {
transform-origin: top center;
animation-name: qbBar;
animation-duration: $duration; // 2s 루프
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes qbBar {
0% { transform: scaleY(0); }
26%, 80% { transform: scaleY(1); }
90%, 100% { transform: scaleY(0); }
}
// 문장 둘은 같은 keyframes 에 백분율만 밀어 시차를 낸다 — animation-delay 를 안 써도 한 박자 차가 난다
@keyframes qbLine1 {
0%, 10% { opacity: 0; transform: translateY(8px); }
34%, 76% { opacity: 1; transform: translateY(0); }
80%, 100% { opacity: 0; }
}
02큰따옴표가 통 튀어나오는 인용구
도움말·가이드의 첫머리에 어울리는 벌입니다. 인용문 왼쪽의 큰따옴표 아이콘이 cubic-bezier(.2, 1.4, .4, 1) 커브로 한 번 튀어 올라 1.14배를 넘었다가 착지하고, 본문 두 줄이 왼쪽에서 밀려 들어옵니다(누적면적 3.828% · 강도 71.7). 튕김 커브는 y 값이 1을 넘는 특수한 이징이라 transform-origin: left bottom 을 함께 묶어야 뿌리가 박힌 채로 튑니다.
// 아이콘 팝 — y 가 1.4 를 넘는 커브로 지나쳤다 돌아온다(overshoot)
.qm.is-demo .qm__mark {
transform-origin: left bottom;
animation-name: qmMark;
animation-duration: $duration;
animation-timing-function: $ease-pop; // cubic-bezier(.2, 1.4, .4, 1)
animation-iteration-count: infinite;
}
@keyframes qmMark {
0% { opacity: 0; transform: scale(.2); }
6% { opacity: 1; transform: scale(.92); }
22% { transform: scale(1.14); } // 착지 전 한 번 넘치는 지점
30%, 78% { opacity: 1; transform: scale(1); }
90%, 100% { opacity: 0; transform: scale(.9); }
}
// 본문 슬라이드는 animation-delay 로 시차 — 무한 루프에서도 위상이 그대로 유지된다
.qm.is-demo .qm__l2 { animation-delay: 180ms; animation-fill-mode: backwards; }
03본문 한가운데로 내려앉는 pull quote
매거진이 본문 사이에 박아 넣는 큰 문장입니다. 앞뒤 지문(맥락 줄)은 steps(1, end) 컷으로 먼저 깔리고, 큰 문장 두 줄이 animation-delay 60ms·160ms 의 시차로 내려앉습니다(누적면적 4.747% · 강도 119.7). 두 줄 모두 opacity 램프를 쓰지 않고 translateY 로만 움직이는데, 그 이유는 함정 절에서 측정값과 함께 적습니다.
// 큰 문장 — 처음부터 불투명하게 18px 위에서 내려앉는다(페이드 중간값이 없다)
.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__a,
.qp.is-demo .qp__b {
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards; // 지연 구간에 첫 프레임을 채운다
}
@keyframes qpDropA {
0%, 2% { opacity: 1; transform: translateY(18px); }
16%, 80% { opacity: 1; transform: translateY(0); }
86%, 100% { opacity: 1; transform: translateY(18px); }
}
04배경이 좌에서 우로 차오르는 인용 박스
요점·팁 박스에 가까운 벌입니다. ::before 로 깔아 둔 옅은 배경이 scaleX 에 transform-origin: left 를 얹어 왼쪽 끝에서부터 오른쪽으로 차오르고, 라벨은 채움이 절반 지날 때 컷으로, 문장은 채움을 따라 슬라이드하며 드러납니다(채움 면적이 커서 누적면적 21.95% 로 아홉 벌 중 가장 큽니다). 실사용에서는 상자에 손을 올리면 라벨 색이 transition-delay 80ms 로 한 박자 늦게 깊어지는 소질도 붙어 있습니다.
// 차오르는 배경 — 내용보다 아래 층(z-index)에 깔고 좌측 원점에서 scaleX 로 채운다
.qt::before {
content: "";
position: absolute;
inset: 0;
z-index: 0;
background: $color; // 라일락
opacity: .3;
}
.qt.is-demo::before {
transform-origin: left center;
animation-name: qtFill;
animation-duration: $duration;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
}
@keyframes qtFill {
0% { transform: scaleX(0); }
18%, 74% { transform: scaleX(1); }
96%, 100% { transform: scaleX(0); } // 걷어냄은 입장(18%)보다 느리게 — 퇴장 프레임이 poster 를 이기지 않게
}
05양 끝 따옴표가 안으로 미끄러지는 인용구
인터뷰 기사·리서치 리포트용입니다. 인용문 양 끝에 ::before·::after 로 얹은 커다란 세리프 따옴표 글리프가 바깥에서 안으로 미끄러져 들어와 문장을 틀 안에 가두고, 문장은 가운데서 한 번 떠오릅니다(누적면적 4.822%). 따옴표는 아이콘이 아니라 타이포그래피 글리프라 font-family: Georgia 로 폰트를 바꿨고, 문장보다 위·아래로 크게 벌려 두는 게 이 벌의 핵심입니다.
// 양 끝 따옴표 — 절대배치 글리프가 안으로 조여든다(불투명 유지, 페이드 없음)
.qs__body::before,
.qs__body::after {
position: absolute;
z-index: -1;
font-family: Georgia, "Times New Roman", serif;
font-size: 52px;
color: $subject-lilac;
opacity: .9;
}
.qs__body::before { content: "\201C"; top: -28px; left: -20px; }
.qs__body::after { content: "\201D"; bottom: -28px; right: -20px; }
@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); }
}
// 닫는 따옴표(qsMarkAfter)는 방향만 +30px 로 반대 — 두 글리프가 같은 프레임에 움직여 입장이 최대 변화가 된다
06말한 사람이 한 박자 늦게 드러나는 출처 줄
에디토리얼 인터뷰·고객 문구에 쓰는 벌입니다. 본문이 먼저 자리를 잡은 뒤 이니셜 원이 scale 로 튀어나오고 이름·소속이 따라 올라오며, 마지막에 밑줄이 좌에서 우로 scaleX 차오릅니다(누적면적 3.433% · 강도 111.5). 본문·원·이름의 등장이 6~24% 구간에 겹쳐 있어 한 프레임에 여럿이 함께 움직이고, 밑줄만 28% 까지 기다렸다 뒤따릅니다 — 이 배치가 왜 필요한지는 함정 절의 첫 이야기입니다.
// 출처 줄 — 원(좌)과 이름·소속(우)을 가로로 잇고 아래 가장자리에 밑줄을 둔다
.qc__src {
display: flex;
align-items: center;
gap: $sp-3;
padding-bottom: $sp-2;
position: relative;
}
.qc__src::after { // 밑줄 — 좌측 원점에서 채워진다
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 2px;
border-radius: $r-pill;
background: rgba(23, 20, 26, .8);
transform-origin: left center;
}
@keyframes qcDisc { // 이니셜 원 — 본문과 겹쳐 튀어나옴(overshoot)
0%, 6% { opacity: 0; transform: scale(.3); }
18% { opacity: 1; transform: scale(1.12); }
24%, 62% { opacity: 1; transform: scale(1); }
76%, 100% { opacity: 0; }
}
07검은 잉크 덩어리로 뒤집히는 인용구
포트폴리오 선언문·제품 철학처럼 무게를 실어 말하고 싶을 때의 벌입니다. 잉크색 블록이 clip-path inset 으로 좌에서 우로 닦이며 나타나고, 그 위의 흰 문장이 steps(1, end) 컷으로 한 번에 뒤집힙니다 — steps 타이밍과 clip-path 를 한 keyframes 에 섞으면 보간이 죽으므로 블록(위프)과 문장(컷)을 애니메이션 둘로 나눴습니다. 잉크 배경 위 흰 글자의 대비는 18.24:1 이고, 누적면적은 20.317% 이며 강도 203.3 은 아홉 벌 중 가장 강합니다.
// 잉크 블록 위프와 문장 컷은 애니메이션을 나눈다 — steps 와 clip-path 를 섞으면 보간이 조용히 죽는다
.qi.is-demo {
animation-name: qiWipe; // ease 위프
animation-timing-function: $ease-out;
}
.qi.is-demo .qi__l1,
.qi.is-demo .qi__l2 {
animation-name: qiCut1;
animation-timing-function: steps(1, end); // 한 프레임 컷
animation-iteration-count: infinite;
}
@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; }
}
08등 뒤에 커다란 따옴표가 떠오르는 인용구
에세이의 여는 글·책 표지 문구에 어울리는 벌입니다. 등 뒤로 120px짜리 큰따옴표 글리프가 서서히 떠올라(라일락 22%) 배경 무늬가 되고, 전경 문장은 z-index 로 한 층 위에 또렷하게 남습니다(누적면적 4.192%). 워터마크가 천천히 자라는 46% 동안 문장이 먼저 컷으로 등장해 겹치는 프레임을 만드는 것이 이 벌의 설계입니다.
// 거대 따옴표 — 문장 뒤 한 층 아래(z-index: 0)에서 천천히 떠오른다
.qw__mark {
position: absolute;
z-index: 0;
font-family: Georgia, "Times New Roman", serif;
font-size: 120px;
color: $subject-lilac;
opacity: .22;
}
// 문장은 steps 컷 등장 + translateY 상승 — 키프레임 안 timing-function 으로 구간별 타이밍을 나눈다
@keyframes qwBody {
0% { opacity: 0; transform: translateY(10px); animation-timing-function: steps(1, end); }
10% { opacity: 1; transform: translateY(10px); animation-timing-function: $easing; }
30%, 60% { opacity: 1; transform: translateY(0); }
74%, 100% { opacity: 0; }
}
09손을 얹으면 다음 문장으로 바뀌는 인용구
랜딩 히어로 아래에서 돌아가는 문구입니다. 세 인용을 grid-area: 1 / 1 로 한 칸에 겹쳐 두고 한 번에 하나만 보이는데, 격자 미리보기에서는 steps(1, end) 컷이 33%·67% 지점에 문장을 갈아 끼웁니다(누적면적 1.697% 로 가장 조용하지만 강도 96.7 로 선명). 카드에 마우스를 올리면 JS 가 격자 루프를 떼고 다음 문장으로 넘기고, 「다음 인용」 버튼은 키보드 경로로 남아 있습니다.
// 세 문장을 한 칸에 겹친다 — 높이는 제일 긴 문장이 결정한다
.qx__swap { display: grid; min-height: 48px; }
.qx__line {
grid-area: 1 / 1;
opacity: 0;
transition-property: opacity;
transition-duration: 140ms; // 실사용 교체는 짧은 오퍼시티 전환
}
.qx__line.is-on { opacity: 1; }
// 격자 미리보기 루프는 컷 교체 — 33% 정각에 두 문장이 동시에 바뀐다(반투명 겹침도 빈 칸도 없다)
@keyframes qxLine1 {
0% { opacity: 1; }
33%, 100% { opacity: 0; }
}
@keyframes qxLine2 {
0% { opacity: 0; }
33% { opacity: 1; }
67%, 100% { opacity: 0; }
}
어디서 깨지는가 — 함정
가장 먼저 걸린 것은 대표 이미지가 텅 빈 화면이 되는 것이었습니다. 06 의 첫 판은 미리보기 루프에서 네 요소(본문·원·이름·밑줄)를 순서대로 하나씩만 세우다가, 본문이 한 프레임 컷으로 통째로 사라지는 순간의 변화량이 입장 전체를 이겨 누적 변화 면적 0.1% 짜리 빈 무대가 poster 로 나갔습니다. 고친 방법은 두 가지입니다 — 등장을 6~24% 구간에 겹쳐 여럿이 같은 프레임에 움직이게 하고, 물러남은 요소마다 서로 다른 구간에 12% 이상 걸친 페이드로 흘렸습니다. 다시 잰 06 은 누적면적 3.433% 에 움직인 프레임 18/23 으로 돌아왔습니다(24 장을 찍어 앞뒤를 견주므로 견줄 칸은 23 개입니다).
두 번째는 텍스트 페이드의 반투명 유령이었습니다. 03 의 큰 문장을 처음엔 opacity 램프로 띄웠다가, 프레임마다 반쯤 투명한 글자가 겹쳐 읽히는 장면이 그대로 poster 로 뽑혔습니다 — 픽셀로 되짚으니 그 프레임의 문장 혼합률이 0.5 안팎이었습니다. 글자가 들어가는 자리는 램프를 통째로 버렸습니다. 03 은 문장이 처음부터 불투명한 채 translateY 로만 내려앉고, 08 은 steps(1, end) 컷으로 등장한 뒤 움직이고, 맥락 줄도 전부 컷입니다. 고친 뒤 03 의 문장 혼합률은 0.87 이상으로 올라왔습니다.
세 번째는 09 의 이중 진행이었습니다. 카드에 마우스를 올리면 다음 문장으로 넘기고 버튼도 같은 일을 하게 했더니, 마우스로 버튼을 누르는 순간 진입(진행 1)과 클릭(진행 2)이 연달아 붙어 두 문장을 건너뛰었습니다. 버튼은 event.detail 이 0 인 키보드 활성화만 받도록 가려놨습니다 — 마우스 진입은 hover 가, 키보드는 버튼이 각자 한 번씩 담당합니다. 격자 미리보기의 컷 지점을 33%·67% 정각으로 맞춘 것도 같은 계열입니다 — 한쪽 문장이 사라진 뒤 다른 쪽이 나타나면 한 프레임짜리 빈 칸이 생겨 깜빡입니다.
마지막으로 무대 색이 아니라 무대 밖이었습니다. 05 의 큰 따옴표 글리프는 position: absolute 로 문장 위아래에 삐져나와 있는데, 회전·확대로 커진 경계 상자는 overflow: hidden 이 걸려 있어도 scrollHeight 에 그대로 잡힙니다. 글리프의 오프셋을 위아래 28px 안에 묶어 320px 폰 화면(세로 여유 174px)에서도 넘침이 0 이 되게 미리 계산해 두었습니다. 아홉 벌의 소스는 이 글이 잰 값 그대로 zip 에 들어 있고, 여는 비밀번호는 zqpvs5yk 입니다.
접근성
움직임을 줄여 달라는 설정(prefers-reduced-motion: reduce)에서 아홉 벌은 전부 같은 원칙을 따릅니다 — 스스로 도는 등장 연출은 끄고, 인용구 자체는 완성된 상태로 남깁니다. 정적 규칙을 먼저 쓰고 그 위에 .is-demo 루프를 덧씌우는 구조라서 애니메이션을 꺼도 01 의 막대는 차 있고, 04 의 박스는 채워져 있고, 08 의 워터마크는 떠 있습니다. reduce 블록의 선언에는 전부 !important 가 붙어 있는데, 미리보기 루프 쪽이 클래스를 겹쳐 쌓은 모양이라 특이도에서 밀리지 않게 하려는 것입니다. 09 는 컷 교체 대신 첫 문장이 고정되고 버튼 경로는 그대로 살아 있습니다. 기준은 MDN 의 prefers-reduced-motion 설명을 따랐습니다.
글자 대비는 픽셀 값을 직접 잤습니다.
| 자리 | 앞·뒤 색 | 실측 대비 | 기준 |
|---|---|---|---|
| 01 카드 글자 | 잉크 / 크림 | 17.11:1 | AA · AAA 통과 |
| 02·06 무대 글자 | 잉크 / 옐로 | 12.63:1 | AA · AAA 통과 |
| 03 큰 문장(22px 800) | 흰 / 오렌지 | 3.32:1 | AA 큰 글자 통과 |
| 07 블록 글자 | 흰 / 잉크 | 18.24:1 | AA · AAA 통과 |
| 04·08 무대 글자 | 잉크 / 흰 | 18.24:1 | AA · AAA 통과 |
| 03 맥락 줄(11px) | 크림 88% / 오렌지 | 2.7:1 | 본문용 아님 — 무대 장식 |
03 의 맥락 줄은 데모 무대를 꾸미는 지문일 뿐이라 실제 글에는 쓰지 않는 값입니다. 큰 문장만 진짜 부품이고 22px 굵은 글자라 AA 큰 글자 기준(3:1)을 넘습니다. 인용 자체의 의미는 HTML 이 갖고 있으므로 — <blockquote> 로 감싸고 09 처럼 바뀌는 문장에는 aria-live="polite" 를, 안 보이는 겹침 문장에는 aria-hidden 을 붙이는 것이 이 페이지의 원칙입니다. 인용 요소의 표준은 MDN 의 blockquote 문서에 정리되어 있습니다.
글자를 움직이는 더 많은 방법은 CSS 텍스트 애니메이션 9종에, 남의 후기를 진열하는 섹션은 후기·소셜 프루프 섹션 9종에 각각 모여 있고, 이 사이트가 데모를 어떻게 렌더하고 잰 뒤에 글을 쓰는지는 소개에 적어 두었습니다.
FAQ
인용 부호는 CSS ::before 로 넣어도 되나요?
됩니다. 05 처럼 content: "\201C" 로 넣으면 HTML 이 깨끗해지고, CSS 가 만든 글자를 낭독기마다 다르게 다루므로 순전한 장식은 본문 밖에 두는 편이 낫습니다. 다만 따옴표가 문장의 뜻에 필요하다면 HTML 안에 직접 넣는 쪽이 안전합니다.
pull quote 와 blockquote 는 다른 태그인가요?
아닙니다. HTML 태그는 <blockquote> 하나이고, pull quote 는 그 쓰임의 이름입니다 — 남의 글을 인용하는지(01·02·04), 내 글에서 한 문장을 크게 뽑아 강조하는지(03·07·08)의 차이일 뿐입니다. 아홉 벌 사이에서 달라지는 것은 크기와 자리이지 고르는 요소가 아닙니다 — 바깥 껍질까지 같지는 않아서 09 는 <blockquote> 없이 <figure> 안에 <p> 세 줄을 두었고, 03 은 앞뒤 맥락 문장까지 <section> 으로 감쌌습니다.
제 블로그 색에 맞게 바꾸려면 어디를 고치나요?
각 벌의 style.scss 맨 위에 있는 $duration·$easing·$color 세 변수만 바꾸면 됩니다. $color 가 막대(01)·채움 배경(04)·블록(07)의 색이 되고, React 판은 같은 자리를 props 로 받습니다. 무대 색은 건드리지 않아도 됩니다 — 부품 색만 갈아 끼우는 구조입니다.