Skip to content

fix(react-grab): harden app theme detection (oklch backgrounds, color-scheme, body markers)#476

Merged
aidenybai merged 3 commits into
mainfrom
cursor/fix-oklch-theme-detection-153b
Jun 17, 2026
Merged

fix(react-grab): harden app theme detection (oklch backgrounds, color-scheme, body markers)#476
aidenybai merged 3 commits into
mainfrom
cursor/fix-oklch-theme-detection-153b

Conversation

@aidenybai

@aidenybai aidenybai commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Problem

react-grab paints its overlay in the inverse of the detected app theme so it stays legible. On a light page, the host should carry data-rg-theme="dark".

Several real-world setups were mis-detected, leaving a low-contrast overlay (e.g. a clearly-light page reporting data-rg-theme="light"):

  1. oklch() backgrounds — e.g. Tailwind v4, which authors colors in oklch.
  2. Dual color-scheme — sites that opt into both schemes via color-scheme: light dark.
  3. Body-level theme markers — apps that mark the theme on <body> instead of <html>.

Root causes & fixes

1. oklch backgrounds

detectTheme() falls back to a background-luminance heuristic, which parsed getComputedStyle().backgroundColor with an rgb()-only regex. Modern browsers serialize computed colors in the authored color space (verified in Chromium: an oklch(1 0 0) background comes back verbatim as "oklch(1 0 0)"). The regex never matched, luminance returned null, and detection fell through to prefers-color-scheme — so a forced-light page looked dark to a dark-OS visitor.

Fix: reuse the package's existing canonical color stack — parseAnyColor (which already resolves oklch via exact OKLab→sRGB math, plus named/rgb/hsl/hwb via canvas) followed by parseHexChannels — for the luminance read. This needs no getImageData, so it's also immune to canvas-fingerprinting perturbation (Brave/Tor/Firefox-RFP). Backgrounds in exotic syntaxes parseAnyColor can't resolve (e.g. oklab()/color()) simply degrade to the prefers-color-scheme fallback rather than being mis-read.

2. color-scheme: light dark / dark light

A dual color-scheme advertises support for both schemes; the active one is chosen by the OS preference (and reflected in the actual paint), not by token order. The previous code returned the first token (light), mis-detecting dark-OS visitors.

Fix: when both schemes are listed, defer to luminance / prefers-color-scheme instead of guessing. Single-value color-scheme still forces that theme.

3. Theme markers on <body>

Detection only inspected <html>, missing apps (and some Bootstrap/MUI setups) that put the theme class / data-* attribute on <body>.

Fix: inspect both <html> and <body> for class, theme attributes, and presence attributes. The mutation observer already watches both roots, so changes still re-trigger detection.

Structure

detectTheme() is expressed as a small firstThemeFromRoots(roots, classify) walk, making the per-concern root precedence explicit data: markers and color-scheme are read root-first, the painted background body-first (frameworks usually paint the page background on <body> while declaring the theme on <html>).

Tests

New e2e/app-theme-detection.spec.ts covers all cases:

  • oklch light/dark backgrounds under opposing OS preferences
  • color-scheme: light dark / dark light deferring to the OS preference
  • single-value color-scheme: dark forcing dark against a light OS
  • a theme marker set on <body>

Each was confirmed to fail against the pre-fix logic and pass with the fix. The existing theme-customization and edit-panel-color suites stay green (43/43 together). pnpm build, pnpm typecheck, pnpm lint, and pnpm format are all clean.

Includes a patch changeset for react-grab.

Code-quality pass

A thermo-nuclear review flagged that an earlier revision introduced a second cached-canvas color resolver duplicating parse-any-color.ts. That utility was removed in favor of the canonical parser, and the repeated html ?? body branches were collapsed into the firstThemeFromRoots walk — net deletion of a file plus the duplicate type/canvas.

Open in Web Open in Cursor 

Summary by cubic

Hardens app theme detection in react-grab so the overlay picks the correct inverse theme on pages using modern CSS colors, dual color-scheme, or body-level theme markers. Works regardless of the user’s OS preference.

  • Bug Fixes

    • Correctly reads background luminance for oklch()/oklab()/color() values; falls back from body to html when body is transparent.
    • Treats dual color-scheme (light dark / dark light) as OS-decided; single-value still forces that theme.
    • Detects theme markers on <body> as well as <html>.
    • Adds e2e coverage for oklch backgrounds, dual/single color-scheme, and body-level markers.
  • Refactors

    • Reuses the canonical parseAnyColor + parseHexChannels stack and removes the duplicate color parser.
    • Consolidates root checks into a firstThemeFromRoots(...) walk to make root-first vs body-first precedence explicit.

Written for commit 35ced63. Summary will update on new commits.

Review in cubic

Browsers serialize getComputedStyle().backgroundColor in the authored
color space (oklch/oklab/color()) instead of always normalizing to rgb().
The rgb-only luminance parser therefore failed on modern theming setups
and fell back to prefers-color-scheme, mis-detecting forced-light pages as
dark for dark-OS visitors. Normalize background colors through a canvas
parser and fall back to the document element when the body is transparent.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-grab-storybook Ready Ready Preview, Comment Jun 17, 2026 2:01am
react-grab-website Ready Ready Preview, Comment Jun 17, 2026 2:01am

@pkg-pr-new

pkg-pr-new Bot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@react-grab/cli@476
npm i https://pkg.pr.new/grab@476
npm i https://pkg.pr.new/react-grab@476

commit: 35ced63

@aidenybai aidenybai marked this pull request as ready for review June 17, 2026 01:44

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

…kers

- color-scheme: light dark/dark light advertises both schemes; the active
  one follows the OS preference, not token order. Defer to luminance /
  prefers-color-scheme instead of always returning the first token, which
  mis-detected dark-OS visitors on dual-scheme sites.
- Inspect <body> as well as <html> for theme markers (class, data-theme/
  data-bs-theme/etc., presence attributes) for apps that theme the body.
- Add e2e coverage for dual/single color-scheme and body-level markers.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot changed the title fix(react-grab): detect app theme from oklch/oklab backgrounds fix(react-grab): harden app theme detection (oklch backgrounds, color-scheme, body markers) Jun 17, 2026
Code-quality follow-up from a thermo-nuclear review:

- Delete parse-css-color.ts (a second cached-canvas singleton + duplicate
  rgba type). Reuse the existing parseAnyColor + parseHexChannels stack,
  which already resolves oklch (Tailwind v4's format) via exact math and
  needs no getImageData (avoids canvas-fingerprinting perturbation).
- Replace the three repeated 'html ?? body' ternaries in detectTheme with
  a firstThemeFromRoots(roots, classify) walk, surfacing the root-first vs
  body-first precedence as explicit data instead of burying the body-first
  luminance order in a separate helper.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
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.

2 participants