06폰트 해부 카드
큰 글자 위에 기준선·x 높이·대문자 높이 줄을 겹쳐 그립니다. x 높이 줄은 ex 단위로 그려 serif 와 sans 의 얼굴 크기 차이가 한눈에 드러납니다.
쓰임타이포그래피 가이드·강의 자료
::beforelinear-gradientex

<main class="stage">
<div class="fa is-demo is-serif" id="fa">
<div class="fa__bar" role="group" aria-label="표본 고르기">
<button class="fa__pick" type="button" data-face="serif" aria-pressed="true">세리프</button>
<button class="fa__pick" type="button" data-face="sans" aria-pressed="false">산세리프</button>
</div>
<div class="fa__cards">
<figure class="fa__card fa__card--serif">
<span class="fa__sample" aria-hidden="true">Hxg</span>
<span class="fa__rule fa__rule--cap"><span class="fa__tag">대문자 높이</span></span>
<span class="fa__rule fa__rule--x"><span class="fa__tag">x 높이</span></span>
<span class="fa__rule fa__rule--base"><span class="fa__tag">기준선</span></span>
<figcaption class="fa__name">세리프</figcaption>
</figure>
<figure class="fa__card fa__card--sans">
<span class="fa__sample" aria-hidden="true">Hxg</span>
<span class="fa__rule fa__rule--cap"><span class="fa__tag">대문자 높이</span></span>
<span class="fa__rule fa__rule--x"><span class="fa__tag">x 높이</span></span>
<span class="fa__rule fa__rule--base"><span class="fa__tag">기준선</span></span>
<figcaption class="fa__name">산세리프</figcaption>
</figure>
</div>
<p class="fa__note">x 높이 줄은 ex 단위로 그려 서체마다 자리가 달라집니다</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 폰트 해부 카드 — 같은 글자를 세리프와 산세리프로 나란히 놓고 기준선·x 높이·대문자 높이 줄을 겹쳐 그린다.
// 줄은 요소를 더 만들지 않고 `::before` 한 겹에 linear-gradient 로 그린 파선이다.
// x 높이 줄만 진짜 서체 치수다 — `1ex` 는 그 서체의 소문자 x 높이라 카드마다 자리가 다르게 잡힌다.
// 기준선·대문자 높이 줄은 em 비율로 어림한 자리다(대부분의 라틴 서체에서 내림 .21em · 대문자 .70em).
// 미리보기(.is-demo)는 두 카드의 강조를 컷으로 주고받고, 그 사이를 재는 눈금 하나가 위아래로 움직인다.
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue-deep;
$serif: ui-serif, Georgia, "Times New Roman", "Noto Serif KR", serif;
$sans: system-ui, -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
$ink-soft: rgba($subject-ink, .72);
$desc: .21em;
$cap: .70em;
@include stage($stage-yellow);
.fa {
display: flex; flex-direction: column; gap: $sp-2;
box-sizing: border-box; width: min(100%, 430px);
padding: $sp-3; border-radius: $r-card;
background: $stage-paper;
}
// ── 표본 고르기
.fa__bar { display: flex; gap: $sp-2; }
.fa__pick {
padding: $sp-1 $sp-2; border: 0; border-radius: $r-pill;
background: rgba($subject-ink, .10); color: $subject-ink; cursor: pointer;
font-weight: 700; font-size: 11px; line-height: 1.4;
}
.fa__pick[aria-pressed="true"] { background: $color; color: $subject-cream; }
.fa__pick:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; }
// ── 카드 두 장
.fa__cards { display: grid; grid-template-columns: 1fr 1fr; gap: $sp-2; }
.fa__card {
position: relative; box-sizing: border-box;
height: 150px; margin: 0; padding: $sp-2;
border: 1px solid rgba($subject-ink, .14); border-radius: $r-sm;
background: $stage-paper; overflow: hidden;
}
.fa__card--serif { font-family: $serif; }
.fa__card--sans { font-family: $sans; }
// 글자는 카드 아래쪽에 앉는다. line-height 1 이라 상자 바닥이 곧 내림 끝이다
.fa__sample {
position: absolute; left: $sp-3; bottom: $sp-4;
color: $subject-ink; font-weight: 400; font-size: 52px; line-height: 1;
}
// 줄 세 개. 위치는 글자 상자 바닥에서 올라온 거리로 잡는다
.fa__rule {
position: absolute; left: $sp-2; right: $sp-2;
font-size: 52px; // ex 가 이 크기의 서체 치수로 풀리게 한다
pointer-events: none;
}
.fa__rule::before {
content: ""; display: block; height: 1px;
background: linear-gradient(90deg, $color 0 6px, transparent 6px 10px);
background-size: 10px 1px;
}
.fa__rule--base { bottom: calc(#{$sp-4} + #{$desc}); }
.fa__rule--x { bottom: calc(#{$sp-4} + #{$desc} + 1ex); }
.fa__rule--cap { bottom: calc(#{$sp-4} + #{$desc} + #{$cap}); }
// x 높이 줄만 어림이 아니라 실제 서체 치수(ex)다 — 파선이 아니라 실선 한 줄로 갈라 둔다
.fa__rule--x::before {
height: 2px;
background: linear-gradient(90deg, $subject-orange-deep 0 100%);
}
.fa__tag {
position: absolute; right: 0; bottom: 2px;
color: $color; font-family: system-ui, sans-serif; font-weight: 700; font-size: 9px; line-height: 1.2;
}
// 줄 셋이 11px 안에 모이는 서체가 있어 라벨은 좌우로 갈라 놓는다
.fa__rule--x .fa__tag { right: auto; left: 0; color: $subject-orange-deep; }
.fa__name {
position: absolute; left: $sp-2; bottom: $sp-1;
color: $ink-soft; font-family: system-ui, sans-serif; font-weight: 700; font-size: 10px; line-height: 1.2;
}
// 지금 보고 있는 카드에만 테두리가 선다
.fa.is-serif .fa__card--serif,
.fa.is-sans .fa__card--sans { border-color: $color; background: rgba($subject-blue, .07); }
.fa__note {
margin: 0; color: $ink-soft;
font-weight: 500; font-size: 11px; line-height: 1.4;
}
// ── 격자 미리보기 루프 (2초)
// 강조는 steps(1, end) 컷이라 두 카드가 반투명하게 겹치는 프레임이 없다.
// 눈금 하나가 기준선에서 대문자 높이까지 오르내리며 줄 사이의 거리를 가리킨다.
.fa.is-demo .fa__card--serif { animation-name: faHoldA; }
.fa.is-demo .fa__card--sans { animation-name: faHoldB; }
.fa.is-demo .fa__card {
animation-duration: $dur-loop; animation-timing-function: steps(1, end);
animation-iteration-count: infinite; animation-fill-mode: both;
}
.fa.is-demo .fa__pick:nth-child(1) { animation-name: faPickA; }
.fa.is-demo .fa__pick:nth-child(2) { animation-name: faPickB; }
.fa.is-demo .fa__pick {
animation-duration: $dur-loop; animation-timing-function: steps(1, end);
animation-iteration-count: infinite; animation-fill-mode: both;
}
.fa.is-demo .fa__rule--x {
animation-name: faSlide; animation-duration: $dur-loop;
animation-timing-function: $easing; animation-iteration-count: infinite;
animation-fill-mode: both;
}
.fa.is-demo .fa__sample {
animation-name: faLift; animation-duration: $dur-loop;
animation-timing-function: $easing; animation-iteration-count: infinite;
animation-fill-mode: both;
}
@keyframes faHoldA {
0% { border-color: $color; background-color: rgba($subject-blue, .07); }
50%, 100% { border-color: rgba($subject-ink, .14); background-color: $stage-paper; }
}
@keyframes faHoldB {
0% { border-color: rgba($subject-ink, .14); background-color: $stage-paper; }
50%, 100% { border-color: $color; background-color: rgba($subject-blue, .07); }
}
@keyframes faPickA {
0% { background-color: $color; color: $subject-cream; }
50%, 100% { background-color: rgba($subject-ink, .10); color: $subject-ink; }
}
@keyframes faPickB {
0% { background-color: rgba($subject-ink, .10); color: $subject-ink; }
50%, 100% { background-color: $color; color: $subject-cream; }
}
// x 높이 줄이 한 번 짚었다 제자리로 — 두 서체의 x 높이 차이를 눈으로 견주게 한다
@keyframes faSlide {
0%, 10% { transform: translateY(0); }
40%, 60% { transform: translateY(-10px); }
90%, 100% { transform: translateY(0); }
}
@keyframes faLift {
0%, 10% { transform: translateY(0); }
40%, 60% { transform: translateY(-4px); }
90%, 100% { transform: translateY(0); }
}
// ── 좁은 화면: 글자와 카드 높이를 줄여 무대 안에 들어오게 한다
@media (max-width: 340px) {
.fa { gap: $sp-1; padding: $sp-2; }
.fa__card { height: 106px; padding: $sp-1; }
.fa__sample { font-size: 38px; left: $sp-2; bottom: $sp-3; }
.fa__rule { font-size: 38px; left: $sp-1; right: $sp-1; }
.fa__rule--base { bottom: calc(#{$sp-3} + #{$desc}); }
.fa__rule--x { bottom: calc(#{$sp-3} + #{$desc} + 1ex); }
.fa__rule--cap { bottom: calc(#{$sp-3} + #{$desc} + #{$cap}); }
.fa__pick, .fa__note { font-size: 10px; }
.fa__tag { font-size: 8px; }
}
@media (prefers-reduced-motion: reduce) {
.fa.is-demo .fa__card,
.fa.is-demo .fa__pick,
.fa.is-demo .fa__rule--x,
.fa.is-demo .fa__sample {
animation: none !important;
transform: none !important;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.fa {
display: flex;
flex-direction: column;
gap: 8px;
box-sizing: border-box;
width: min(100%, 430px);
padding: 12px;
border-radius: 16px;
background: #ffffff;
}
.fa__bar {
display: flex;
gap: 8px;
}
.fa__pick {
padding: 4px 8px;
border: 0;
border-radius: 999px;
background: rgba(23, 20, 26, 0.1);
color: #17141a;
cursor: pointer;
font-weight: 700;
font-size: 11px;
line-height: 1.4;
}
.fa__pick[aria-pressed=true] {
background: #0a4fe8;
color: #fff7e6;
}
.fa__pick:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.fa__cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.fa__card {
position: relative;
box-sizing: border-box;
height: 150px;
margin: 0;
padding: 8px;
border: 1px solid rgba(23, 20, 26, 0.14);
border-radius: 8px;
background: #ffffff;
overflow: hidden;
}
.fa__card--serif {
font-family: ui-serif, Georgia, "Times New Roman", "Noto Serif KR", serif;
}
.fa__card--sans {
font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fa__sample {
position: absolute;
left: 12px;
bottom: 16px;
color: #17141a;
font-weight: 400;
font-size: 52px;
line-height: 1;
}
.fa__rule {
position: absolute;
left: 8px;
right: 8px;
font-size: 52px;
pointer-events: none;
}
.fa__rule::before {
content: "";
display: block;
height: 1px;
background: linear-gradient(90deg, #0a4fe8 0 6px, transparent 6px 10px);
background-size: 10px 1px;
}
.fa__rule--base {
bottom: calc(16px + 0.21em);
}
.fa__rule--x {
bottom: calc(16px + 0.21em + 1ex);
}
.fa__rule--cap {
bottom: calc(16px + 0.21em + 0.7em);
}
.fa__rule--x::before {
height: 2px;
background: linear-gradient(90deg, #c93000 0 100%);
}
.fa__tag {
position: absolute;
right: 0;
bottom: 2px;
color: #0a4fe8;
font-family: system-ui, sans-serif;
font-weight: 700;
font-size: 9px;
line-height: 1.2;
}
.fa__rule--x .fa__tag {
right: auto;
left: 0;
color: #c93000;
}
.fa__name {
position: absolute;
left: 8px;
bottom: 4px;
color: rgba(23, 20, 26, 0.72);
font-family: system-ui, sans-serif;
font-weight: 700;
font-size: 10px;
line-height: 1.2;
}
.fa.is-serif .fa__card--serif,
.fa.is-sans .fa__card--sans {
border-color: #0a4fe8;
background: rgba(47, 109, 246, 0.07);
}
.fa__note {
margin: 0;
color: rgba(23, 20, 26, 0.72);
font-weight: 500;
font-size: 11px;
line-height: 1.4;
}
.fa.is-demo .fa__card--serif {
animation-name: faHoldA;
}
.fa.is-demo .fa__card--sans {
animation-name: faHoldB;
}
.fa.is-demo .fa__card {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: both;
}
.fa.is-demo .fa__pick:nth-child(1) {
animation-name: faPickA;
}
.fa.is-demo .fa__pick:nth-child(2) {
animation-name: faPickB;
}
.fa.is-demo .fa__pick {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: both;
}
.fa.is-demo .fa__rule--x {
animation-name: faSlide;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: both;
}
.fa.is-demo .fa__sample {
animation-name: faLift;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: both;
}
@keyframes faHoldA {
0% {
border-color: #0a4fe8;
background-color: rgba(47, 109, 246, 0.07);
}
50%, 100% {
border-color: rgba(23, 20, 26, 0.14);
background-color: #ffffff;
}
}
@keyframes faHoldB {
0% {
border-color: rgba(23, 20, 26, 0.14);
background-color: #ffffff;
}
50%, 100% {
border-color: #0a4fe8;
background-color: rgba(47, 109, 246, 0.07);
}
}
@keyframes faPickA {
0% {
background-color: #0a4fe8;
color: #fff7e6;
}
50%, 100% {
background-color: rgba(23, 20, 26, 0.1);
color: #17141a;
}
}
@keyframes faPickB {
0% {
background-color: rgba(23, 20, 26, 0.1);
color: #17141a;
}
50%, 100% {
background-color: #0a4fe8;
color: #fff7e6;
}
}
@keyframes faSlide {
0%, 10% {
transform: translateY(0);
}
40%, 60% {
transform: translateY(-10px);
}
90%, 100% {
transform: translateY(0);
}
}
@keyframes faLift {
0%, 10% {
transform: translateY(0);
}
40%, 60% {
transform: translateY(-4px);
}
90%, 100% {
transform: translateY(0);
}
}
@media (max-width: 340px) {
.fa {
gap: 4px;
padding: 8px;
}
.fa__card {
height: 106px;
padding: 4px;
}
.fa__sample {
font-size: 38px;
left: 8px;
bottom: 12px;
}
.fa__rule {
font-size: 38px;
left: 4px;
right: 4px;
}
.fa__rule--base {
bottom: calc(12px + 0.21em);
}
.fa__rule--x {
bottom: calc(12px + 0.21em + 1ex);
}
.fa__rule--cap {
bottom: calc(12px + 0.21em + 0.7em);
}
.fa__pick, .fa__note {
font-size: 10px;
}
.fa__tag {
font-size: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.fa.is-demo .fa__card,
.fa.is-demo .fa__pick,
.fa.is-demo .fa__rule--x,
.fa.is-demo .fa__sample {
animation: none !important;
transform: none !important;
}
}var fa = document.getElementById('fa');
var picks = fa.querySelectorAll('.fa__pick');
// 실사용 경로 — 고른 표본 카드만 앞으로 나온다. 미리보기 루프는 같은 전환을 CSS 컷으로 돈다
picks.forEach(function (b) {
b.addEventListener('click', function () {
fa.classList.remove('is-demo');
var face = b.getAttribute('data-face');
fa.classList.toggle('is-serif', face === 'serif');
fa.classList.toggle('is-sans', face === 'sans');
picks.forEach(function (x) {
x.setAttribute('aria-pressed', x.getAttribute('data-face') === face ? 'true' : 'false');
});
});
});// FontAnatomyCards — vanilla 판(run/074/demo/06-font-anatomy-cards/index.html) 의 React 포트.
// 세리프와 산세리프 카드에 기준선·x 높이·대문자 높이 줄을 겹쳐 그린다.
// x 높이 줄만 1ex 로 그 실제 서체 치수에서 나온다 — 카드마다 자리가 달라진다.
import { useState } from "react";
import type { FC } from "react";
import styles from "./FontAnatomyCards.module.scss";
export type FontAnatomyCardsProps = {
duration?: number;
easing?: string;
color?: string;
};
type Face = "serif" | "sans";
const FACES: Face[] = ["serif", "sans"];
const FACE_LABEL: Record<Face, string> = { serif: "세리프", sans: "산세리프" };
const NOTE = "x 높이 줄은 ex 단위로 그려 서체마다 자리가 달라집니다";
const TAGS = ["대문자 높이", "x 높이", "기준선"] as const;
const RULE_CLASS: Record<(typeof TAGS)[number], string> = {
"대문자 높이": "fa__rule--cap",
"x 높이": "fa__rule--x",
"기준선": "fa__rule--base",
};
const Card: FC<{ face: Face }> = ({ face }) => (
<figure className={styles["fa__card--" + face]}>
<span className={styles.fa__sample} aria-hidden="true">Hxg</span>
{TAGS.map((t) => (
<span key={t} className={[styles.fa__rule, styles[RULE_CLASS[t]]].join(" ")}>
<span className={styles.fa__tag}>{t}</span>
</span>
))}
<figcaption className={styles.fa__name}>{FACE_LABEL[face]}</figcaption>
</figure>
);
const FontAnatomyCards: FC<FontAnatomyCardsProps> = ({ duration, easing, color }) => {
const [face, setFace] = useState<Face>("serif");
const [demo, setDemo] = useState(true);
const cls = [styles.fa, demo ? styles["is-demo"] : "", styles["is-" + face]]
.filter(Boolean).join(" ");
const vars = {
"--duration": duration != null ? duration + "ms" : undefined,
"--easing": easing,
"--color": color,
} as never;
return (
<div className={cls} style={vars}>
<div className={styles.fa__bar} role="group" aria-label="표본 고르기">
{FACES.map((f) => (
<button
key={f}
type="button"
className={styles.fa__pick}
aria-pressed={face === f ? "true" : "false"}
onClick={() => {
setDemo(false);
setFace(f);
}}
>
{FACE_LABEL[f]}
</button>
))}
</div>
<div className={styles.fa__cards}>
<Card face="serif" />
<Card face="sans" />
</div>
<p className={styles.fa__note}>{NOTE}</p>
</div>
);
};
export default FontAnatomyCards;06 폰트 해부 카드 — 측정 누적면적 41.664% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · css typography 플레이그라운드 9종 — 웹폰트 한 줄도 없이
이 데모가 속한 글
css typography 플레이그라운드 9종 — 웹폰트 한 줄도 없이css typography 는 페이지의 글자 손질, 곧 어느 서체에 얼마나 굵게, 자간과 행간을 얼마나 벌리고, 크기를 어디까지 키울지를 정하는 일입니다. 이번 놀이터 아홉 개는 웹폰트 한 줄도 없이 이미 컴퓨터에 이 글의 데모 전부 보기


