Skip to content

Highlight code with dual-theme light-dark() colors so the first frame matches - #988

Merged
RhysSullivan merged 1 commit into
mainfrom
claude/shiki-dual-theme
Jun 12, 2026
Merged

Highlight code with dual-theme light-dark() colors so the first frame matches#988
RhysSullivan merged 1 commit into
mainfrom
claude/shiki-dual-theme

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

Problem

Code blocks resolved their shiki theme in JavaScript (useResolvedShikiThemeuseIsDarkmatchMedia). During SSR there is no window, so the server always rendered light-theme token colors; a dark-mode visitor saw the wrong palette on the first frame (visible on the connect card's npx add-mcp command) until hydration re-resolved the theme and the colors snapped. The streamdown highlighter plugin had a worse variant: its token cache was keyed by whichever color scheme computed first, so a stale palette could stick for the session.

Change

Switch all three highlight call sites to shiki's dual-theme mode with defaultColor: "light-dark()". Both palettes are baked into the markup as CSS light-dark(#light, #dark) colors and the browser's own color scheme picks one:

  • the markup is scheme-independent, so SSR can't render the wrong palette and hydration can't mismatch
  • a live scheme flip restyles instantly with zero re-renders
  • the streamdown token cache no longer needs (or has) a theme key

CodeBlock's tree and the streamdown plugin pick the colors up automatically; ExpandableCodeBlock's custom token renderer now reads token.htmlStyle (where dual-theme colors live) instead of the single-theme token.color. The theme prop still works, now meaning "this theme in both schemes". useResolvedShikiTheme is deleted along with useIsDark's last consumer.

Verification

Against a live stack, signed in via the emulator flow, with /api/account/me held open so the first SSR'd frame is inspectable: computed span colors of the connect-card command compared at first paint vs after settle, in both colorScheme: dark and light browser contexts — byte-identical in both (previously the dark context's first frame showed the light palette). Screenshots of both schemes' first frames confirm visually. Format, lint, and typecheck pass.

Note: the inline style attribute string still changes at hydration (React re-serializes hex to rgb); the computed colors — what's painted — do not.

… matches

The shiki theme was picked in JS (useIsDark → matchMedia), which is
wrong for the SSR'd first frame: the server doesn't know the scheme, so
a dark-mode visitor's connect-card command rendered light-theme token
colors until hydration re-resolved the theme. Dual-theme mode bakes
both palettes into the markup as light-dark(...) colors and the
browser's own color scheme picks one — identical computed colors at
first paint and after settle, in both schemes, and a scheme flip needs
no re-render. The streamdown plugin's token cache also stops being
keyed (and poisoned) by whichever scheme computed first.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing b5b79e4 Commit Preview URL

Branch Preview URL
Jun 12 2026, 08:39 PM

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud b5b79e4 Jun 12 2026, 08:40 PM

@pkg-pr-new

pkg-pr-new Bot commented Jun 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@988

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@988

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@988

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@988

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@988

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@988

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@988

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@988

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@988

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@988

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@988

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@988

executor

npm i https://pkg.pr.new/executor@988

commit: b5b79e4

@RhysSullivan
RhysSullivan marked this pull request as ready for review June 12, 2026 20:44
@RhysSullivan
RhysSullivan merged commit cde6e1a into main Jun 12, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant