이 글은 Claude Code로 요약하였습니다
prefers-color-scheme 미디어 쿼리CSS 미디어 쿼리의 일종으로, 사용자의 OS 또는 브라우저 설정이 다크 모드인지 라이트 모드인지를 감지한다.
@media (prefers-color-scheme: dark) {
/* OS가 다크 모드일 때 적용 */
}
JS 없이 순수 CSS만으로 OS 테마를 감지할 수 있어, 초기 렌더링 시 깜빡임(FOUC) 없이 테마를 적용할 수 있다.
data-theme 속성 패턴<html> 또는 최상위 요소에 data-theme="dark" 같은 속성을 붙이고, CSS에서 이를 셀렉터로 활용해 테마를 전환하는 패턴이다.
[data-theme="dark"] {
--color-bg-main: #242424;
}
JS로 속성값만 바꾸면 전체 CSS 변수가 일괄 교체되어, 컴포넌트 코드는 건드리지 않아도 된다.
@storybook/addon-themesStorybook 툴바에 테마 토글 버튼을 추가해주는 공식 애드온. withThemeByDataAttribute 데코레이터를 제공하며, 토글 클릭 시 story iframe의 <html> 요소에 지정한 data-* 속성을 자동으로 적용한다.
preview.ts에 데코레이터 등록 후 main.ts의 addons 배열에 추가manager.tsStorybook UI(사이드바, 툴바 등 shell 영역)의 테마를 설정하는 파일. story 콘텐츠가 렌더링되는 iframe과는 별개의 영역이다.
addons.setConfig({ theme: themes.dark }) 호출
tokens.css—prefers-color-scheme미디어 쿼리 추가
↓
Storybook 애드온 설치 및preview.ts설정
↓preview.css— docs 배경 다크 모드 오버라이드
↓manager.ts— Storybook UI 다크 테마 적용
tokens.css — prefers-color-scheme 미디어 쿼리 추가기존에는 [data-theme="dark"] 셀렉터만 있어서, JS로 속성을 직접 세팅해야만 다크 모드가 적용됐다. OS 테마를 자동으로 따라가게 하려면 prefers-color-scheme 미디어 쿼리를 추가해야 한다.
셀렉터를 :root:not([data-theme="light"])로 구성한 게 핵심이다. 이렇게 하면 세 가지 경우를 모두 처리할 수 있다.
| 상황 | 결과 |
|---|---|
OS 다크 모드, data-theme 없음 | 다크 모드 적용 |
OS 다크 모드, data-theme="light" | 라이트 모드 강제 유지 |
data-theme="dark" 명시 | 기존 셀렉터가 처리 |
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--color-bg-main: #242424;
--color-bg-sub: #1a1a1a;
--color-contents-default: #ffffff;
/* ... 나머지 다크 모드 변수들 */
}
}
/* 명시적 다크 모드 (JS로 토글하는 경우) */
[data-theme='dark'] {
--color-bg-main: #242424;
/* ... 동일한 변수들 */
}
두 블록의 변수값은 동일하다. OS 자동 감지와 수동 토글 모두 같은 다크 테마를 적용한다.
preview.ts 설정npm install --save-dev @storybook/addon-themes
main.ts의 addons 배열에 등록하고, preview.ts에 withThemeByDataAttribute 데코레이터를 추가한다.
// .storybook/preview.ts
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import type { Preview, ReactRenderer } from '@storybook/react-vite';
const preview: Preview = {
decorators: [
withThemeByDataAttribute<ReactRenderer>({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
attributeName: 'data-theme', // <html data-theme="light|dark">
}),
],
// ...
};
이 데코레이터는 Storybook 툴바에 Light / Dark 토글을 추가하고, 선택된 테마값을 story iframe의 <html> 요소에 data-theme 속성으로 주입한다. tokens.css의 [data-theme="dark"] 셀렉터가 그대로 반응한다.
preview.css — docs 배경 다크 모드 오버라이드Storybook docs 모드에서는 Storybook 자체 스타일이 배경색을 덮어씌운다. data-theme="dark" 가 <html>에 적용돼도 docs 페이지 배경이 흰색으로 남는 문제가 있어, !important로 직접 오버라이드했다.
/* .storybook/preview.css */
/* 툴바 토글로 Dark 선택 시 */
html[data-theme='dark'] body,
html[data-theme='dark'] #storybook-docs,
html[data-theme='dark'] .sbdocs-wrapper,
html[data-theme='dark'] .sbdocs-content {
background-color: #242424 !important;
color: #ffffff;
}
html[data-theme='dark'] .docs-story {
background-color: #1a1a1a !important;
}
/* OS 다크 모드 자동 감지 */
@media (prefers-color-scheme: dark) {
html:not([data-theme='light']) body,
html:not([data-theme='light']) #storybook-docs,
html:not([data-theme='light']) .sbdocs-wrapper {
background-color: #242424 !important;
}
}
이 파일은 preview.ts에서 import해 story iframe에 주입된다.
manager.ts — Storybook UI 다크 테마 적용docs 배경과 달리, Storybook 사이드바·툴바 영역(manager)은 story iframe 외부에 있어 CSS 오버라이드가 통하지 않는다. storybook/theming의 빌트인 테마를 사용해야 한다.
// .storybook/manager.ts
import { addons } from 'storybook/manager-api';
import { themes } from 'storybook/theming/create';
const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
addons.setConfig({
theme: isDark ? themes.dark : themes.light,
});
@storybook/theming 패키지를 별도 설치할 필요 없이 storybook 패키지에 포함된 storybook/theming/create에서 가져올 수 있다. OS 테마를 window.matchMedia로 감지해 초기 테마를 결정한다.
manager는 story 툴바 토글과 연동되지 않는다. OS 설정 기준으로만 동작한다.