GODRICH

인용구 디자인 CSS 9종 — 블로그에 바로 붙이는 인용구 박스 코드

인용구 디자인은 남의 문장을 내 글 사이에 예쁘게 앉히는 인용구 박스를 말합니다. 이 페이지에는 그 박스 아홉 벌이 실제로 돌아가는 채로 들어 있고, 격자 아래 코드를 통째로 복사해 블로그에 바로 붙이는 데 씁니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 예쁜 순서가 아니라 인용구가 글에서 일하는 순서로 놓았습니다. 독자가 제일 먼저 만나는 기본형인 왼쪽 막대(01)에서 시작해, 장식이 문장보다 앞서 나오고(02), 문장이 본문을 가로지르며(03), 상자가 색으로 차오르고(04), 틀이 양 끝에서 조여들고(05), 말한 사람이 한 박자 늦게 마저 드러나고(06), 색이 아예 뒤집히며 무게가 실리고(07), 배경이 깊어지다가(08), 마지막에야 손이 닿으면 살아 움직입니다(09). 아홉 벌이 같은 색·간격·이징 토큰을 나눠 쓰므로 한 글에 몇 개를 섞어도 톤이 어긋나지 않습니다.

01왼쪽 막대가 자라며 열리는 인용구

블로그 본문의 기본 인용구입니다. 왼쪽의 굵은 막대가 scaleY 로 위에서 아래로 차게 자라 자리를 만들면 문장 둘과 출처가 한 박자씩 차이 나게 내려앉습니다. 막대와 문장이 동시에 움직이는 구간의 누적 변화 면적은 화면의 3.683% 로 측정됐고, 매거진 기사에 그대로 쓸 수 있는 무난한 첫 벌입니다.

scaleYtranslateYopacity
// 막대 — 위에서 아래로 자란다(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 을 함께 묶어야 뿌리가 박힌 채로 튑니다.

transform-originscaletranslateX
// 아이콘 팝 — 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 로만 움직이는데, 그 이유는 함정 절에서 측정값과 함께 적습니다.

translateYanimation-delayopacity
// 큰 문장 — 처음부터 불투명하게 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 로 깔아 둔 옅은 배경이 scaleXtransform-origin: left 를 얹어 왼쪽 끝에서부터 오른쪽으로 차오르고, 라벨은 채움이 절반 지날 때 컷으로, 문장은 채움을 따라 슬라이드하며 드러납니다(채움 면적이 커서 누적면적 21.95% 로 아홉 벌 중 가장 큽니다). 실사용에서는 상자에 손을 올리면 라벨 색이 transition-delay 80ms 로 한 박자 늦게 깊어지는 소질도 붙어 있습니다.

scaleXtransform-origintransition-delay
// 차오르는 배경 — 내용보다 아래 층(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 로 폰트를 바꿨고, 문장보다 위·아래로 크게 벌려 두는 게 이 벌의 핵심입니다.

translateXopacity::before
// 양 끝 따옴표 — 절대배치 글리프가 안으로 조여든다(불투명 유지, 페이드 없음)
.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% 까지 기다렸다 뒤따릅니다 — 이 배치가 왜 필요한지는 함정 절의 첫 이야기입니다.

translateYscaleXcolor
// 출처 줄 — 원(좌)과 이름·소속(우)을 가로로 잇고 아래 가장자리에 밑줄을 둔다
.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 은 아홉 벌 중 가장 강합니다.

clip-pathsteps(1, end)color
// 잉크 블록 위프와 문장 컷은 애니메이션을 나눈다 — 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% 동안 문장이 먼저 컷으로 등장해 겹치는 프레임을 만드는 것이 이 벌의 설계입니다.

opacityscalez-index
// 거대 따옴표 — 문장 뒤 한 층 아래(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 가 격자 루프를 떼고 다음 문장으로 넘기고, 「다음 인용」 버튼은 키보드 경로로 남아 있습니다.

steps(1, end)translateYgrid-area
// 세 문장을 한 칸에 겹친다 — 높이는 제일 긴 문장이 결정한다
.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 로 받습니다. 무대 색은 건드리지 않아도 됩니다 — 부품 색만 갈아 끼우는 구조입니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.