Skip to content

[DX-1518] feat: site-wide dark mode + theme toggle#3466

Draft
jamiehenson wants to merge 10 commits into
mainfrom
dx-1518-sitewide-dark-mode
Draft

[DX-1518] feat: site-wide dark mode + theme toggle#3466
jamiehenson wants to merge 10 commits into
mainfrom
dx-1518-sitewide-dark-mode

Conversation

@jamiehenson

Copy link
Copy Markdown
Member

Site-wide dark mode + theme toggle

Adds a site-wide dark mode to the docs, with a light / dark / system toggle. Jira: DX-1518.

The @ably/ui cut-off (DX-1128) is what made this tractable — the localised components already carried their dark: variants and the vendored highlight.js theme already had .ui-theme-dark rules; they were just dormant because nothing ever set the class. This PR activates that and closes the gaps.

How it works

  • No-flash init — an inline script in gatsby-ssr sets ui-theme-light / ui-theme-dark on <html> from localStorage (or the OS preference) before first paint, so the static build never flashes the wrong theme.
  • Theme contextThemeProvider / useTheme (light / dark / system), persisted to localStorage, reflected as the ui-theme-* class which drives Tailwind's dark: variants (darkMode: ['selector', '.ui-theme-dark']). Storage reads are guarded and values normalised, so blocked storage or a corrupt value falls back to the default instead of crashing the root provider.
  • Toggle — a three-cell segmented control (System / Dark / Light) in the footer, under the status unit.
  • Systemic typography — the vendored ui-text-* classes hardcoded dark colours (only labels had a dark override); added dark overrides for headings/body + fixed an inline-code rule that targeted .dark instead of .ui-theme-dark.
  • Page canvasbody had no background, so dark mode showed white behind themed sections; set an explicit body background per theme.
  • JS-driven renderers wired to the resolved theme: the Sandpack examples editor and the Redoc API reference (which gets a dedicated dark theme object, re-initialised safely on toggle — script load memoised, listeners/observer torn down between inits so nothing accumulates).
  • Odds and ends — dark logo variant, admonitions, examples grid, decorative graphics hidden in dark, remaining light-only components.

Verified in-browser (dark + a light/toggle pass)

Homepage, MDX doc pages (prose, tables, admonitions, code blocks, inline code), the Redoc API reference, the examples listing, and header/footer chrome. The Redoc re-init leak fix was measured directly (scroll-listener add/remove balanced across a toggle; single Redoc render). yarn lint clean; 190 unit tests pass.

Out of scope (follow-up)

Content imagery (Phase 4) — inline MDX diagrams/screenshots with baked-in light backgrounds (e.g. the platform architecture diagram, example preview shots) need dark asset variants; deferred to a separate effort. Default theme is deliberately light (not system) until that lands.

Not yet eyeballed: the live Sandpack editor (gated behind a logged-in API key), a mobile/responsive pass, and every component variant (nested API tables, <Tiles>, the code-tab switcher) — the systemic fixes should cover them but they warrant a look on the review app.

Testing

Add the review-app label for a deployment, then use the footer toggle (System / Dark / Light) and walk the page types above.

/cc @team-deved for review.

🤖 Generated with Claude Code

jamiehenson and others added 6 commits July 9, 2026 13:42
Add shared theme constants and an inline pre-paint script, injected into
<head> by gatsby-ssr, that reads the persisted choice (validated against the
known themes, or the system preference) and sets ui-theme-light/ui-theme-dark
on <html> before first paint. On the static build this avoids a flash of the
wrong theme before React hydrates.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ThemeProvider/useTheme exposing the user's choice (light/dark/system),
the resolved light|dark, and a setter that persists to localStorage and
reflects the choice as ui-theme-* on <html>. Follows the system preference
live when set to 'system'. resolvedTheme is exposed for JS-driven consumers
that can't use CSS dark: variants (e.g. the Sandpack editor).

The persisted read is wrapped in try/catch and normalised to a known theme,
so blocked storage (privacy modes, partitioned iframes) or a corrupt value
falls back to the default instead of crashing the root provider or applying
an unknown class.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the root element in ThemeProvider in both gatsby-browser and gatsby-ssr
so useTheme is available across the whole app. Add a ThemeSwitcher dropdown
(Light/Dark/System with the active option checked) to the docs header, on
both the desktop actions cluster and the mobile bar. The trigger renders a
sun and a moon toggled with CSS dark: variants - the theme class is on <html>
before paint, so no client-only state and no hydration mismatch. The header
wordmark also swaps to a white variant (ably-logo-dark.svg) in dark mode via
the same CSS toggle. Stub the theme context in the Header test so its render
path stays provider-free.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The primary Admonition component already carries dark variants; the legacy
Aside (used for new/updated/experimental/public-preview badges) still had
hardcoded light-only colours. Tokenise its container background/border and
add a ui-theme-dark surface override, tokenise the versioning text colour so
it reads on both themes, and give the note/important/further-reading labels
a dark text variant.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Sandpack examples editor was hardcoded to light (githubLight + the
Chrome light variant), leaving the existing dark sandpackTheme unused. Read
resolvedTheme from the theme context and pick the dark or light Sandpack
theme and Chrome variant accordingly. Sandpack takes the theme as JS values
rather than CSS dark: variants, so gate on mount - render light (matching the
server) until hydrated - to avoid a hydration mismatch for dark-mode users.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add dark variants to the last components that assumed a light background:
the demo API-key tooltip and the examples grid cards (surfaces, borders,
title/description text, and the search-term highlight).

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamiehenson jamiehenson added the review-app Create a Heroku review app label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ffd0c0ee-98ab-42ff-a164-068c6ac70955

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dx-1518-sitewide-dark-mode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jamiehenson jamiehenson changed the title feat: site-wide dark mode + theme toggle (DX-1518) [DX-1518] feat: site-wide dark mode + theme toggle Jul 9, 2026
@ably-ci ably-ci temporarily deployed to ably-docs-dx-1518-sitew-yuihru July 9, 2026 13:28 Inactive
jamiehenson and others added 4 commits July 9, 2026 15:50
html/body had no background-color, so in dark mode the browser's default
white showed through behind themed sections. Set an explicit body background
(neutral-000 light, neutral-1300 dark) and a light default text colour.

Flip the vendored ui-text-* typography classes for dark mode: headings
(title/h1-h5/quote) and body copy (p1-p4) hardcoded dark colours with only
labels overridden, so any heading or paragraph without an explicit dark:
variant rendered dark-on-dark. The overrides are wrapped in :where() so they
add no specificity - they still override the hardcoded base but defer to any
explicit dark:text-* a component sets (text.css is imported after Tailwind's
utilities, so without this they would wrongly win, e.g. washing out a tooltip
on a light surface). Also correct an inline-code rule that targeted the wrong
class (.dark vs .ui-theme-dark).

Keep tooltips a dark chip in both themes (dark surface, light text) rather
than inverting to a light chip in dark mode.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several decorative background grids and gradients assume a light page and
read as stray light panels on the dark canvas. In dark mode: hide the
homepage hero pattern, drop the Examples card's light gradient (so it matches
the transparent Changelog card) and hide its background grid, and hide the
examples listing page's desktop/mobile grid patterns.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redoc used a hardcoded light theme, so on the dark page canvas its sidebar
and body text rendered dark-on-dark. Wire the Loader to the theme context:
pass a dark Redoc theme (light text, dark sidebar/right-panel/code blocks and
borders; literal hex since Redoc ignores CSS variables) when resolvedTheme is
dark, and re-init Redoc into a cleared container when the theme changes.

Make the re-init safe: the CDN script load is memoised so toggling while it
downloads can't attach a second onload and double-init, and each init's
menu-navigation listeners/observer are captured and torn down before the next
init and on unmount rather than accumulating (overrideMenuItemNavigation's
disposer is now held in a ref, and its cleanup also disconnects the observer).

Redoc's section sub-headers (h5) use an internal muted dark token the theme
object can't reach; and since redoc.module.css is a CSS module its
.redoc-content selectors were hashed and never matched Redoc's DOM, so use
:global for a dark-mode h5 colour rule that gives them the same light treatment.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the header theme dropdown (ThemeSwitcher) with a three-cell segmented
toggle (System / Dark / Light) in the footer, right-aligned under the status
unit. The toggle is a radiogroup of icon cells that highlights the active
choice and calls setTheme; it defers the active-cell highlight until mount to
avoid a hydration mismatch. Remove ThemeSwitcher from the header (desktop and
mobile) and drop the now-unused theme-context mock from the Header test.

Refs DX-1518

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-app Create a Heroku review app

Development

Successfully merging this pull request may close these issues.

2 participants