글 전체

·4분

두 개의 서피스로 만드는 다크 모드

색을 반전하는 대신 표면 단계만 바꾸면 생기는 것들. 토큰 설계 기록.

#css#design

다크 모드를 만들 때 가장 흔한 실수는 색을 반전하는 것입니다. 흰 배경을 검게, 검은 글자를 희게 바꾸면 끝날 것 같지만 그렇지 않습니다.

반전이 무너지는 지점 #

이 사이트도 처음엔 섹션마다 배경과 글자를 뒤집었습니다. 문제는 세 군데서 동시에 터졌습니다.

  • 다크 모드인데 화면 절반이 흰 종이라 밤에 눈이 아픕니다
  • 커버 이미지가 밝은 섹션에서만 떠 보입니다
  • 헤더처럼 어디에도 속하지 않는 고정 요소가 색을 잃습니다

세 번째가 특히 고약합니다. 색 토큰이 정의되지 않은 자리에서 color 는 브라우저 기본값으로 떨어지는데, 다크 모드에서 그건 흰색입니다. 밝은 배경 위에서 흰 글자는 그냥 사라집니다.

역할을 두 축으로 나눕니다 #

해법은 하나의 축을 두 개로 쪼개는 것이었습니다.

축 정하는 것 값
테마 글자색, 선, 강조색 dark · light
서피스 배경 단계만 1 · 2

테마가 글자를 정하고, 서피스는 배경만 정합니다. 그러면 서피스가 바뀌어도 글자색은 그대로입니다.

[data-theme="dark"] {
  --bg-1: #0a0b0d;
  --bg-2: #14161c;
  --fg:   #e6e8ec;
}

[data-surface="1"] { --bg: var(--bg-1); }
[data-surface="2"] { --bg: var(--bg-2); }

[data-surface] {
  background-color: var(--bg);
  color: var(--fg);
}

두 서피스 모두 어둡습니다. 이게 핵심입니다. 대비는 배경 차이(약 4%)와 1px 선과 여백으로 만듭니다.

세 단계를 만들지 않는 이유 #

--bg-3 을 추가하고 싶은 순간이 반드시 옵니다. 카드 안에 카드를 넣을 때요. 그때는 서피스를 늘리는 대신 --card 를 씁니다.

--ink-card: rgba(230, 232, 236, 0.045);

배경색이 아니라 배경 위에 얹는 아주 얇은 층입니다. 이러면 서피스 1 위에 올라가든 2 위에 올라가든 관계가 유지됩니다. 단계를 늘리면 조합이 곱으로 늘어나고, 어느 조합에서 대비가 깨지는지 추적할 수 없게 됩니다.

밝은 테마는 순백을 쓰지 않습니다 #

--paper-1: #ece9e2;   /* 흰색이 아니라 따뜻한 상아색 */
--paper-fg: #191815;  /* 파란 검정이 아니라 따뜻한 검정 */

#ffffff 는 눈이 시립니다. 그리고 커버 이미지가 배경에서 떠 보입니다. 상아색으로 조금 내리면 두 문제가 같이 해결됩니다.

고정 요소는 뒤를 따라가게 합니다 #

헤더와 점 내비는 어떤 섹션에도 속하지 않습니다. 그래서 JS 로 뒤에 있는 섹션의 서피스를 따라가게 했습니다.

var current = surfaceSections[0];
for (var i = 0; i < surfaceSections.length; i++) {
  var r = surfaceSections[i].getBoundingClientRect();
  if (r.top <= line && r.bottom > line) { current = surfaceSections[i]; break; }
}
if (current && header) header.dataset.surface = current.getAttribute("data-surface");

그리고 그 요소의 background 는 transparent !important 로 지웁니다. data-surface 가 배경색을 넣어버리기 때문입니다.

남은 규칙 하나 #

강조색은 화면의 1% 만 차지해야 합니다. 쓰는 자리를 목록으로 못 박아 두었습니다. 링크, 활성 상태, 진행 표시, 터미널 프롬프트, 브랜드 마크. 그 밖에서는 쓰지 않습니다.

띠나 배너처럼 넓은 면을 강조색으로 채우면, 그 색이 대표색처럼 보이기 시작하고 정작 강조해야 할 곳이 묻힙니다.