Skip to content

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

Open
jamiehenson wants to merge 15 commits into
mainfrom
dx-1518-sitewide-dark-mode
Open

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

Conversation

@jamiehenson

@jamiehenson jamiehenson commented Jul 9, 2026

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.

Design-review polish

A round of designer feedback, addressed on top of the base implementation:

  • Tech-logo glyphs — Objective-C, PHP, Vercel and the generic web/code glyph hardcoded near-black fills (invisible on dark); switched to currentColor so they inherit the theme-aware text colour. (Next.js and Postgres are two-tone marks that need a separate dark asset — deferred, see below.)
  • Homepage platform card — added dark variants of the three stack-layer SVGs, CSS-toggled against the light set (dark:hidden / hidden dark:block).
  • Links.ui-link plus the hardcoded blue call sites (FeaturedLink, API-reference breadcrumbs) resolve to the GUI blue (#4DA6FF) in dark.
  • Admonitions — designer palette applied: coloured fills at 100 / 900 (light / dark), strokes standardised to 500 / 500; neutral unchanged (100 / 1200, 500 / 800); usp kept deliberately borderless/fill-less with an orange-700 / 500 stroke. usp headline made visible in dark.
  • Tiles — headings flip to neutral-000.
  • Navigation selection — header, product-bar and left-nav selected states softened from the bold orange to a subtle orange-1100 tint (keeping the orange-600 accent border); also fixed a left-nav light-on-light legibility bug where the selected item kept its light orange-100 fill in dark.
  • Native controlscolor-scheme now follows the theme, so scrollbars and form controls render dark.
  • Typography safety net — the ui-text-* dark overrides (title/h1–h5/quote, p1–p4, label1–4, code-inline, links) auto-flip role-classed text without a per-call-site dark:; extended to sub-header. A sweep confirmed residual dark-on-dark risk is effectively nil apart from the two deferred two-tone logos.

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 design-review round above was verified the same way — homepage platform card, <Tiles> grid (incl. the Objective-C / PHP / web tech-logo tiles), the header / product-bar / left-nav selection states, and each admonition variant. 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.

Two-tone tech-logo glyphs — Next.js and Postgres are multi-tone marks (a filled shape with a contrasting mark knocked over it); a single currentColor can't flip both tones, so correct dark rendering needs a separate dark asset with a CSS toggle (like the platform-card layers). Deferred.

Not yet eyeballed: the live Sandpack editor (gated behind a logged-in API key), a mobile/responsive pass, and the nested API reference tables — 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: 053d68ef-f62c-4c93-932e-0388167c737a

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>
@jamiehenson jamiehenson force-pushed the dx-1518-sitewide-dark-mode branch from 897fb79 to ee50ba6 Compare July 9, 2026 14:51
@ably-ci ably-ci temporarily deployed to ably-docs-dx-1518-sitew-yuihru July 9, 2026 14:51 Inactive
@jamiehenson jamiehenson marked this pull request as ready for review July 14, 2026 09:58
jamiehenson and others added 3 commits July 14, 2026 12:29
Objective-C, PHP, Vercel and the generic web/code glyph hardcoded a
near-black fill, rendering them invisible on the dark canvas. Switch
them to currentColor so they inherit the theme-aware text colour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add dark variants of the three platform-stack layers and CSS-toggle
them against the light set (dark:hidden / hidden dark:block) so the
homepage platform card reads correctly in both themes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Links resolve to the GUI blue (#4DA6FF) in dark, incl. hardcoded call
  sites (FeaturedLink, API reference breadcrumbs)
- Normalise admonition backgrounds to the -800 weight; make the usp
  header visible in dark
- Tiles headings flip to neutral-000
- color-scheme follows the theme so native scrollbars/controls go dark
- Extend the ui-text-* dark safety net to sub-header

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ably-ci ably-ci temporarily deployed to ably-docs-dx-1518-sitew-yuihru July 14, 2026 11:30 Inactive
jamiehenson and others added 2 commits July 14, 2026 12:50
The header, product-bar and left-nav selected states read as too bold in
dark mode. Switch the fill from orange-1000 to orange-1100 (a subtle warm
tint), and give the left-nav selection a dark variant — it previously kept
the light orange-100 fill in dark mode, rendering as a bright block with
near-invisible light-on-light text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deepen the coloured admonition dark fills to -900 and standardise the
coloured strokes to -500 in both themes (note/further-reading/warning
were -400 in dark). usp keeps its deliberate empty background; its stroke
moves to orange-700 (light) / orange-500 (dark). neutral is unchanged.

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