01System font stack comparer
The same sentence sits in three panes wearing serif, sans, and mono system stacks, and one tap re-dresses all of them so you can see what a font-family list built only from fonts every OS ships really looks like.
Best forFont spec page of a design system
font-familysystem-uiui-monospace

<main class="stage">
<section class="fs is-demo" id="fs" aria-label="System font stack comparer">
<div class="fs__tabs">
<button class="fs__tab" type="button" aria-pressed="true">Korean</button>
<button class="fs__tab" type="button" aria-pressed="false">English</button>
<button class="fs__tab" type="button" aria-pressed="false">Mixed</button>
</div>
<div class="fs__grid">
<article class="fs__pane">
<header class="fs__head">
<span class="fs__chip">SERIF</span>
<span class="fs__mark" aria-hidden="true"></span>
</header>
<p class="fs__spec fs__spec--serif" id="specSerif">The quick brown fox jumps</p>
<p class="fs__stack">ui-serif · Georgia</p>
</article>
<article class="fs__pane">
<header class="fs__head">
<span class="fs__chip">SANS</span>
<span class="fs__mark" aria-hidden="true"></span>
</header>
<p class="fs__spec fs__spec--sans" id="specSans">The quick brown fox jumps</p>
<p class="fs__stack">system-ui · Segoe UI</p>
</article>
<article class="fs__pane">
<header class="fs__head">
<span class="fs__chip">MONO</span>
<span class="fs__mark" aria-hidden="true"></span>
</header>
<p class="fs__spec fs__spec--mono" id="specMono">The quick brown fox jumps</p>
<p class="fs__stack">ui-monospace · Consolas</p>
</article>
</div>
<p class="fs__note">Use the buttons to swap the sample sentence</p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 시스템 서체 스택 비교 — 같은 문장을 serif·sans·mono 세 판에 나란히 놓는다.
// 스택은 어느 OS 에나 깔려 있는 서체만 담는다: ui-serif·Georgia / system-ui·Segoe UI / ui-monospace·Consolas.
// 웹폰트는 한 줄도 없다 — 이 편의 주제 자체가 그것이다.
// 격자 미리보기(.is-demo)는 세 판을 steps(1, end) 컷으로 돌아가며 강조한다(반투명 유령 프레임 방지).
// 컷만으로는 움직인 프레임이 두세 장에 그치므로(함정표), 활성 판의 표본이 같은 창 안에서
// 살짝 떠오르는 transform 보조를 얹는다. 세 창이 2초를 꽉 채운다.
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue-light;
$serif: ui-serif, Georgia, "Times New Roman", "Noto Serif KR", serif;
$sans: system-ui, -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
$mono: ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;
$line: rgba($subject-cream, .18);
$dim: #beb7ad;
@include stage($stage-ink);
.fs {
display: flex; flex-direction: column; gap: $sp-2;
box-sizing: border-box; width: min(100%, 420px);
}
// ── 표본 문장 탭 (실사용 경로). 미끄러지는 인디케이터가 없으니 flex 배분은 자유다
.fs__tabs { display: flex; gap: $sp-2; }
.fs__tab {
flex: 1 1 0; appearance: none; cursor: pointer;
padding: $sp-1 $sp-2; border: 1px solid $line; border-radius: $r-pill;
background: transparent; color: $subject-cream;
font-family: $sans; font-weight: 700; font-size: 12px; line-height: 1.4;
transition: background-color $duration $easing, color $duration $easing, border-color $duration $easing;
}
.fs__tab[aria-pressed="true"] {
border-color: $subject-cream; background: $subject-cream; color: $subject-ink;
}
.fs__tab:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 세 판
.fs__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: $sp-2; }
.fs__pane {
display: flex; flex-direction: column; gap: $sp-1;
box-sizing: border-box; padding: $sp-2;
border: 1px solid $line; border-radius: $r-control;
background: rgba($subject-cream, .02);
}
.fs__head { display: flex; align-items: center; justify-content: space-between; }
.fs__chip {
font-family: $mono; font-weight: 700; font-size: 10px; line-height: 1.4;
letter-spacing: .08em; color: $dim;
}
// 활성 판 머리의 점 — 미리보기 루프에서만 보인다(정적 경로에서는 숨겨도 화면이 성립한다)
.fs__mark {
width: $sp-1; height: $sp-1; border-radius: 50%; background: $color;
opacity: 0;
}
// 표본. 세 판이 각자의 시스템 스택을 입는다 — 이 세 선언이 이 데모의 본체다
.fs__spec {
margin: 0; color: $subject-cream;
font-size: clamp(13px, 4.6vw, 20px); font-weight: 600; line-height: 1.45;
}
.fs__spec--serif { font-family: $serif; }
.fs__spec--sans { font-family: $sans; }
.fs__spec--mono { font-family: $mono; }
.fs__stack {
margin: 0; color: $dim;
font-family: $mono; font-weight: 500; font-size: 9px; line-height: 1.4;
}
.fs__note {
margin: 0; color: $dim;
font-family: $sans; font-weight: 500; font-size: 11px; line-height: 1.4;
}
// ── 격자 미리보기 루프 (2초). 세 판이 돌아가며 한 칸(약 33%)씩 강조 창을 받는다.
// 강조 교체는 steps(1, end) 컷, 표본의 떠오름은 같은 창 안의 부드러운 transform —
// 컷 + transform 보조 조합(함정표: 컷만 쓰면 움직인 프레임이 2~3장에 그친다).
.fs.is-demo .fs__pane {
animation-name: fsGlow;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fs.is-demo .fs__pane:nth-child(2) { animation-delay: calc($dur-loop / 3); }
.fs.is-demo .fs__pane:nth-child(3) { animation-delay: calc($dur-loop * 2 / 3); }
.fs.is-demo .fs__spec {
animation-name: fsRise;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fs.is-demo .fs__pane:nth-child(2) .fs__spec { animation-delay: calc($dur-loop / 3); }
.fs.is-demo .fs__pane:nth-child(3) .fs__spec { animation-delay: calc($dur-loop * 2 / 3); }
.fs.is-demo .fs__mark {
animation-name: fsDot;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fs.is-demo .fs__pane:nth-child(2) .fs__mark { animation-delay: calc($dur-loop / 3); }
.fs.is-demo .fs__pane:nth-child(3) .fs__mark { animation-delay: calc($dur-loop * 2 / 3); }
// steps(1, end) 는 구간 끝에서만 값이 바뀐다 — 0% 값이 한 창(33%) 내내 보이고 33% 에 내려앉는다.
// 경계 복귀는 루프 시작(0%)에서만 일어나 유령 프레임이 없다.
@keyframes fsGlow {
0% { background: rgba($subject-cream, .10); border-color: rgba($subject-cream, .60); }
33%, 100% { background: rgba($subject-cream, .02); border-color: rgba($subject-cream, .18); }
}
@keyframes fsRise {
0% { transform: translateY(5px); }
33% { transform: translateY(-1px); }
39%, 100% { transform: translateY(5px); }
}
@keyframes fsDot {
0% { opacity: 1; transform: scale(1); }
33%, 100% { opacity: 0; transform: scale(.4); }
}
// ── 좁은 화면: 글자와 여백을 내려 무대 안에 들어오게 한다 (올리지 않는다)
@media (max-width: 340px) {
.fs { gap: $sp-1; }
.fs__tabs { gap: $sp-1; }
.fs__tab { padding: $sp-1; font-size: 11px; }
.fs__pane { gap: 0; }
.fs__spec { font-size: 13px; line-height: 1.4; }
.fs__stack { font-size: 9px; }
.fs__note { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.fs.is-demo .fs__pane,
.fs.is-demo .fs__spec,
.fs.is-demo .fs__mark {
animation: none !important;
transform: none !important;
opacity: 0 !important;
background: rgba($subject-cream, .02) !important;
border-color: rgba($subject-cream, .18) !important;
}
}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);
}
.fs {
display: flex;
flex-direction: column;
gap: 8px;
box-sizing: border-box;
width: min(100%, 420px);
}
.fs__tabs {
display: flex;
gap: 8px;
}
.fs__tab {
flex: 1 1 0;
appearance: none;
cursor: pointer;
padding: 4px 8px;
border: 1px solid rgba(255, 247, 230, 0.18);
border-radius: 999px;
background: transparent;
color: #fff7e6;
font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 700;
font-size: 12px;
line-height: 1.4;
transition: background-color 220ms cubic-bezier(0.2, 0.8, 0.2, 1), color 220ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fs__tab[aria-pressed=true] {
border-color: #fff7e6;
background: #fff7e6;
color: #17141a;
}
.fs__tab:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
.fs__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.fs__pane {
display: flex;
flex-direction: column;
gap: 4px;
box-sizing: border-box;
padding: 8px;
border: 1px solid rgba(255, 247, 230, 0.18);
border-radius: 12px;
background: rgba(255, 247, 230, 0.02);
}
.fs__head {
display: flex;
align-items: center;
justify-content: space-between;
}
.fs__chip {
font-family: ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;
font-weight: 700;
font-size: 10px;
line-height: 1.4;
letter-spacing: 0.08em;
color: #beb7ad;
}
.fs__mark {
width: 4px;
height: 4px;
border-radius: 50%;
background: #5b8cf8;
opacity: 0;
}
.fs__spec {
margin: 0;
color: #fff7e6;
font-size: clamp(13px, 4.6vw, 20px);
font-weight: 600;
line-height: 1.45;
}
.fs__spec--serif {
font-family: ui-serif, Georgia, "Times New Roman", "Noto Serif KR", serif;
}
.fs__spec--sans {
font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fs__spec--mono {
font-family: ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;
}
.fs__stack {
margin: 0;
color: #beb7ad;
font-family: ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;
font-weight: 500;
font-size: 9px;
line-height: 1.4;
}
.fs__note {
margin: 0;
color: #beb7ad;
font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 500;
font-size: 11px;
line-height: 1.4;
}
.fs.is-demo .fs__pane {
animation-name: fsGlow;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fs.is-demo .fs__pane:nth-child(2) {
animation-delay: 0.6666666667s;
}
.fs.is-demo .fs__pane:nth-child(3) {
animation-delay: 1.3333333333s;
}
.fs.is-demo .fs__spec {
animation-name: fsRise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fs.is-demo .fs__pane:nth-child(2) .fs__spec {
animation-delay: 0.6666666667s;
}
.fs.is-demo .fs__pane:nth-child(3) .fs__spec {
animation-delay: 1.3333333333s;
}
.fs.is-demo .fs__mark {
animation-name: fsDot;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fs.is-demo .fs__pane:nth-child(2) .fs__mark {
animation-delay: 0.6666666667s;
}
.fs.is-demo .fs__pane:nth-child(3) .fs__mark {
animation-delay: 1.3333333333s;
}
@keyframes fsGlow {
0% {
background: rgba(255, 247, 230, 0.1);
border-color: rgba(255, 247, 230, 0.6);
}
33%, 100% {
background: rgba(255, 247, 230, 0.02);
border-color: rgba(255, 247, 230, 0.18);
}
}
@keyframes fsRise {
0% {
transform: translateY(5px);
}
33% {
transform: translateY(-1px);
}
39%, 100% {
transform: translateY(5px);
}
}
@keyframes fsDot {
0% {
opacity: 1;
transform: scale(1);
}
33%, 100% {
opacity: 0;
transform: scale(0.4);
}
}
@media (max-width: 340px) {
.fs {
gap: 4px;
}
.fs__tabs {
gap: 4px;
}
.fs__tab {
padding: 4px;
font-size: 11px;
}
.fs__pane {
gap: 0;
}
.fs__spec {
font-size: 13px;
line-height: 1.4;
}
.fs__stack {
font-size: 9px;
}
.fs__note {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.fs.is-demo .fs__pane,
.fs.is-demo .fs__spec,
.fs.is-demo .fs__mark {
animation: none !important;
transform: none !important;
opacity: 0 !important;
background: rgba(255, 247, 230, 0.02) !important;
border-color: rgba(255, 247, 230, 0.18) !important;
}
}var fs = document.getElementById('fs');
var tabs = Array.prototype.slice.call(document.querySelectorAll('.fs__tab'));
var specs = Array.prototype.slice.call(document.querySelectorAll('.fs__spec'));
var SAMPLES = ['The quick brown fox jumps', 'Hello Type 0123', 'CSS Type 2026'];
// 탭을 누르면 세 판의 표본 문장이 한꺼번에 갈아 입는다 — 미리보기 루프는 여기서 뗀다
tabs.forEach(function (tab, i) {
tab.addEventListener('click', function () {
fs.classList.remove('is-demo');
tabs.forEach(function (t) { t.setAttribute('aria-pressed', String(t === tab)); });
specs.forEach(function (spec) { spec.textContent = SAMPLES[i]; });
});
});
fs.addEventListener('pointerdown', function () { fs.classList.remove('is-demo'); });// FontStackCompare — vanilla 판(run/074/demo/01-font-stack-compare/index.html) 의 React 포트.
// 같은 문장을 serif·sans·mono 세 판에 나란히 놓고 탭으로 표본 문장을 바꾼다.
// 스택 목록과 미리보기 루프의 키프레임은 vanilla 와 같은 글자다 (값을 다시 타이핑하지 않는다).
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./FontStackCompare.module.scss";
export type FontStackCompareProps = {
duration?: number;
easing?: string;
color?: string;
samples?: string[];
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const TAB_LABELS = ["한글", "영문", "혼합"];
const DEFAULT_SAMPLES = ["동해물과 백두산이", "Hello Type 0123", "CSS 타이포 2026"];
const NOTE = "버튼으로 표본 문장을 바꿔 보세요";
const ARIA_LABEL = "시스템 서체 스택 비교";
const PANES = [
{ chip: "SERIF", stack: "ui-serif · Georgia", spec: "serif" },
{ chip: "SANS", stack: "system-ui · Segoe UI", spec: "sans" },
{ chip: "MONO", stack: "ui-monospace · Consolas", spec: "mono" },
];
export default function FontStackCompare(props: FontStackCompareProps) {
const duration = props.duration ?? 220;
const easing = props.easing ?? "cubic-bezier(.2, .8, .2, 1)";
const color = props.color ?? "#5b8cf8";
const samples = props.samples ?? DEFAULT_SAMPLES;
const [active, setActive] = useState<number>(0);
const [preview, setPreview] = useState<boolean>(true);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
return (
<main className={styles.stage}>
<section
className={styles.root + (preview ? " " + styles.isDemo : "")}
style={vars}
aria-label={ARIA_LABEL}
onPointerDown={() => setPreview(false)}
>
<div className={styles.tabs}>
{TAB_LABELS.map((label, i) => (
<button
key={label}
type="button"
className={styles.tab}
aria-pressed={String(i === active)}
onClick={() => {
setPreview(false);
setActive(i);
}}
>
{label}
</button>
))}
</div>
<div className={styles.grid}>
{PANES.map((pane) => (
<article key={pane.chip} className={styles.pane}>
<header className={styles.head}>
<span className={styles.chip}>{pane.chip}</span>
<span className={styles.mark} aria-hidden="true"></span>
</header>
<p className={styles.spec + " " + styles["spec--" + pane.spec]}>{samples[active]}</p>
<p className={styles.stack}>{pane.stack}</p>
</article>
))}
</div>
<p className={styles.note}>{NOTE}</p>
</section>
</main>
);
}01 System font stack comparer — Measured area 23.762% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · CSS Typography Playground: 9 Demos, No Webfonts
The post this demo lives in
CSS Typography Playground: 9 Demos, No Webfontscss typography is setting type with CSS: which family, how heavy, how far apart, how large. These nine playgrounds do al See all demos in this post
If you like this effect - similar demos
base05 Form element font inheritance — a rule that matches buttons and inputs to surrounding textCSS Reset: 9 Patterns From Basics to Safe-Area
base01 Squircle card — a superellipse exponent value flows between circle and square while the card's corner ridge changesCSS corner-shape: 9 Corner Cutting Patterns
Landing sections04 Skills stack9 Project Showcase Section Templates — Copy-Paste