Dark Mode
The shared light/dark theme used across all BSS services. It works by flipping a single token set — no separate dark design is maintained per component. Use the Light/Dark toggle in the header to see this page respond in real time.
How It Works
Flash-free SSR + subdomain sync. A single cookie shares the theme preference across all subdomains — edu, data, portal, and more.
Storage — theme cookie
The theme cookie (values: light | dark) is written as domain=.ggbss.or.kr; path=/; max-age=1y; samesite=lax. The leading dot makes the preference readable across every subdomain.
Application — flash-free SSR
The server (layout.tsx) reads the cookie and sets <html lang="ko" data-theme={theme}> during SSR. The correct theme is in place from the very first paint, so there is no FOUC. The default value is light.
Toggle — ThemeToggle
The ThemeToggle client component lives in the header .account area (desktop) and at the bottom of the mobile drawer. On click it: ① immediately updates document.documentElement.dataset.theme (the html, body transition in globals provides a smooth crossfade), ② writes the cookie, and ③ dispatches a window themechange event.
Client subscription — JS color sync
Anywhere colors are set imperatively in JS — charts, canvas, etc. (e.g. the /data explorer) — the current theme is tracked via a MutationObserver(html[data-theme]) + themechange event listener. The initial value is received as an SSR cookie prop to keep hydration in sync.
Dark Token Set
The tokens.css [data-theme="dark"] block. Override only the base --c-* tokens and the aliases in globals.css (--bg, --card-bg, --text-main, etc.) follow automatically. The swatches below render each token's actual dark value.
Canvas / BG
--c-canvas / --bg
#0a1622
Page background
Ink Deep
--c-ink-deep
#ffffff
Heading & emphasis text — white in dark mode
Body
--c-body
#ecf0f3
Body text
Grey
--c-grey
#c3ccd6
Secondary text & captions
Light Grey
--c-light-grey
#1f3a59
Dark surface tint (input fill & section background)
Line Soft
--c-line-soft
rgba(255,255,255,.10)
Hairline divider
Line Default
--c-line-default
rgba(255,255,255,.24)
Input/card outline — default
Line Hover
--c-line-hover
rgba(255,255,255,.85)
Input/card outline — hover
Line Focus
--c-line-focus
#58C5FF
Focus ring — pinned to sky
Alias Re-pinning
globals.css's --color-white: var(--c-canvas) and the footer's --c-ink-deep are flipped by the overrides above. They must be re-pinned inside the dark block so that button labels stay white and the footer background renders correctly.
Color White
--color-white
#ffffff
Pinned for button label text — stays white even when canvas flips
Footer BG
--footer-bg
#0e2236
Dark footer on light; one step deeper surface in dark mode
Dark Design Rules
Four rules to internalize before implementing dark mode. Prefer token overrides over per-component exceptions; reach for the exception pattern only when truly necessary.
Restrict text colors
Dark text must use only white, sky (#58C5FF), light-blue (#CDEAFF), or light-grey (#ECF0F3). For links and emphasis, use --c-sky instead of primary blue — primary is too dark against a dark background.
Separate surfaces with borders
In dark mode, cards and sections are distinguished by a single border on --card-bg (= the canvas) — no separate light fill. Stacking fill layers destroys contrast.
No light-on-light gradients
Light washes (hero sections, etc.) must be overridden with a dark wash for dark mode (e.g. #102a44 → --c-canvas). Express depth with solid color + lines, not layered washes.
Component dark overrides
Add per-component dark corrections exclusively via :global(html[data-theme="dark"]) .className { … } in the CSS module. Never touch light values — layer the override as a separate rule.