Files
Santhosh Janardhanan 70710239c7
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
Theming done
2026-02-10 20:10:06 -05:00

5.0 KiB

Context

The site currently uses a single dark theme defined via CSS variables in site/public/styles/global.css (e.g., --bg0, --bg1, --fg, --muted, --stroke, --accent). There is no existing theme selection mechanism (no data-theme attribute, no persisted preference).

The site shell uses a sticky .site-header and a per-page .subnav row (when present). A theme switcher notch must be fixed-positioned such that it does not overlap the header and leaves enough space for the subnav region.

Goals / Non-Goals

Goals:

  • Provide three themes: dark, light, high-contrast.
  • Allow switching themes via a floating notch on the right side of the screen positioned below the primary nav bar and not overlapping the subnav area.
  • Make switching feel premium:
    • hover animation on the notch
    • smooth theme transitions (without an abrupt flash)
  • Ensure accessibility:
    • keyboard operable
    • visible focus
    • respects prefers-reduced-motion
    • high contrast theme is meaningfully higher contrast, not just a color swap
  • Persist the user's selection across page loads.

Non-Goals:

  • Rebuild the entire visual system or rewrite all CSS to a design-token framework.
  • PWA theming (manifest/theme-color) beyond what is required to implement UI themes.
  • Adding user accounts or server-side persistence of theme preference.

Decisions

1. Theme selection mechanism: data-theme on <html>

Use document.documentElement.dataset.theme = "dark" | "light" | "high-contrast".

Rationale:

  • Works cleanly with CSS variables.
  • Scopes theme styles across the entire page without specificity fights.

Alternatives considered:

  • Adding theme classes to body (works, but html-scoped variables are simpler for form control theming).

2. Token strategy: override existing CSS variables per theme

Keep the existing variable names and provide theme-specific overrides:

  • :root remains the default (dark)
  • html[data-theme="light"] { ... }
  • html[data-theme="high-contrast"] { ... }

Rationale:

  • Minimizes churn in existing CSS.
  • Enables incremental migration of any remaining hard-coded colors to tokens.

3. Default theme resolution order

On first load, resolve the active theme in this order:

  1. stored user preference (localStorage.theme)
  2. forced colors / high-contrast OS mode (if detected) -> high-contrast
  3. system color scheme -> light if prefers-color-scheme: light, else dark

Rationale:

  • User choice wins.
  • If the user is in a forced/high-contrast environment, defaulting to high-contrast aligns with accessibility intent.

4. Prevent flash of wrong theme with a tiny head script

Insert a small inline script in the document <head> that sets data-theme before first paint.

Rationale:

  • Avoids "flash" where the page renders in dark before switching to light/high-contrast.

Trade-off:

  • Inline scripts can constrain future CSP hardening; keep script small and self-contained.

5. Smooth transitions without animating on every page load

Use a transient attribute/class (e.g., data-theme-transition="on") only during user-initiated theme changes.

Implementation shape:

  • When switching: set data-theme-transition="on", update data-theme, then remove after ~250ms.
  • CSS applies transitions for color/background/border/shadow only when the attribute is present.

Rationale:

  • Avoids "everything animates" feeling during initial load.
  • Avoids subtle jank on navigation.

6. Notch UI: fixed-position, expands on hover/focus

Implement the switcher as a fixed-position control at the right edge:

  • Default collapsed: a small vertical tab.
  • On :hover and :focus-within: expands into a small panel exposing the three theme options.

Accessibility decisions:

  • Use a real <button> to open/close (for touch) OR a <fieldset role="radiogroup"> with three radio-like buttons.
  • Ensure it is reachable via keyboard and has clear aria-labels.

Placement decisions:

  • Use a CSS variable --theme-notch-top to position it.
  • A small inline script computes this based on .site-header height and, if a .subnav exists near the top, positions below it.

7. High Contrast theme semantics

The high-contrast theme will be a dedicated palette (not only increased brightness) with:

  • strong background/foreground contrast
  • high visibility focus ring
  • more assertive stroke borders

Additionally, handle OS forced-colors mode:

  • In @media (forced-colors: active), prefer system colors and avoid gradients that reduce clarity.

Risks / Trade-offs

  • [Notch overlaps content] -> compute top offset from header/subnav; provide safe-area padding; add responsive rules for small viewports.
  • [Theme transitions reduce readability] -> scope transitions to a short window and limit properties; disable via prefers-reduced-motion.
  • [High contrast breaks brand feel] -> keep layout/typography unchanged and only adjust palette and borders.
  • [CSP constraints] -> keep head script minimal and consider moving to an external script if CSP hardening becomes a priority.