Skip to content

디자인 시스템

ranui가 딛고 선 디자인 언어와, 그것을 표현하는 토큰의 완전한 목록입니다. 라이브러리가 선언하는 모든 전역 --ran-* 사용자 정의 속성을, 두 테마에서의 값과 함께 실었습니다. 컴포넌트는 값을 직접 박아 넣는 대신 이 토큰들을 읽으므로, 토큰 하나를 덮어쓰면 그것을 쓰는 모든 것의 모습이 바뀝니다.

세 페이지가 세 가지 다른 물음에 답하며, 일부러 나누어 두었습니다.

페이지답하는 것
디자인 시스템(이 페이지)토큰이 무엇인지, 곧 어휘
디자인 지침화면을 만들 때 그중에서 어떻게 고를지
테마실행 중에 어떻게 바꾸고 덮어쓸지

이럴 때 씁니다. 토큰의 이름이나 값(색의 역할, 간격 단계, 아이콘 크기, 그림자 단계, 이징 곡선)이 필요할 때, 또는 스케일이 왜 이런 모양인지 알고 싶을 때.

언어: Geist

ranui의 토큰은 Vercel의 오픈소스 디자인 시스템 Geist에 바탕을 둡니다. 모든 색 스케일은 고를 수 있는 명암의 모음이 아니라, 단마다 할 일이 정해진 사다리입니다. 200단은 "조금 더 어두운 회색"이 아니라 "호버 배경"입니다. 단의 할 일이 정해지고 나면, 상호작용 상태의 색을 고르는 일은 판단이 아니라 조회가 됩니다.

ranui는 그 사다리를 --ran-* 스케일로 받아들이고, 그 위에 시맨틱 토큰을 얹으며, 기본 서체로 Geist Sans / Geist Mono를 함께 싣습니다.

두 개의 층

1층: 기본 팔레트. 아래에 늘어놓은 날 스케일입니다. 직접 쓰는 일은 드뭅니다.

2층: 시맨틱 토큰. --ran-color-*와 그 동료들로, 1층 위에 대응됩니다. 쓰는 것은 이 층입니다. 다크 모드는 1층만 다시 정의하므로, 모든 시맨틱 토큰이 var()를 통해 함께 바뀌고, 라이브러리 어디에도 컴포넌트별 다크 전용 덮어쓰기는 없습니다.

--ran-gray-1000        →  #171717(라이트) / #ededed(다크)        ← 1층. 바뀝니다
--ran-color-text       →  var(--ran-gray-1000)                    ← 2층. 따라갑니다
--ran-btn-color        →  var(--ran-color-text, …)                ← 컴포넌트 토큰

이 사슬이 구조의 전부입니다. 기본 단을 바꾸면 어디로든 퍼지고, 시맨틱 토큰을 바꾸면 역할 하나가 바뀌며, 컴포넌트 토큰을 바꾸면 요소 하나가 바뀝니다.

사다리

모든 색상 스케일은 100 → 1000으로 달리고, 단마다 할 일이 하나씩 정해져 있습니다.

역할역할
100기본 배경600활성 테두리
200호버 배경700단색 채움(버튼/배지)
300활성(눌림) 배경800단색 채움(호버)
400기본 테두리900보조 텍스트와 아이콘
500호버 테두리1000주요 텍스트와 아이콘

배경

토큰라이트다크쓰임새
--ran-background-100 #ffffff #000000페이지 배경
--ran-background-200 #fafafa #000000은은한 페이지 영역

회색 — --ran-gray-100..1000

텍스트와 테두리, 면 뒤에 놓인 스케일입니다.

라이트다크
100 #f2f2f2 #1a1a1a
200 #ebebeb #1f1f1f
300 #e6e6e6 #292929
400 #eaeaea #2e2e2e
500 #c9c9c9 #454545
600 #a8a8a8 #878787
700 #8f8f8f #8f8f8f
800 #7d7d7d #7d7d7d
900 #4d4d4d #a0a0a0
1000 #171717 #ededed

회색 알파 — --ran-gray-alpha-100..1000

반투명이라 어떤 면 위에도 겹칠 수 있습니다. 가림막, 호버의 옅은 물듦, 무엇이 밑에 올지 모르는 자리에 놓을 구분선에는 이것이 맞습니다.

라이트다크
100 #0000000d #ffffff12
200 #00000015 #ffffff17
300 #0000001a #ffffff21
400 #00000014 #ffffff24
500 #00000036 #ffffff3d
600 #0000003d #ffffff82
700 #00000070 #ffffff8a
800 #00000082 #ffffff78
900 #000000b3 #ffffff9c
1000 #000000e8 #ffffffeb

파랑 — --ran-blue-100..1000

링크와 포커스 링을 위해 남겨 둔 색입니다.

라이트다크
100 #f0f7ff #06193a
200 #e9f4ff #022248
300 #dfefff #002f62
400 #cae7ff #003674
500 #94ccff #00418b
600 #48aeff #0090ff
700 #006bff #006efe
800 #0059ec #005be7
900 #005ff2 #47a8ff
1000 #002359 #eaf6ff

빨강 — --ran-red-100..1000

위험과 오류입니다.

라이트다크
100 #ffeeef #330a11
200 #ffe8ea #440d13
300 #ffe3e4 #5d0e17
400 #ffd7d6 #6f101b
500 #ffb1b3 #88151f
600 #ff676d #f32e40
700 #fc0035 #f13242
800 #ea001d #e2162a
900 #d8001b #ff565f
1000 #47000c #ffe9ed

호박 — --ran-amber-100..1000

경고입니다.

라이트다크
100 #fff6de #2a1700
200 #fff4cf #361900
300 #fff1c1 #502800
400 #ffdc73 #5b3000
500 #ffc543 #703e00
600 #ffa600 #ed9a00
700 #ffae00 #ffae00
800 #ff9300 #ff9300
900 #aa4d00 #ff9300
1000 #561900 #fff3d5

초록 — --ran-green-100..1000

성공입니다.

라이트다크
100 #ecfdec #002608
200 #e5fce7 #00320b
300 #d3fad1 #003a0e
400 #b9f5bc #004615
500 #82eb8d #006717
600 #4ce15e #00952d
700 #28a948 #00ac3a
800 #279141 #009432
900 #107d32 #00ca50
1000 #003a00 #d8ffe4

시맨틱 색 토큰

컴포넌트가 실제로 읽는 층입니다. 여기 있는 것은 모두 위의 스케일을 통해 풀리므로, 테마에 맞춰 저절로 바뀝니다.

토큰풀리는 곳역할
--ran-color-bg--ran-background-100페이지 배경
--ran-color-bg-subtle--ran-background-200은은한 페이지 영역
--ran-color-bg-elevated--ran-background-100 · gray-100 (다크)카드, 면
--ran-color-bg-muted--ran-gray-100들어간·가라앉은 채움
--ran-color-bg-hover--ran-gray-200호버의 면
--ran-color-bg-active--ran-gray-300활성(눌림)의 면
--ran-color-text--ran-gray-1000주요 텍스트
--ran-color-text-secondary--ran-gray-900보조 텍스트
--ran-color-text-disabled--ran-gray-700비활성 텍스트
--ran-color-border--ran-gray-400기본 테두리
--ran-color-border-secondary--ran-gray-300더 은은한 테두리
--ran-color-border-hover--ran-gray-500호버 테두리
--ran-color-border-active--ran-gray-600활성 테두리
--ran-color-primary--ran-gray-1000주된 동작(흑백)
--ran-color-primary-hover #383838 · #cccccc (다크)primary의 호버
--ran-color-primary-active #4d4d4d · #b3b3b3 (다크)primary의 눌림
--ran-color-primary-text--ran-background-100primary 면 위에 얹히는 잉크
--ran-color-success--ran-green-700성공
--ran-color-warning--ran-amber-700경고
--ran-color-danger--ran-red-700위험 / 오류
--ran-color-link--ran-blue-700링크

--ran-color-primary-hover / -active는 시맨틱 층에 있는 두 개의 리터럴입니다. 스케일을 따라가는 대신 페이지 배경 쪽으로 다가가므로, 다크 모드에서는 이것들을 직접 다시 정의합니다.

강조색마다의 뜻

  • primary는 흑백입니다. 라이트에서는 흰 바탕에 검정, 다크에서는 검은 바탕에 흰색(Geist의 브랜드 톤, <r-button type="primary">). 그 위의 텍스트와 아이콘은 --ran-color-primary-text를 쓰고, 이것도 함께 바뀝니다. 따로 "대비" 토큰은 없습니다. primary 자체가 대비가 가장 큰 동작입니다.
  • 파랑은 남겨 둔 색입니다. 링크(--ran-color-link)와 포커스 링을 위한 것입니다. primary의 대안이 아닙니다.
  • 초록=성공 · 호박=경고 · 빨강=위험. 저마다 뜻은 하나입니다.

--ran-color-error는 없습니다. 토큰은 --ran-color-danger입니다. 선언되지 않은 속성을 가리키는 var()는 아무것으로도 풀리지 않고 선언 전체가 조용히 버려집니다. 그래서 이름이 틀렸을 때는 짐작하지 말고 이 표와 맞춰 보는 편이 낫습니다.

간격

사물 사이의 틈, 곧 padding, margin, gap입니다. 기본 단위는 4px이고 값은 아홉 개, 그 이상은 없습니다.

토큰토큰
--ran-space-14px--ran-space-832px
--ran-space-28px--ran-space-1040px
--ran-space-312px--ran-space-1664px
--ran-space-416px--ran-space-2496px
--ran-space-624px

숫자는 4px의 배수라서 스케일은 건너뜁니다. --ran-space-5는 없습니다. 바로 그 점이 핵심입니다. 페이지의 리듬을 만드는 것은 제한된 선택지입니다.

크기

요소 자신의 치수입니다. 아이콘 크기, 컨트롤 높이, 작은 정사각형이나 직사각형 컨트롤 같은 것들.

토큰흔한 쓰임
--ran-size-116px체크박스의 상자, 작은 인라인 아이콘
--ran-size-218px
--ran-size-320px컨트롤 안의 아이콘
--ran-size-424px툴바의 아이콘 버튼
--ran-size-528px촘촘한 컨트롤의 높이
--ran-size-630px
--ran-size-732px기본 컨트롤 높이

이것은 일부러 간격과 별개로 둔 스케일이며, 둘을 섞어 쓰는 것은 기계가 잡아내는 오류입니다(sizing-scale). 둘은 범위도 진행도 다릅니다(4px에서 배로 늘어나는 간격 스케일은 아이콘과 컨트롤 크기로는 어색한 값을 냅니다). 그리고 쓰는 쪽은 한쪽을 조정해도 다른 쪽을 흔들지 않을 수 있어야 합니다. 아이콘이 커졌다고 해서 우연히 같은 픽셀 값을 쓰던 모든 틈까지 넓어져서는 안 됩니다. 어떤 단이 간격의 단과 숫자로 겹칠 때(--ran-size-4--ran-space-6은 둘 다 24px입니다) 그것은 우연이지 별칭이 아닙니다.

다른 어떤 컴포넌트도 함께 쓰지 않는, 정말로 한 번뿐인 치수(예컨대 메뉴의 min-width)는 억지로 단에 밀어 넣지 말고, 자기 리터럴 대체값을 지닌 평범한 컴포넌트 토큰으로 남깁니다.

타이포그래피

토큰
--ran-font-familyGeist / Geist Sans, 그다음 시스템 UI 스택
--ran-font-monoGeist Mono, 그다음 ui-monospace, SF Mono, Menlo, Consolas …
--ran-font-size14px(기준 크기)
--ran-line-height1.5715

글자는 역할로 정리되고, 역할이 글꼴·크기·굵기·행간을 한꺼번에 정합니다.

역할쓰임굵기 토큰크기 토큰
heading제목--ran-text-heading-weight (600)--ran-text-heading-1..4 (32/24/20/16px)
label한 줄로, 눈으로 훑는 것--ran-text-label-weight (500)--ran-text-label-1..3 (14/13/12px)
copy여러 줄의 본문--ran-text-copy-weight (400)--ran-text-copy-1..2 (16/14px)
button버튼의 글자--ran-text-button-weight (500)--ran-text-button-size (14px)
mono코드, 데이터, 작은 머리글--ran-text-mono-weight-regular (400) / --ran-text-mono-weight-medium (500)label / copy의 크기를 빌립니다

역할이 제대로 자리 잡게 하려고만 있는 토큰이 둘 있습니다.

토큰이유
--ran-text-heading-tracking-0.03em큰 표시 크기에서는 제목의 자간을 더 좁혀야 합니다.
--ran-text-button-line-height1높이가 정해진 컨트롤 안에서 세로 가운데를 또렷하게 맞춥니다.

Geist는 굵기의 상한이 600(세미볼드)입니다. 강조는 크기와 여백에서 나오지, 더 굵은 서체에서 나오지 않습니다. --ran-text-copy-3은 없습니다. 12px 단은 --ran-text-label-3입니다.

글꼴

ranui는 두 서체를 직접 호스팅합니다(가변 굵기 100–900, SIL OFL 1.1). 그래서 import 하나로 CDN 의존 없이 불러올 수 있습니다.

js
import 'ranui/fonts'; // 번들러용
html
<link rel="stylesheet" href="…/ranui/dist/fonts/fonts.css" />

이것이 없으면 토큰은 시스템 글꼴 스택으로 물러납니다. 모든 것이 여전히 동작하지만 Geist 서체는 아닙니다.

모서리

토큰쓰임새
--ran-radius-sm6px컨트롤: 버튼, 입력, 선택
--ran-radius-md12px카드, 대화 상자
--ran-radius-lg16px큰 면
--ran-radius-full9999px알약 모양, 아바타

그림자 높이

그림자는 장식이 아니라 역할입니다. 단계는 그 요소가 무엇인지로 고르세요. 다크 모드에서는 셋 모두 갈아 끼웁니다. 흰 페이지에 맞춰 다듬은 그림자는 검은 페이지에서 사라지기 때문입니다.

토큰쓰임새라이트다크
--ran-shadow-elevated흐름 속에 있으면서 테두리도 지닌 면: r-card, r-section0 1px 2px rgba(0,0,0,.04), 0 2px 4px -2px rgba(0,0,0,.05)0 1px 2px rgba(0,0,0,.16)
--ran-shadow-menu내용 위에 잠시 겹치는 층: 드롭다운, 선택 메뉴, 팝오버, 토스트0 2px 4px rgba(0,0,0,.05), 0 8px 24px -6px rgba(0,0,0,.14)0 1px 1px rgba(0,0,0,.2), 0 4px 8px -4px rgba(0,0,0,.4), 0 16px 24px -8px rgba(0,0,0,.5)
--ran-shadow-modal앞을 막는 대화 상자: r-modal0 4px 12px rgba(0,0,0,.08), 0 20px 48px -12px rgba(0,0,0,.22)0 1px 1px rgba(0,0,0,.2), 0 8px 16px -4px rgba(0,0,0,.4), 0 24px 32px -8px rgba(0,0,0,.5)

테두리 없는 오버레이는 주변과의 분리를 오로지 그림자에 기댑니다. 그래서 오버레이 단계에는 실제로 무게가 실려 있습니다. 오버레이가 들린 면 단계로 내려앉으면 납작해 보이고 페이지에 붙박인 것처럼 보입니다.

쌓임

떠 있는 오버레이는 <body>로 포털되므로, 명시적인 단계가 필요합니다.

토큰기본값쓰임새
--ran-z-modal1000앞을 막는 대화 상자와 그 마스크
--ran-z-dropdown1100드롭다운 / 선택 메뉴 / 팝오버: 모달보다 위. 그래야 대화 상자 안의 select가 계속 보입니다
--ran-z-message1200토스트와 알림: 언제나 맨 위

사다리가 1000에서 시작하는 것은 평범한 페이지 틀을 넘어서기 위해서입니다(내비게이션 바와 배경막은 보통 수십 대에 놓입니다). 단계를 덮어쓸 때는 :root에서, 또는 컴포넌트별로(--ran-dropdown-host-z-index, --ran-modal-root-z-index, --ran-message-z-index) 하고, !important는 절대 쓰지 마세요.

모션

토큰쓰임
--ran-motion-duration-fast0.15s호버·활성 상태의 전환
--ran-motion-duration-base0.2s팝오버, 메뉴
--ran-motion-duration-slow0.35s더 큰 등장
이징 토큰곡선성격
--ran-motion-ease-standardcubic-bezier(0.645,0.045,0.355,1)인-아웃. 범용
--ran-motion-ease-snappycubic-bezier(0.33,0,0.15,1)빠르고 넘침 없음: 토글
--ran-motion-ease-springcubic-bezier(0.34,1.26,0.5,1)살짝 넘침: 버튼, 카드
--ran-motion-ease-bouncycubic-bezier(0.34,1.56,0.64,1)장난스러운 넘침: 좋아요, 장바구니 담기
--ran-motion-ease-smoothcubic-bezier(0.4,0,0.2,1)차분하고 넘침 없음: 등장, 레이아웃

spring 계열은 다듬어진 SwiftUI 스프링에서 증류한 것입니다(response/damping을 한 번만 넘치는 베지어로 줄였습니다).

이것들은 움직임 속성하고만 짝지으세요. transform, opacity, 상자의 형상입니다. 팔레트 속성(background-color, color, border-color, box-shadow, fill, stroke)에는 일부러 기본 트랜지션을 두지 않았습니다. CSS는 상호작용과 테마 전환을 구별하지 못하기 때문입니다. 색에 붙인 페이드는 라이트↔다크가 바뀔 때도 발동합니다. 그래도 다시 켜고 싶다면, 모든 컴포넌트가 --ran-*-transition 훅을 열어 둡니다.

포커스

토큰용도
--ran-focus-ring0 0 0 2px var(--ran-background-100), 0 0 0 4px var(--ran-blue-700)표준 링. box-shadow
--ran-focus-ring-inverse-color#fff 테마 모두에서 어두운 면을 위한 링 색

링은 두 겹입니다. 배경색의 안쪽 링과 파란 바깥 링. 그래서 어떤 면 위에서도 보이고, 이제 흑백이 된 primary를 따라가지 않고 파랑으로 남습니다.

--ran-focus-ring-inverse-color일부러 다크 모드에서 다시 정의하지 않습니다. 페이지 테마와 상관없이 자기 면이 늘 어두운 컴포넌트(임의의 영상 위에 얹히는 r-player의 컨트롤 바)를 위한 것이고, 그 면은 페이지가 바뀌어도 바뀌지 않기 때문입니다.

스킨 프리미티브

컴포넌트들이 함께 쓰는 구조적인 값 가운데, 색도 크기도 글자도 아닌 몇 안 되는 것들입니다. 일부러 최소로 유지합니다. 이 층은 예전에 훨씬 컸고, 대부분은 테마 팩과 함께 걷어냈습니다.

토큰용도
--ran-skin-border-width1px컴포넌트가 그리는 테두리 두께
--ran-skin-border-stylesolid컴포넌트가 그리는 테두리 종류
--ran-skin-border-image-width4pxborder-image-slice의 안쪽 여백. button/checkbox/input/modal/message가 함께 씁니다
--ran-skin-raised-shadowvar(--ran-shadow-elevated)들린 면의 그림자. 스킨이 바꿀 수 있도록 한 번 거쳐 갑니다
--ran-skin-font-familyvar(--ran-font-family)컴포넌트가 쓰는 서체. 같은 방식으로 한 번 거쳐 갑니다

다크 모드가 다시 정의하는 것

<html>(또는 임의의 서브트리. 테마 참고)에 붙은 data-ran-theme="dark"가 다시 정의하는 것은 기본 팔레트뿐입니다. 다만 스케일을 통해 풀 수 없는 예외가 셋 있습니다.

  • 1층 전체: 회색, 회색 알파, 파랑, 빨강, 호박, 초록의 모든 단과 두 배경.
  • --ran-color-bg-elevated. 다크에서는 --ran-gray-100을 가리켜, 카드가 검은 페이지에 묻히지 않고 들려 보이게 합니다.
  • --ran-color-primary-hover / -active. 스케일 참조가 아니라 리터럴이기 때문입니다.
  • 그림자 세 단계 모두. 어두운 바탕에 맞춰 다시 다듬습니다.

그 밖의 모든 것(다른 모든 시맨틱 토큰, 모든 크기, 모든 지속 시간)은 한 번만 정의됩니다.

컴포넌트 토큰

시맨틱 층 아래에서는 모든 컴포넌트가 자기 훅을 이렇게 이름 붙여 노출합니다.

--ran-{component}-{element}[-{state}]-{property}

예를 들어 --ran-btn-hover-background, --ran-select-search-active-border-width입니다. 기본값은 시맨틱 토큰으로 물러납니다: var(--ran-btn-background, var(--ran-color-primary, #171717)). 그래서 시맨틱 토큰 하나를 덮어쓰면 그 전부에 닿고, 컴포넌트 토큰을 덮어쓰면 변화가 요소 하나로 좁혀집니다.

생성된 전체 목록은 저장소의 style-tokens-public.md에 있습니다. 요소별 API는 여기에 있습니다. 적용하는 법은 테마를 보세요.

내 CSS에서 토큰 쓰기

css
.panel {
  background: var(--ran-color-bg-elevated);
  color: var(--ran-color-text);
  border: var(--ran-skin-border-width) var(--ran-skin-border-style) var(--ran-color-border);
  border-radius: var(--ran-radius-md);
  padding: var(--ran-space-4);
  box-shadow: var(--ran-shadow-elevated);
}

세 가지 규칙이 그것을 다크에서도 안전하게 지켜 줍니다.

  1. 테마를 따라야 하는 것에는 날 16진수를 쓰지 마세요.
  2. 대체값은 함께 바뀌는 토큰을 가리켜야 합니다: var(--ran-color-text, var(--ran-gray-1000))이지, var(--ran-color-text, #171717)이 아닙니다.
  3. 대체값은 존재하는 토큰을 가리켜야 합니다. 그러지 않으면 선언이 버려지고, 요소는 물려받은 값을 조용히 지닌 채로 남습니다.

라이브러리가 선언하는 전역 토큰은 모두 이 페이지에 실려 있고, 여기에 적지 않은 채 토큰을 늘리면 단위 테스트가 실패합니다. 컴포넌트 범위의 토큰은 따로 생성되어 style-tokens-public.md에 있습니다.

MIT 라이선스로 배포됩니다.