[DX-1518] feat: site-wide dark mode + theme toggle#3466
Conversation
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>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
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>
897fb79 to
ee50ba6
Compare
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>
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>
Change the dark-mode inversion of neutral-300 borders from neutral-1000 to neutral-1100 for subtler, lower-contrast borders across cards, panels, tables, dropdowns and dividers site-wide. Two off-standard borders (neutral-200 and the legacy light-grey) are normalised to the standard neutral-300 / neutral-1100 pair so the border system is uniform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flip the default theme from light to system so the docs follow the reader's OS preference on first visit. The no-flash script and theme context already resolve 'system' via matchMedia, so this changes only the DEFAULT_THEME constant (plus the two tests asserting the old default). Some content imagery is still light-only (DX-1518 phase 4), so dark-preferring readers may see light-background diagrams on dark pages until those variants land. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@m-hulbert @AndyTWF teed this up as a code review as from this point there's only gonna be incremental styling updates that won't change the body of the work. I appreciate you're both busy men, so will rope in other reviewers if this is too far down your list. Before merge, this is pending signoff from design, but kicking off now as I believe we're close after two rounds. |
Two issues with the vendored ui-text-* dark-mode overrides: - Specificity: label1-4 and code-inline used plain selectors, so — because text.css loads after Tailwind's utilities — they beat any call-site dark:text-* instead of deferring. That flattened intended hierarchy in dark mode: breadcrumbs lost their current-page/section dimming and the product-bar/header/language-selector inactive states were forced bright. Wrap every role override in :where() (zero specificity), like the heading/body overrides, so they beat the hardcoded base by source order yet defer to an explicit call-site dark:text-*. - Inversion shades: align every override to the palette's mirror (light-N -> dark-(1300-N)): code-inline bg 1000->1100, label 300->400, sub-header 300->500. (Body/headings already matched.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0e834d8 to
cb6b300
Compare
AndyTWF
left a comment
There was a problem hiding this comment.
Looking good, my retinas thank you. I would move the theme switcher to the top of the page if possible, the vast majority of people will probably never spot it down the bottom.
| { value: 'light', label: 'Light theme', icon: SunIcon }, | ||
| ]; | ||
|
|
||
| const ThemeToggle: React.FC = () => { |
There was a problem hiding this comment.
Feels like there should be a basic test here?
| <Head title={meta_title} metaTitle={meta_title} canonical={canonical} description={meta_description} /> | ||
| <div className="ml-2 mt-8 mb-6"> | ||
| <div className="text-gui-default hover:text-gui-hover focus:text-gui-focus focus:outline-gui-focus group ui-text-p2"> | ||
| <div className="text-gui-default hover:text-gui-hover dark:text-gui-default-dark dark:hover:text-gui-hover-dark focus:text-gui-focus focus:outline-gui-focus group ui-text-p2"> |
There was a problem hiding this comment.
Very small nit, we use this in many places, is it worth a shared constant to prevent future divergence?
Site-wide dark mode + theme toggle
Adds a site-wide dark mode to the docs, with a light / dark / system toggle that defaults to the reader's OS preference. Jira: DX-1518.
The
@ably/uicut-off (DX-1128) is what made this tractable — the localised components already carried theirdark:variants and the vendored highlight.js theme already had.ui-theme-darkrules; they were just dormant because nothing ever set the class. This PR activates that and closes the gaps.How it works
gatsby-ssrsetsui-theme-light/ui-theme-darkon<html>fromlocalStorage(or the OS preference) before first paint, so the static build never flashes the wrong theme.ThemeProvider/useTheme(light / dark / system), persisted tolocalStorage, reflected as theui-theme-*class which drives Tailwind'sdark: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.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.darkinstead of.ui-theme-dark.bodyhad no background, so dark mode showed white behind themed sections; set an explicit body background per theme.Design-review polish
A round of designer feedback, addressed on top of the base implementation:
currentColorso they inherit the theme-aware text colour. (Next.js and Postgres are two-tone marks that need a separate dark asset — deferred, see below.)dark:hidden/hidden dark:block)..ui-linkplus the hardcoded blue call sites (FeaturedLink, API-reference breadcrumbs) resolve to the GUI blue (#4DA6FF) in dark.100 / 900(light / dark), strokes standardised to500 / 500;neutralunchanged (100 / 1200,500 / 800);uspkept deliberately borderless/fill-less with anorange-700 / 500stroke. usp headline made visible in dark.neutral-000.orange-1100tint (keeping theorange-600accent border); also fixed a left-nav light-on-light legibility bug where the selected item kept its lightorange-100fill in dark.color-schemenow follows the theme, so scrollbars and form controls render dark.neutral-300borders moved fromneutral-1000toneutral-1100site-wide (cards, panels, API tables, dropdowns, code-block chrome, dividers) for subtler, lower-contrast edges; two off-standard borders (neutral-200, legacylight-grey) normalised to the standardneutral-300/neutral-1100pair.ui-text-*dark overrides (title/h1–h5/quote,p1–p4,sub-header,label1–4,code-inline, links) auto-flip role-classed text without a per-call-sitedark:. All are wrapped in:where()(zero specificity) so they beat the hardcoded base by source order yet defer to any explicit call-sitedark:text-*— which is what preserves e.g. the breadcrumb's dimmed current-page crumb and inactive nav/tab states (text.cssloads after Tailwind's utilities, so a plain override would wrongly win). 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 lintclean; 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. The default theme is
system(it follows the reader's OS), so dark-preferring readers may see these light-background images on dark pages until the dark variants land.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
currentColorcan'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-applabel 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