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.

01

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.

02

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.

03

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.

04

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.