테마
스타일시트를 불러오고, 앱의 라이트와 다크 모드를 따라가고, 색 토큰을 덮어써요.
차트의 색은 앱이 불러오는 스타일시트의 CSS 커스텀 속성에서 가져와요. 앱의 라이트와 다크 모드는 별다른 설정 없이 따라가요.
스타일시트 불러오기
앱 어디에서든 스타일시트를 한 번만 불러오면 돼요. 컴포넌트만 가져오면 스타일은 하나도 딸려오지 않아요.
import { useState } from 'react';
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';
const initialTasks: Task[] = [
{ id: 'spec', name: 'Spec', parentId: null, sequence: '1',
startDate: '2026-03-02', endDate: '2026-03-06' },
{ id: 'build', name: 'Build', parentId: null, sequence: '2',
startDate: '2026-03-09', endDate: '2026-03-20', progress: 40 },
];
export function ProjectChart() {
const [tasks, setTasks] = useState<Task[]>(initialTasks);
return (
<ReactGanttChart
tasks={tasks}
onTasksChange={setTasks}
showTaskList
height={420}
/>
);
}불러오지 않으면 차트는 마운트되고 클릭에도 반응하지만 읽기 어려워요. 토큰 선언과 색은 물론 고정
헤더, 막대 지오메트리, 컨테이너의 flex 컬럼, 레이아웃 계산이 전제하는 box-sizing: border-box까지
모두 이 파일에 있어요.
이 규칙들은 .gantt-container와 그 하위 요소에만 적용되니 라이브러리가 전역 리셋을 넣지 않아요.
앱의 테마 따라가기
theme은 생략할 수 있고, 생략하는 것과 theme="light"는 달라요. prop이 없으면 <html>에서 읽은
페이지의 color-scheme을 따라가요.
:root { color-scheme: light; }
.dark { color-scheme: dark; }color-scheme은 플랫폼이 정한 라이트와 다크 신호이고 상속돼요. next-themes와 그 위에 올린
Tailwind, fumadocs 구성이 이미 이 값을 쓰니 theme prop이 없는 차트는 추가 설정 없이 앱의 토글을
따라가요.
MutationObserver가 <html>의 class, style, data-theme을 감시해서 새로고침이나 리마운트
없이 다음 렌더에서 다시 칠해요. 읽는 대상은 <html>뿐이라 <body>나 래퍼에 설정한 값은 차트에
닿지 않아요.
차트 하나를 페이지와 무관하게 고정하려면 'light'나 'dark'를 넘기고, 앱 테마가 고정된 화면에서
OS만 따라가게 하려면 'system'을 넘겨요. 페이지의 color-scheme이 light dark면 선택을 OS에
맡기고, 한 번도 설정하지 않은 페이지는 normal로 계산되어 라이트로 읽혀요.
서버 렌더와 첫 하이드레이션 렌더에서는 'system'과 생략한 prop 모두 라이트로 결정돼요.
컨테이너에는 항상 결정된 data-theme이 붙고 스타일시트는 이 속성만 읽어요. className은 뒤에
붙기만 할 뿐 테마와 무관해서 dark라는 클래스를 넘겨도 충돌하지 않아요.
위 데모는 theme="dark"로 고정돼 있어서 사이트의 테마 스위치를 눌러도 바뀌지 않아요.
커스텀 속성
토큰 26개는 모두 :root가 아니라 .gantt-container에 선언돼요. :root에 두면 앱의
--background나 --border와 충돌하기 때문이에요. 다크 블록은 그중 20개를 다시 정의하고, 표에
같음으로 적은 6개는 두 테마가 함께 써요.
스타일시트는 컨테이너에 color-scheme도 설정해서 브라우저가 스크롤바와 캐럿을 테마에 맞게 칠해요.
| 토큰 | 라이트 | 다크 | 칠하는 곳 |
|---|---|---|---|
--gantt-background | #fafafa | #09090b | 컨테이너, 두 열, 헤더, 상세 패널 |
--gantt-foreground | #18181b | #fafafa | 본문 텍스트와 그리드 셀 |
--gantt-muted | #f4f4f5 | #18181b | 하단 헤더 행, 행 호버와 선택 |
--gantt-muted-foreground | #71717a | #a1a1aa | 보조 텍스트, 크기 조절 그립과 행 그립 |
--gantt-border | #e4e4e7 | #27272a | 헤더와 그리드 테두리, 스크롤바 thumb |
--gantt-border-subtle | rgba(0, 0, 0, 0.04) | rgba(255, 255, 255, 0.04) | 행 구분선 |
--gantt-accent | #3b82f6 | 같음 | 포커스 링, 선택 표시, 링크 미리보기 |
--gantt-bar-bg | #c7c7cd | #3a3a41 | 작업 막대 |
--gantt-bar-bg-hover | #b4b4bc | #47474f | 같은 막대의 호버 |
--gantt-bar-text | #18181b | #fafafa | 막대 안의 작업 이름 |
--gantt-bar-shadow | none | none | 평소 상태의 막대 |
--gantt-bar-shadow-hover | 0 1px 2px rgba(0, 0, 0, 0.08) | 0 1px 2px rgba(0, 0, 0, 0.4) | 호버된 막대 |
--gantt-bar-shadow-drag | 0 4px 12px rgba(0, 0, 0, 0.16) | 0 4px 12px rgba(0, 0, 0, 0.5) | 드래그 중인 막대 |
--gantt-shadow-overlay | 0 4px 16px rgba(0, 0, 0, 0.15) | 0 8px 24px rgba(0, 0, 0, 0.55)에 1px 링 추가 | 막대 툴팁 |
--gantt-summary-bg | #dcdce0 | #2a2a2f | 요약 행의 막대 |
--gantt-summary-bg-hover | #cfcfd4 | #35353c | 같은 막대의 호버 |
--gantt-progress-bg | #8e8e97 | #6c6c76 | 작업 막대의 진행률 채움 |
--gantt-progress-handle | #52525b | #a1a1aa | 진행률 핸들 테두리 |
--gantt-arrow | #a1a1aa | #71717a | 화살표 선과 화살촉, 평소에는 불투명도 45% |
--gantt-today-marker | #f43f5e | #fb7185 | 오늘 선과 거부된 상태 |
--gantt-non-working-bg | rgba(0, 0, 0, 0.035) | rgba(255, 255, 255, 0.03) | 비근무일 열 |
--gantt-detail-width | 320px | 같음 | 도킹된 상세 패널의 너비 |
--gantt-font-sans | Pretendard 다음 시스템 폰트 | 같음 | 컨테이너의 font-family |
--gantt-duration-fast | 150ms | 같음 | 툴팁 페이드인 |
--gantt-transition-fast | 150ms ease | 같음 | 막대 배경, 펼침 표시 회전, 그립과 핸들 |
--gantt-transition-normal | 200ms ease | 같음 | 막대의 box-shadow |
요약 행의 롤업 채움은 --gantt-foreground를 불투명도 25%로 써서 --gantt-progress-bg가 닿지
않아요. 의존성 화살표는 화살표나 양 끝 작업 중 하나가 호버나 선택 상태가 되면 불투명해져요.
--gantt-arrow 대신 --gantt-accent를 쓰는 경우는 화살표 자체를 호버하거나 선택했을 때뿐이에요.
어떤 날을 음영 처리할지는 showNonWorkingDays, workingWeekdays, holidays가 정하고 자세한
내용은 타임라인에 있어요. 작업의 color는 막대 토큰보다 먼저 읽히니 색을 지정한 작업은
막대 토큰을 무시해요. 스타일링에서 확인해요.
폰트는 번들에 넣지도 받아오지도 않아요. 스타일시트가 Pretendard를 이름으로 지정하고 없으면 시스템
폰트로 넘어가니 폰트 제공은 앱의 몫이에요. 기본 크기는 13px이고 토큰이 아니에요.
prefers-reduced-motion: reduce 아래에서는 컨테이너 안의 모든 트랜지션과 애니메이션이 0.01ms로,
scroll-behavior는 auto로 강제돼요. 모션 토큰을 어떻게 설정했든 마찬가지예요.
키보드와 스크린 리더에서 확인해요.
토큰 덮어쓰기
덮어쓸 값은 .gantt-container에 선언하고 스타일시트는 라이브러리 스타일시트 뒤에 불러오세요. 다크
블록이 다시 정의하지 않는 토큰 6개는 규칙 하나로 충분해요. 나머지 20개는 규칙 두 개가 필요해요.
.gantt-container만 쓴 규칙은 명시도가 (0,1,0)이라 (0,2,0)인
.gantt-container[data-theme="dark"]에 밀려 라이트 모드에서만 적용되기 때문이에요.
.gantt-container {
--gantt-bar-bg: #dbeafe;
--gantt-bar-bg-hover: #bfdbfe;
}
.gantt-container[data-theme="dark"] {
--gantt-bar-bg: #1e3a5f;
--gantt-bar-bg-hover: #2b4f7d;
}토큰을 :root에 설정하면 효과가 없어요. 토큰은 컨테이너 자체에 선언돼 있고, 요소에 직접 붙은
선언이 상속된 값보다 우선해요.
소스 순서는 같은 캐스케이드 레이어 안에서만 우선순위를 정해요. 라이브러리 스타일시트를 레이어로 가져오면 레이어 순서가 대신 정하고 레이어 없이 쓴 규칙이 이겨요.
차트 하나에만 적용하기
className을 넘기고 [data-theme="dark"] 규칙까지 모든 선택자에 그 클래스를 덧붙이세요. 선택자마다
라이브러리보다 클래스가 하나 더 붙어서 이겨요.
.gantt-container.planning-chart {
--gantt-bar-bg: #dbeafe;
}
.gantt-container.planning-chart[data-theme="dark"] {
--gantt-bar-bg: #1e3a5f;
}적용 전에 확인하기
GanttTheme은'light' | 'dark' | 'system'유니온이에요.tokensprop, 테마 객체, 내보내는 기본 팔레트, 테마 프로바이더는 없어요.theme값은 검증하지 않아요. 알 수 없는 값을 넘겨도 경고 없이 생략한 것처럼 동작해요.- 지오메트리는 토큰이 아니에요.
--gantt-row-height는 컴포넌트가 JS 상수를 인라인으로 쓰기 때문에 스타일시트로 덮어써도 밀려요. - 상세 패널의 슬라이드는 모션 토큰과 무관한 고정
200ms이고, 의존성 삭제 글리프의 선은 흰색으로 하드코딩돼 있어요. 토큰이 닿지 않는 유일한 색이에요. - RTL은 지원하지 않아요.
dir을 읽거나 설정하지 않고 스타일시트는 전부 물리 속성을 써요.
props와 기본값은 GanttProps에 있어요.