디자인
iStack 디자인
iStack 디자인
모든 클라이언트 앱(web · admin · site · lite · mobile · desktop)이 따르는 화면 규칙이다. 값은 위 토큰이 기준이고, 아래 글은 그 값을 언제 쓰는지다.
이름 끝의 -dark 는 다크 모드 값이다 (prefers-color-scheme: dark, React Native 는 useColorScheme). 컴포넌트의 -dark 는 바뀌는 값만 적는다.
Overview
Apple 시스템 앱처럼 조용하고 단정하다. 내용이 주인공이라 꾸밈은 내용을 가리지 않을 만큼만 둔다. 화면만 봐도 아는 것에는 설명 문구를 달지 않고, 출처 · 가정처럼 정직하게 밝혀야 할 것만 짧게 쓴다. 라이트 · 다크를 모두 지원하고, 둘 다 같은 정성으로 맞춘다.
Colors
색은 Apple 시스템 색과 그룹 배경만 쓴다. 새 색이 필요하면 시스템 색에서 고르고 여기 토큰으로 더한다.
- Primary (systemBlue): 누를 수 있는 것에만. 링크 · 주 버튼 · 선택 표시.
- Background (그룹 배경): 페이지 바탕. 그 위에 surface 카드가 뜬다.
- Surface: 카드 · 시트처럼 내용을 담는 면.
- On-surface: 본문 글자. 보조 글자는 같은 색의 투명도로 만든다 (새 회색을 만들지 않는다).
- Fill: 입력칸 · 버튼처럼 테두리 없이 구분되는 칸.
- Danger (systemRed): 삭제처럼 되돌릴 수 없는 조작의 글자에만.
- Warning container: 오류 · 실패 알림의 면. systemOrange(#FF9500 · 다크 #FF9F0A)를 surface 에 14% 만 섞는다 (글자는 on-surface).
Typography
시스템 글꼴 하나(SF · Apple SD Gothic Neo)를 무게와 크기로만 나눈다. 웹 글꼴은 불러오지 않는다.
- Title (22px · 700): 화면 제목, 한 화면에 하나.
- Headline (17px · 600): 카드 · 구역 제목.
- Body (14px): 본문 · 입력. 자간을 살짝 좁혀(-0.01em) 시스템 앱과 같은 밀도를 낸다.
- Caption (12px · 500): 태그처럼 내용에 붙는 짧은 표시.
- Body-mobile (16px): 모바일 입력칸. 16px 보다 작으면 iOS 가 입력할 때 화면을 확대한다.
Layout
카드를 세로로 쌓는 단일 열이 기본이다. 내용 폭은 page-max 까지.
간격은 4px 단위: 요소 사이 md(12), 카드 사이 lg(16), 페이지 가장자리 xl(24).
Elevation & Depth
깊이는 그림자보다 면의 톤으로 낸다: background → surface.
라이트에서만 카드에 아주 옅은 그림자(0 1px 2px · 0 4px 16px, 검정 4%)를 두고, 다크에서는 그림자 없이 톤 차이로만.
스크롤해도 남는 머리글은 background 를 72% 만 칠하고 뒤를 흐리게 비춘다(saturate(180%) blur(20px)).
Shapes
테두리선 없이 둥근 면으로 나눈다. 큰 면일수록 더 둥글게: 입력칸 sm(8) · 알림 md(12) · 카드 lg(18).
Components
- Page: background 바탕에 on-surface 본문.
- Card: surface 면, 테두리 없음, 안쪽 20px. 한 카드에는 한 가지 일만 담는다.
- Input: fill 칸, 테두리 없음. 포커스는 글자색 30% 의 3px 바깥선(
:focus-visible). 모바일은 손가락으로 누르므로 안쪽 · 둥글기를md(12)로 키운다. - Button: fill 칸에 primary 글자, 테두리 없음. 안쪽은 위아래
sm(8) · 좌우md(12). 삭제처럼 되돌릴 수 없는 것은 글자만 danger. 요청이 끝나기 전에는 막고 40% 로 흐린다. 모바일은 손가락으로 누르므로 위아래도md(12), 글자는 body-mobile. - Tag: fill 칸에 on-surface 글자(caption), 테두리 없음. 안쪽은 위아래
xs(4) · 좌우sm(8), 칩 사이도xs(4). 누를 수 없으니 primary 를 쓰지 않는다. 내용 아래에 줄을 바꿔 가며 늘어놓고, 태그가 없으면 칸도 두지 않는다. - Alert: warning container 면에 on-surface 글자. 오류 내용을 그대로 보여 준다 (
role="alert"). - Header: 화면 제목과 그 화면 전체에 걸리는 조작만. 반투명 · 흐림으로 스크롤 위에 뜬다.
- Link: primary 글자, 밑줄 없이. 누를 수 있는 글자에만 primary 를 쓴다.
Do’s and Don’ts
- Do: 같은 정보 · 조작은 한 곳에만 둔다.
- Do: 새 값이 필요하면 먼저 이 파일의 토큰을 더하고 코드에서 그 값을 쓴다.
- Do: 라이트 · 다크를 둘 다 띄워 확인한다 (
verify-localskill). - Don’t: 장식용 색 · 그라디언트 · 테두리선 · 진한 그림자를 쓰지 않는다.
- Don’t: 화면만 봐도 아는 설명 문구, 같은 내용을 되풀이하는 제목 · 안내를 달지 않는다.
- Don’t: 시스템 글꼴 밖의 글꼴, 토큰에 없는 크기 · 간격 · 둥글기를 쓰지 않는다.
Tokens
version: alpha
name: iStack
description: Apple 시스템 앱처럼 조용한 화면. 시스템 색 · 시스템 글꼴, 라이트 · 다크 둘 다
colors:
primary: "#007AFF"
primary-dark: "#0A84FF"
background: "#F2F2F7"
background-dark: "#000000"
surface: "#FFFFFF"
surface-dark: "#1C1C1E"
on-surface: "#1D1D1F"
on-surface-dark: "#F5F5F7"
fill: "rgba(118, 118, 128, 0.12)"
fill-dark: "rgba(118, 118, 128, 0.24)"
danger: "#FF3B30"
danger-dark: "#FF453A"
warning-container: "color-mix(in srgb, #FF9500 14%, #FFFFFF)"
warning-container-dark: "color-mix(in srgb, #FF9F0A 14%, #1C1C1E)"
typography:
title:
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Apple SD Gothic Neo', system-ui, sans-serif"
fontSize: 22px
fontWeight: 700
lineHeight: 1.45
letterSpacing: -0.025em
headline:
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Apple SD Gothic Neo', system-ui, sans-serif"
fontSize: 17px
fontWeight: 600
lineHeight: 1.45
letterSpacing: -0.02em
body:
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Apple SD Gothic Neo', system-ui, sans-serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.45
letterSpacing: -0.01em
caption:
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Apple SD Gothic Neo', system-ui, sans-serif"
fontSize: 12px
fontWeight: 500
lineHeight: 1.45
body-mobile:
fontFamily: "-apple-system, 'Apple SD Gothic Neo', system-ui, sans-serif"
fontSize: 16px
fontWeight: 400
lineHeight: 1.45
spacing:
xs: 4px
sm: 8px
md: 12px
lg: 16px
xl: 24px
xxl: 32px
page-max: 1440px
rounded:
sm: 8px
md: 12px
lg: 18px
components:
page:
backgroundColor: "{colors.background}"
textColor: "{colors.on-surface}"
typography: "{typography.body}"
page-dark:
backgroundColor: "{colors.background-dark}"
textColor: "{colors.on-surface-dark}"
card:
backgroundColor: "{colors.surface}"
rounded: "{rounded.lg}"
padding: 20px
card-dark:
backgroundColor: "{colors.surface-dark}"
input:
backgroundColor: "{colors.fill}"
textColor: "{colors.on-surface}"
typography: "{typography.body}"
rounded: "{rounded.sm}"
padding: "{spacing.sm}"
input-dark:
backgroundColor: "{colors.fill-dark}"
textColor: "{colors.on-surface-dark}"
button:
backgroundColor: "{colors.fill}"
textColor: "{colors.primary}"
typography: "{typography.body}"
rounded: "{rounded.sm}"
padding: "{spacing.sm}"
button-dark:
backgroundColor: "{colors.fill-dark}"
textColor: "{colors.primary-dark}"
button-danger:
textColor: "{colors.danger}"
button-danger-dark:
textColor: "{colors.danger-dark}"
tag:
backgroundColor: "{colors.fill}"
textColor: "{colors.on-surface}"
typography: "{typography.caption}"
rounded: "{rounded.sm}"
padding: "{spacing.xs}"
tag-dark:
backgroundColor: "{colors.fill-dark}"
textColor: "{colors.on-surface-dark}"
alert:
backgroundColor: "{colors.warning-container}"
textColor: "{colors.on-surface}"
rounded: "{rounded.md}"
padding: "{spacing.md}"
alert-dark:
backgroundColor: "{colors.warning-container-dark}"
textColor: "{colors.on-surface-dark}"
header:
backgroundColor: "color-mix(in srgb, #F2F2F7 72%, transparent)"
padding: "{spacing.xl}"
header-dark:
backgroundColor: "color-mix(in srgb, #000000 72%, transparent)"
link:
textColor: "{colors.primary}"
link-dark:
textColor: "{colors.primary-dark}"