디자인 지침
ranui 를 실제 화면에 적용하는 법입니다. 역할과 상태를 고르고, 값은 토큰에 맡기세요.
규칙 자체는 두 곳에 있고, 이 페이지는 내용을 되풀이하지 않고 그쪽으로 연결합니다. 어떤 제품에나 통하는 방법은 디자인 가이드에, ranui 의 토큰·단계·컴포넌트 규칙은 ranui 구현 어댑터에 있습니다. 둘 다 node_modules/ranui/docs/DESIGN.md에 함께 배포됩니다. 모든 토큰과 그 값은 디자인 시스템에, 실행 중 덮어쓰기는 테마에, 페이지 구조는 정보 구조에 있습니다.
이럴 때 씁니다.
<r-*>요소로 만든 화면의 색, 간격, 글자, 깊이, 모션을 고를 때.
원칙
- 개성보다 명료함. 먼저 주된 과제와 주된 동작이 헷갈릴 여지 없이 드러나야 합니다.
- 기존 컨트롤을 재사용하세요.
r-button,r-input,r-select,r-modal은 폼, 키보드, 접근성 동작을 이미 갖추고 있습니다. 다시 만들지 말고 스타일을 바꾸세요. - 역할과 상태로 정하세요. "이 글자는 무엇인가"에는 답이 있지만, "어느 크기가 좋아 보이나"에는 답이 없습니다. 역할에 맞는 토큰이 값을 정하고, 그 값은 테마를 따라갑니다.
- 닿을 수 있는 모든 상태를 설계하고, 결과를 보세요. 기본 상태는 여덟 중 하나일 뿐이고, 아무도 못 보는 그림자는 브라우저에서 봐야만 드러납니다.
이것들은 출발점입니다. 화면이 더 나아진다면 벗어나도 되지만, 그 이유를 밝히세요.
색 고르기
호버와 활성이 어떤 모습인지는 사다리가 이미 정해 두었습니다. 여러분의 몫은 역할에 이름을 붙이는 일입니다.
| 이 요소는… | 이것을 쓰세요 |
|---|---|
| 페이지나 면의 배경 | --ran-color-bg / -bg-subtle / -bg-elevated / -bg-muted |
| 포인터 아래 / 눌리는 중 | --ran-color-bg-hover / -bg-active |
| 글자 | --ran-color-text / -text-secondary / -text-disabled |
| 테두리 | --ran-color-border / -hover / -active |
| 이 화면이 존재하는 이유인 동작 | --ran-color-primary(그 위에 --ran-color-primary-text) |
| 상태 | --ran-color-success / -warning / -danger |
| 링크 | --ran-color-link |
강조색은 저마다 한 가지 뜻만 가집니다. 프라이머리는 무채색이라 파랑은 링크와 포커스 링 몫으로 남겨 둡니다. 초록은 성공, 호박색은 경고, 빨강은 위험입니다. 강조하려고 빨강을 쓰면, 나중에 "위험"을 말할 색이 남지 않습니다.
날 16 진수 값, 테마를 따라 바뀌지 않는 대체값, 존재하지 않는 토큰을 가리키는 대체값은 다크 모드에서 조용히 깨지거나 선언 전체를 무효로 만듭니다. 각각의 이유는 어댑터의 색 규칙이 설명하고, ranui 자체 소스에서는 verify:design이 이를 잡아냅니다.
간격과 리듬
간격은 4px 스케일에서 가져오고, 거리가 뜻을 지니게 하세요.
- 한 묶음 안의 요소 사이는 8px.
- 묶음과 묶음 사이는 16px.
- 구역과 구역 사이는 24–32px.
간격이 좁을수록 관계가 가깝다는 뜻이므로, 스케일을 벗어난 간격 하나만으로도 리듬이 깨집니다. 간격마다 책임지는 곳을 하나로 두세요. 둘 사이에 세 번째 여백을 더하기 전에 자식의 margin 과 부모의 padding 부터 세어 보세요.
글자 고르기
그 글자가 어떤 역할을 하는지 (제목, 레이블, 본문, 버튼, 고정폭) 물으면 글꼴·크기·굵기·행간이 타입 스케일에서 따라 나옵니다. 동영상 위의 제스처 표시처럼 정말 한 번뿐인 글자는 가장 가까운 역할에 억지로 끼워 맞추기보다 전용 컴포넌트 토큰을 주는 편이 낫습니다.
깊이: 그림자와 쌓임
그림자 단계는 요소가 무엇인지로 고릅니다. 흐름 안의 면인지, 떠 있는 오버레이인지, 조작을 막는 대화 상자인지. 카드 단계로 떨어진 오버레이는 페이지에 박혀 있는 것처럼 보입니다.
여러분의 틀 안에 ranui 오버레이를 품을 때. z-index 사다리는 1000 에서 시작하므로, 포털된 오버레이는 평범한 페이지 틀을 스스로 넘어섭니다. 하지만 자기 섀도 DOM 안에 남아 있는 position: fixed 오버레이 (r-modal의 대화 상자) 는 가장 가까운 조상의 쌓임 맥락까지만 빠져나갑니다. 품은 내용을 쌓임 맥락을 만드는 것 (isolation, opacity < 1, transform, filter, will-change) 으로 감쌌다면, 오버레이가 실제로 열려 있는 동안에만 그 껍데기를 올리세요.
.embed {
isolation: isolate; /* cheap: no z-index of its own, so nothing is promoted */
}
/* Escalate only while a real overlay is open — never "just in case" */
.embed:has(r-modal[open]),
.embed:has(r-modal[closing]) {
position: relative;
z-index: 100;
}껍데기에 z-index를 늘 걸어 두면 정적인 내용까지 포함해 안의 모든 것이 스크롤 내내 고정 헤더 위로 올라옵니다. 이 사이트도 한 번 그렇게 배포된 적이 있습니다. open뿐 아니라 closing도 맞추세요. 마스크는 닫히는 트랜지션 동안에도 계속 그려집니다.
모션
자주 일어나는 피드백 (호버, 포커스, 누름) 은 즉시 반응합니다. 모션으로 변화를 설명할 때는 피드백은 0–100ms 안에, 들어올 때는 250–400ms 동안 감속하며, 나갈 때는 150–200ms 동안 가속합니다. 중단할 수 있게 하고 prefers-reduced-motion을 존중하세요.
색 속성은 절대 트랜지션하지 마세요. CSS 는 색이 왜 바뀌었는지 알지 못합니다. 그래서 background-color, color, border-color, box-shadow, fill, stroke에 건 transition은 테마가 바뀔 때도 발동하고, 페이지는 이미 바뀌었는데 요소마다 제 속도로 서서히 물듭니다. 대신 transform과 opacity를 애니메이션하세요. transition: all과 맨 transition: 0.2s도 색을 포함합니다.
상태와 문구
닿을 수 있는 상태는 모두 디자인의 일부입니다. 호버, 활성, 포커스, 비활성, 로딩, 비어 있음, 오류. 호버는 bg-hover / border-hover, 활성은 bg-active, 포커스는 포커스 링에 대응합니다. 누를 수 없는 것이 누를 수 있어 보이면 안 됩니다. r-card는 hoverable이 있을 때만 호버에 반응합니다.
문구도 시스템의 일부입니다.
- 버튼은 결과를 나타내고, 문맥에서 대상이 드러나지 않으면 대상도 씁니다. "멤버 삭제". 대상이 분명한 대화 상자라면 "삭제"로 충분합니다. 막연한 "확인"은 피하세요.
- 오류는 무슨 일이 있었는지 말하고, 이어서 어떻게 고치는지 말합니다. ✅ "빌드 실패: 번들이 크기 한도를 넘었습니다. 줄이거나 한도를 올리세요." ❌ "작업에 실패했습니다. 다시 시도해 주세요."
- 확인 알림은 성공이 아니라 변화를 말하고, 변화가 확정된 뒤에만 나타납니다. ✅ "프로젝트를 삭제했습니다" ❌ "작업에 성공했습니다".
- 맥락이 군더더기를 덜어 내게 하세요. "프로젝트 삭제"라는 제목의 대화 상자에 "프로젝트를 영구히, 완전히 삭제"라는 버튼은 필요 없습니다.
접근성
- 글자와 배경의 대비는 WCAG AA를 만족시키세요.
- 상태를 색만으로 알리지 마세요. 아이콘이나 레이블, 글과 짝지으세요.
- 조작할 수 있는 모든 요소는 보이는 포커스 링을 지킵니다 (
--ran-focus-ring, 또는outline: 2px solid var(--ran-color-focus); outline-offset: 2px). - 모든 것은 키보드로 닿을 수 있어야 합니다.
prefers-reduced-motion과prefers-color-scheme을 존중하세요.
마우스와 손끝, 좁은 화면과 넓은 화면
입력 방식도 뷰포트도, 어느 한쪽이 뒷전인 대상은 아닙니다.
- 드래그, 슬라이더, 제스처는 Pointer Events를 쓰고, 정확히 드래그하는 면에
touch-action: none을 겁니다. 뒤에 포인터 핸들러가 없는touch-action: none은 망가진 컨트롤입니다. - 호버로만 드러나는 단서에는 탭 대안이 필요합니다.
r-select나r-popover의trigger="hover"는 터치 기기에서 클릭이 됩니다. 여러분이 만드는 것도 그래야 합니다. - 브레이크포인트를 새로 만들기보다 뷰포트 기준 크기(
min(560px, calc(100vw - 32px)),clamp()) 를 쓰세요. ranui 에는 브레이크포인트 토큰이 없으니, 고정 브레이크포인트 하나하나가 누군가 관리해야 하는 숫자가 됩니다. - 모바일에서 무언가를 하는 유일한 방법을 숨기지 마세요. 대신 재배치하세요.
- 측정한 위치는 다음 리플로까지만 맞습니다.
getBoundingClientRect()에서 나온 값은 창 크기 변경, 컨테이너 리플로, 그리고 (포털된 패널이라면) 스크롤 때 낡아집니다. 좁은 폭에서 페이지를 불러오는 것으로는 그 폭으로 줄여 가는 경우를 시험하지 못하는데, 깨지는 곳은 바로 거기입니다.
라이브러리가 기계적으로 강제하는 것
pnpm -F ranui verify:design은 ranui 자체 소스에서 아홉 가지 규칙을 검사합니다. 테마를 따라 바뀌지 않는 색 대체값, 날 색 리터럴, 간격과 크기 스케일, 마우스 전용 드래그, hidden을 망가뜨리는 :host 규칙, 선언되지 않은 토큰을 가리키는 대체값, 자기 섀도 트리를 뒤지는 컴포넌트, 생성자 밖에서 만든 섀도 트리입니다. 일부러 규칙을 어기는 줄에는 design-ok: <rule> <reason>으로 그 사실을 적습니다. 검사는 설정된 소스 루트만 다룹니다. ranui 를 쓰는 앱은 자기 검사를 돌리고 자기 페이지를 직접 봐야 합니다.
디자인 예시
ranui 기본 토큰으로 그린 도식이며 실제 컴포넌트가 아닙니다.
행의 정렬 유지하기
한 행에 내용이 추가되어도 텍스트 시작점과 작업 버튼의 열을 다른 행과 맞춥니다.
안팎 모서리 맞추기
안팎의 경계가 모두 필요하다면 바깥 반경에서 안쪽 여백을 뺍니다: 16 − 4 = 12. 두 곡선의 중심이 맞아 간격이 고르게 보입니다.
--ran-radius-lg − --ran-space-1 = --ran-radius-md작은 아이콘을 쉽게 누르게 하기
16px 아이콘의 클릭 영역을 32px로 넓혀 쉽게 누를 수 있게 합니다. 마우스나 터치 등 입력 방식에 맞게 크기를 정하고 인접 영역 사이에 간격을 둡니다.
--ran-size-1 · --ran-size-7증거와 승인
실제 브라우저에서 무엇을 확인하고 무엇을 보고할지는 가이드의 변경 검증에 있습니다. 관찰한 것, 추론만 한 것, 확인하지 못한 것을 나눠 보고합니다. 에이전트의 검토는 사용자의 승인이 아닙니다.