Design System
Raw → Semantic → Component
Principles
3-Tier Abstraction
원시 값을 직접 쓰지 않고 Raw → Semantic → Component 세 단계로 감싸기 때문에, 어디를 바꿔도 영향 범위가 그 계층 안에서 통제됩니다. Component 토큰은 button·badge·row 처럼 컴포넌트마다 타입이 정해져 있어서, 엉뚱한 값을 골라 일관성이 깨지는 실수를 막아 줍니다.
Single Source of Truth
토큰 하나를 바꾸면 모든 참조가 함께 바뀝니다. hex 값이 코드베이스에 흩어지지 않습니다.
Semantic 레이어에서 테마 전환
Semantic 변수만 재정의하면 dark/light 전환이 모든 컴포넌트에 자동 반영됩니다.
컴포넌트에 raw 값 금지
#hex 나 rgba 를 직접 쓰지 않고 반드시 토큰을 거쳐 참조합니다. 그래서 모든 시각적 결정을 토큰 단위로 되짚어 추적할 수 있습니다.
Token Files
_color.css브랜드·중립·알파_typography.css폰트·크기·굵기_spacing.css--spacing-*_radius.cssxs → capsule → circle_shadow.cssxs → 2xl_motion.cssduration, easing, delay_z-index.css레이어 순서_sizing.css컴포넌트 크기_index.cssbarrel_semantic.css용도별 의미 매핑Colors
Presets
Brand
Neutral Scale
Alpha Variants
Accent Alpha
Neutral Alpha
Semantic Colors
Typography
Variants
Design tokens in action
Design tokens in action
Design tokens in action
Design tokens in action
Design tokens in action
Design tokens in action
Design tokens in action
Design tokens in action
Colors
primary color
secondary color
tertiary color
muted color
accent color
Gradient Tokens
--gradient-accent
accent-light → accent-dark--gradient-accent-soft
accent-light → accent--gradient-neutral
neutral-300 → neutral-700Weights
light
normal
medium
semibold
bold
Spacing
Border Radius
9999px
알약·칩·행 하이라이트
50%
정원
24px
면 있는 것
8px
예외 전용
6px
예외 전용
4px
예외 전용
2px
예외 전용
Grid Templates
Shadows
Motion
Duration
Easing
Z-Index
3D (Three.js)
Coffee Cup
컵과 소서는 LatheGeometry, 손잡이는 TorusGeometry, 액면은 CylinderGeometry 로 만듭니다. Canvas 2D 로 parametric heart curve 와 80-band cream↔coffee wave, blur 엽맥 라떼아트 텍스처를 생성해 MeshPhysicalMaterial 에 매핑합니다. 마우스를 따라 lerp 로 부드럽게 회전합니다.
Scroll Torus
누적 스크롤을 따라 리사주 곡선(X·Y·Z 주파수 차이) 경로를 끝없이 순환하는 메탈릭 토러스입니다. 테마에 따라 색상과 emissive 가 바뀌고, 커서가 가까우면 자석처럼 끌리고 멀면 반발하는 물리 인터랙션이 동작합니다.
Bunny Character
몸·귀·팔·발은 LatheGeometry, 머리·눈·꼬리는 SphereGeometry, 찡그린 눈은 CapsuleGeometry 로 조합한 마스코트입니다. 자동으로 눈을 깜빡이고, 클릭하면 놀람·기쁨 표정으로 전환됩니다. RAF 물리 기반으로 벽에 부딪혀 튕기며 이동하고, 충돌 시 사운드가 재생됩니다.
Components
Layout & Brand
제목과 선택적 부가설명, 선택적 우측 액션 슬롯으로 구성됩니다. 여러 곳에 흩어져 있던 <h2>제목</h2> + <p>부가설명</p> 인라인 패턴을 흡수합니다. 저장/되돌리기와 dirty 계산이 결합된 admin 설정의 SectionHeader 와 달리, 이 컴포넌트는 프레젠테이션 전용입니다.
섹션 제목
제목 아래 부가설명이 들어갑니다.
액션 있는 헤더
우측에 버튼 슬롯이 붙습니다.
내용이 maxHeight 를 넘을 때만 접고 더보기를 붙입니다. 넘치지 않으면 버튼도 페이드도 없어서 짧은 내용에는 흔적이 남지 않습니다. 높이는 ResizeObserver 로 추적합니다. 마크다운 이미지는 늦게 로드돼 그때 높이가 바뀌는데, 한 번만 재면 "로드 전 = 안 넘침" 상태로 굳어 긴 댓글이 접히지 않기 때문입니다. 첫 클램프에는 애니메이션을 걸지 않아, 글이 저절로 접히는 듯한 연출을 피합니다.
이 문단은 maxHeight(140px)보다 길어서 접힙니다. 잘린 아래쪽 페이드가 "여기서 끝이 아니다"를 알리는 유일한 시각 단서입니다 — 버튼만 있으면 딱 잘린 글자 줄이 그냥 마지막 줄처럼 읽힙니다.
클램프는 바깥(.clip)이 하고 측정은 안쪽(.inner)이 합니다. 같은 요소가 둘 다 하면 max-height 에 눌린 높이를 재게 되어 항상 "딱 맞음"이 나옵니다.
펼친 뒤 높이는 실제로 auto 입니다 — framer-motion 이 height: "auto" 를 실측해 애니메이트하기 때문에, 나중에 이미지가 더 로드돼 내용이 길어져도 잘리지 않습니다.
chevron 회전 transition 은 .root .chevron 처럼 compound 셀렉터로 씁니다. 전역 theme transition(html[data-theme-ready] *)이 shorthand 라 단일 클래스로는 transform transition 이 통째로 덮어써집니다.
Buttons & Actions
tone 은 variant 위에 의미를 나타내는 색을 얹습니다. 모든 조합이 정의돼 있지는 않고 실제로 쓰이는 조합만 존재합니다. accent 와 danger 는 둘 다 ghost 에서 색이 시작하지만 hover 방향은 반대입니다. accent 는 text-primary 로 가라앉고(강조 → 평상), danger 는 text-error 로 올라옵니다(평상 → 경고).
눌리는 것의 동작만 담는 기반입니다 — type="button" · 클릭/호버 사운드 · disabled · 누를 때 축소. 생김새는 주지 않으므로 className 으로 쓰는 쪽이 정합니다. MUI 의 ButtonBase, React Aria 의 useButton 과 같은 구조입니다.
버튼처럼 생긴 것은 Button 을, 절대위치로 깔린 클릭 영역·부모 글꼴을 물려받는 페이지 번호·::after 로 밑줄을 그리는 칩처럼 생김새를 통일하면 안 되는 자리는 Pressable 을 씁니다. raw <button> 은 쓰지 않습니다 — 사운드가 빠지고 type 누락 시 폼이 제출됩니다.
도움말 ? 버튼으로, Button 의 subtle/circle 을 고정한 wrapper 입니다. variant·shape·children 은 일관성을 위해 고정하고 size 만 열어 둡니다(폼 라벨 옆에는 2xs, 섹션 헤더에는 sm 을 씁니다). 나머지 props 는 Button 으로 그대로 흘려보내므로 Popover/Tooltip 의 trigger 로 바로 쓸 수 있습니다.
누르고 있으면 action 을 가속하며 반복합니다. 클릭하면 즉시 한 번 실행하고, 380ms 이상 누르고 있으면 반복을 시작합니다(130ms 에서 28ms 로 점점 빨라집니다). 버튼 밖에서 손을 떼도 pointerCapture 로 안전하게 멈춥니다. 자체 시각 스타일은 없고, 놓이는 자리의 모양을 className 으로 받습니다(NumberInput 의 스텝퍼가 그 예입니다).
닫기 버튼입니다. 평소엔 minus(하단 line 하나)였다가 hover 하면 두 line 이 X 로 morph 합니다([data-active] 로 강제 X 상태도 가능). vector line 을 top: 50%; left: 50% + negative margin 으로 sub-pixel 정렬해 어느 크기에서도 정확히 겹칩니다. 아래 버튼에 마우스를 올려 보세요.
메뉴 버튼 아이콘입니다. 9-dot 격자가 눌리면 X 로 모였다가 닫힐 때 다시 흩어집니다. 사이트 Navigation 메뉴 버튼과 admin/settings 서랍 토글이 같은 아이콘을 쓰도록 공용화했습니다. viewBox 8×8 의 vector circle 이라 어떤 크기에서도 완전한 원을 유지합니다(작은 span 에서는 subpixel 안티앨리어싱이 dot 마다 달라 타원처럼 보였습니다). 클릭해 보세요.
Toggles & Selection
정렬 필드 Select 와 역순 토글을 하나의 pill 로 결합합니다. 달력 블록 툴바와 댓글이 함께 씁니다. 예전에는 달력은 pill, 댓글은 gap 배치에 고정 아이콘이라 같은 기능이 서로 다르게 보였습니다. 역순 아이콘은 현재 정렬 방향을 그대로 반영합니다(고정 아이콘은 '누르면 뒤집힌다'만 알려줄 뿐 지금 방향은 알려주지 못합니다).
캡슐 안에서 하나를 고르는 컨트롤로, posts 정렬·시리즈/태그 필터·달력 뷰 전환 등 33곳에서 씁니다. 테두리를 border 가 아니라 inset box-shadow 로 그립니다. border 는 layout 에 영향을 줘서 '전체 높이 = 버튼 높이 + padding' 규칙을 지킬 수 없기 때문입니다(그래서 색만 바꾸려 해도 border-color 로는 먹지 않습니다). variant 는 테두리 세기만 가릅니다. subtle 은 주변이 전부 border-light 결인 자리(달력 블록)에서 기본값이 혼자 진하게 튀는 것을 막아 줍니다.
Inputs & Fields
스텝퍼는 SpinButton 이라 누르고 있으면 가속하며 반복합니다. 경계값에 닿으면 toast 로 알리되, hold-repeat 로 도배되는 것을 막으려고 1.2초당 한 번만 띄웁니다.
maxHint 설정 시 contenteditable="plaintext-only" 모드로 자동 전환 — 초과 글자에 inline <mark> highlight · preset (short 200 / basic 500 / long 2000) 또는 숫자 · 카운터 80% 부터 warning, 100% 부터 over · native resize 핸들 위 투명 overlay 로 커스텀 cursor 표시
tabIndent 는 opt-in 입니다. 키보드로 폼을 빠져나갈 수 있으려면 기본적으로 Tab 이 다음 포커스로 동작해야 하기 때문입니다(a11y). 그래서 코드나 마크다운을 입력하는 칸(댓글 작성란 등)에서만 켭니다. maxHint 가 있어야 켜지는 contenteditable 모드 전용입니다.
접힌 아이콘에서 펼쳐지는 검색 인풋입니다. 지우개·검색 이력·문법 도움말을 자체 내장해서, 목록이나 필터 바에서 raw input 을 다시 짤 필요가 없습니다.
실제 필터 바는 이 옵션들을 한꺼번에 씁니다. 왼쪽 typeSelector 로 검색 범위를, 오른쪽 ?(showHelp) 말풍선으로 문법·매칭 강도(prefix/regex) 도움말을 열고, historyKey 로 최근 검색을 저장합니다(검색어 입력 → Enter → 빈 상태로 다시 포커스하면 이력이 뜹니다).
collapsible 은 접힌 원형 아이콘에서 클릭 시 캡슐로 펼쳐지고, 비었을 때 blur 하면 다시 접힙니다. compact 배치용 — 펼침 너비는 expandedWidth 로 조정합니다.
size 는 캡슐 높이를 정합니다 — sm(28) · md(32, 기본). align 은 dropdown 정렬을 좌/우로 잡습니다.
native <input> 은 텍스트 일부만 색칠할 수 없어서, 초과 글자에 inline <mark> 하이라이트를 하려면 contentEditable 이 필요합니다 — 그걸 위한 단일행 contentEditable input 입니다(native 가 공짜로 주는 caret·IME·autofill 을 대신 손으로 재구현하는 대신 하이라이트를 얻는 트레이드오프). 하이라이트가 필요할 때만 이걸 직접 쓰고, 그 외엔 native Input 을 씁니다 — 둘은 완전히 분리돼 있고 서로를 모릅니다. 제한은 두 갈래 — maxHint 는 권장 한도라 초과분에 <mark> 와 카운터만 띄우고 자르진 않으며(붙여넣은 긴 제목 보존), maxLength 는 하드 상한이라 입력·붙여넣기 시점에 잘라냅니다. inlineLabel 로 KO/EN 배지를 input 안에 넣습니다.
Pickers & Selects
combobox 는 input 을 trigger 로 씁니다. label 과 searchTerms(한글 alias)로 필터하고, Enter 나 쉼표로 free text 를 추가하며, 지우개 버튼과 화살표 키 이동, group/icon 옵션을 지원합니다.
editable 을 주면 트리거를 더블클릭할 때 입력칸으로 바뀌어, 드롭다운 프리셋에 없는 값도 직접 타이핑할 수 있습니다(에디터 툴바의 폰트 크기·줄간격 입력이 이 방식입니다). 한 번 클릭은 평소대로 드롭다운을 엽니다 — 더블클릭과 구분하려고 첫 클릭을 250ms 지연시킵니다. editableInputProps 로 maxLength(길이 제한)·placeholder·sanitize(확정 직전 정규화, 예: 숫자만 · 숫자·점만)를 지정합니다. blur 나 Enter 로 확정, Escape 로 취소하며, 프리셋에 없는 값은 현재 값을 임시 옵션으로 얹어 드롭다운에서도 보이게 합니다. value 가 비어 있으면 처음부터 입력 모드로 시작합니다(PeriodPicker 의 연·월·일 칸이 그 예입니다).
render-prop trigger 와 portal popover 로 이뤄집니다. trigger 를 호출부가 직접 그리기 때문에 스와치·버튼·칩 등 무엇이든 될 수 있습니다. 모바일(≤768px)에서는 dropdown 대신 bottom sheet 으로 바뀝니다. 색을 고르려면 두 손가락만 한 면적이 필요한데, 작은 화면의 popover 로는 그만한 공간이 나오지 않기 때문입니다. inline 모드는 이미 제자리에 펼쳐진 형태라 sheet 전환에서 제외됩니다.
이모지·아이콘·커스텀 이미지를 선택합니다. 탭 전환과 검색, 셔플, 최근 사용 목록을 제공합니다.
폰트를 고르는 드롭다운입니다. 각 항목이 그 폰트 그대로 렌더되기 때문에, 고르기 전에 실제 생김새를 미리 볼 수 있습니다. 폰트를 groups 로 묶어 넘기면 그룹마다 라벨(예: 영문·고정폭)이 붙고, 그룹이 하나뿐일 때는 group: "" 로 그 라벨만 숨길 수 있습니다. 어떤 항목에 googleName 을 지정해 두면 그 폰트를 고르는 순간 컴포넌트가 해당 Google Font 를 알아서 불러오므로, 호출부에서 따로 로드하지 않아도 됩니다. preferEn 을 켜면 한글 그룹이 목록 맨 뒤로 밀려서 영문 UI 를 우선하는 자리에 맞출 수 있습니다.
Chips
variant 는 capsule 또는 bare 를 고르고, leftIcon·active(편집 중)·onClick(button) 을 지원합니다. 핸들 위에 커서를 올리면 data-cursor 로 "Drag" 커서가 나타납니다.
variant="capsule" 에 leftIcon 으로 색 dot 을 넣어 상태를 구분합니다. 에디터 상단 바의 발행 상태 표시가 이 패턴입니다 — 발행됨은 success, 예약 발행은 warning, 미발행은 muted 색 dot 입니다. dot 은 7px 원이고 색만 semantic 토큰으로 바뀝니다.
관련 글이나 프로젝트를 보여 주는 칩입니다. 썸네일·제목·카테고리를 담고, +N 더보기 토글을 제공하며, 데스크톱에서는 hover 시 미리보기 카드를 띄웁니다.
Overlays & Popovers
아래 이미지를 클릭하면 뷰어가 열립니다. 확대·이동·썸네일 탐색·풀스크린을 지원합니다.
anchor + portal · outside click / ESC 자동 닫힘 · 터치 디바이스에선 bottom sheet 로 자동 분기 · 모달 안에선 그 stacking context 로 portal 돼 전역 z-index 없이도 모달 위에 뜬다
예전에는 호출부마다 유리 배경을 제각각 override 했는데, 이를 variant 로 흡수했습니다. glass(기본)는 반투명 scrim 과 blur 라 안쪽 색이 그대로 살아납니다. solid 는 뒤가 전혀 비치면 안 될 때 씁니다. difference 는 패널째 뒤 페이지와 반전 합성해서 밑에 무엇이 깔리든 대비가 자동으로 잡힙니다. 다만 difference 는 subtree 가 한 덩어리로 합성돼 안쪽 색 구분이 사라지고, 배경이 임의의 색이면 색이 틀어집니다. 그래서 기본값은 glass 입니다. 아래 그라데이션 위에 열어 비교해 보세요.
데스크톱에서는 hover 로 열립니다. 열림은 즉시 이뤄지고 닫힘은 500ms 지연되므로, trigger 와 content 사이를 지나가도 닫히지 않습니다. hover popover 는 한 번에 하나만 열립니다(둘을 번갈아 올려 보세요). 클릭도 그대로 동작하며, hover 개념이 없는 터치/sheet 모드에서는 무시됩니다.
Feedback & Display
스택 중 하나에 hover 하면 그 토스트만이 아니라 전체가 멈춥니다(pauseAllToasts). 하나씩만 멈추면 다른 토스트가 사라지면서 스택이 재배치되고, 커서가 저절로 벗어나기 때문입니다. 클릭하면 즉시 사라집니다.
state machine (empty / filling / filled / draining) · 3-layer wave fill (SVG path d 애니메이션) · 채우기 완료 시 burst 1회 · burst 거리/크기 size 비례 스케일 · stroke / fill 모두 currentColor 상속 (부모 color 따라감)
검색어와 일치하는 부분만 <mark> 로 감쌉니다. query 를 주지 않으면 SearchHighlightProvider context 의 값을 쓰므로, 리스트의 각 행이 검색어를 일일이 넘겨받을 필요가 없습니다. query 가 비면 그냥 평문으로 렌더되므로, 조건 분기 없이 항상 이 컴포넌트를 쓰면 됩니다.
인라인 code 의 내용이 색상값(#hex · rgb() · hsl())이면 앞에 색 원(스와치)이 붙습니다 — GitHub 스타일. 렌더 후 applyColorSwatches 가 인라인 코드를 스캔해 검증된 색만 배경으로 주입하므로, 이름색·비색상은 평문으로 남습니다. 인라인 코드 자체는 Notion 식 배경형(보더 없음)입니다. 에디터에서는 툴바의 색상 칩 도구(Palette 아이콘)로 팔레트에서 고른 #hex 를 인라인 코드로 삽입하고, 리더가 이걸 그대로 이 스와치로 렌더합니다.
#e11d48rgb(46, 204, 113)hsl(280, 70%, 55%)not-a-color리더/미리보기에서 코드블록 위에 붙는 바 — 좌측 언어 라벨 + 우측 복사·줄바꿈 토글. 두 버튼은 각자 떠오른 emboss 타일로 구분되고, 코드블록 위 세로 스크롤은 페이지로 통과합니다(축 기반 wheel 라우팅). 런타임엔 attachCodeWrapToggle 이 주입합니다.
Tooltip
마우스를 올리면 반대 언어 번역을 보여 줍니다. (지연: 0ms / 600ms)