Skip to content

feat(react-grab): render overlay canvas in HDR on capable displays#481

Open
aidenybai wants to merge 5 commits into
mainfrom
cursor/hdr-overlay-canvas-aa6e
Open

feat(react-grab): render overlay canvas in HDR on capable displays#481
aidenybai wants to merge 5 commits into
mainfrom
cursor/hdr-overlay-canvas-aa6e

Conversation

@aidenybai

@aidenybai aidenybai commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What

Renders the React Grab overlay highlights (selection, drag, grabbed, labels) in HDR on capable displays/browsers, so the pink accent glows brighter-than-SDR-white — without dimming the rest of the page.

The problem with naive HDR canvas

A first attempt put the full-screen overlay canvas into extended-range HDR mode. On HDR displays this dimmed the entire app: an extended canvas with no headroom limit makes the OS compositor reserve the display's full HDR headroom, which it does by mapping SDR reference white down — so all SDR content darkens while a subtle accent barely exceeds the lowered white. Net result: everything dims and the highlight doesn't even read as HDR.

The fix: constrain the headroom

Per the W3C CSS Color HDR spec, dynamic-range-limit applies to canvas elements, and for an "extended" canvas it caps the HDR headroom the browser requests. Setting dynamic-range-limit: constrained — the spec value meant for "a mix of SDR and HDR content [that] can be comfortably viewed together" — lets the highlight exceed reference white while the compositor reserves only a sliver of headroom, so surrounding SDR content stays put. Shipped in Chromium 136+.

How

  • utils/supports-hdr.ts (new): cached check that enables HDR only when all of these hold — (dynamic-range: high) matches, a probe 2D canvas backs float16, configureHighDynamicRange exists, and CSS.supports("dynamic-range-limit", "constrained"). That last gate is the safety net: a browser that can enter extended mode but can't constrain it falls back to SDR instead of regressing to page-wide dimming. Also carries the ambient type augmentations for the experimental HDR canvas API (not in the TS DOM lib yet), in a real module so they reach every importer.
  • components/overlay-canvas.tsx: when HDR is supported, the main canvas and offscreen layers use { colorType: "float16" }; the visible canvas calls configureHighDynamicRange({ mode: "extended" }) and sets dynamic-range-limit: constrained. Falls back to the existing unorm8 / SDR path otherwise.
  • utils/overlay-color.ts: overlayColor(alpha, extendedRange) scales the Display P3 channels past SDR white (, clamped down by the constrained headroom) only when extendedRange is set — opt-in for the four canvas highlight colors. Plain-CSS consumers like the frozen-glow box-shadow stay SDR-correct (extended-range channels in a normal CSS context would gamut-clamp into a distorted hue).

Notes

  • Additive and conservatively feature-detected — non-HDR/older browsers keep the exact current SDR behavior.
  • HDR canvas is experimental; the dynamic-range-limit gate ensures we never trade page-wide dimming for a brighter accent.
  • Needs visual confirmation on a real HDR display (can't be verified in CI — there's no WPT harness for HDR/wide-gamut rendering yet).

Verification

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm format
Open in Web Open in Cursor 

Summary by cubic

Render the overlay canvas in HDR in react-grab on capable browsers, using full headroom (dynamic-range-limit: no-limit) so the pink highlight blows out and the SDR page intentionally dims.
HDR is gated on (dynamic-range: high), float16 2D + configureHighDynamicRange; the 4× P3 brightness boost only applies to the HDR canvas layers, while CSS effects and unsupported browsers stay SDR.

Written for commit 42d8c73. Summary will update on new commits.

Review in cubic

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

vercel Bot commented Jun 20, 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 22, 2026 1:47am
react-grab-website Ready Ready Preview, Comment Jun 22, 2026 1:47am

@pkg-pr-new

pkg-pr-new Bot commented Jun 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 42d8c73

- Gate the P3 brightness boost behind an opt-in extendedRange flag so only the
  float16 canvas layers get past-SDR-white channels; the frozen-glow box-shadow
  (a DOM consumer that gamut-clamps) stays SDR-correct on HDR displays.
- Require configureHighDynamicRange in the HDR probe so supportsHdr() fully
  reflects the canvas capability the color boost assumes.
- Drop the redundant inner try/catch and round P3 channel output.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
A persistent full-viewport canvas in extended HDR mode forces the OS
compositor to reserve HDR headroom, which dims all surrounding SDR page
content while the subtle highlight accent isn't bright enough to read as
HDR. This is inherent compositor behavior for full-screen HDR surfaces,
not a fixable bug, so revert to the SDR (wide-gamut P3) overlay.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The previous attempt dimmed the whole page: an extended-range canvas with
no headroom limit makes the compositor reserve the display's full HDR
headroom, darkening surrounding SDR content while the subtle accent barely
exceeds the lowered reference white.

Cap the headroom with CSS `dynamic-range-limit: constrained` (the spec
value for SDR+HDR coexistence) so the highlight glows brighter-than-white
without dimming the rest of the page. Gate HDR on `dynamic-range-limit`
support so browsers that can enter extended mode but cannot constrain it
fall back to SDR instead of regressing to page-wide dimming. The boost
stays opt-in (extendedRange) so plain-CSS consumers like the frozen-glow
box-shadow remain SDR-correct.

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

Switch from constrained to no-limit headroom: the highlight now glows far
brighter than SDR white (4x) and the compositor dims the surrounding SDR
page, which is the intended dramatic HDR effect. no-limit also overrides
any inherited dynamic-range-limit from the host page. Drop the
dynamic-range-limit support gate from detection since we no longer
constrain.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 42d8c73. Configure here.

// the intended dramatic effect. `no-limit` also overrides any inherited
// dynamic-range-limit from the host page.
canvasRef.configureHighDynamicRange?.({ mode: "extended" });
canvasRef.style.setProperty("dynamic-range-limit", "no-limit");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unconstrained HDR dims entire page

High Severity

The PR describes avoiding page-wide SDR dimming by gating HDR on CSS.supports("dynamic-range-limit", "constrained") and setting dynamic-range-limit: constrained on the overlay canvas. The change turns on extended HDR with no-limit and never checks constrained support, matching the naive HDR path the PR says regresses UX by dimming the whole app while the accent barely reads as HDR.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 42d8c73. Configure here.

@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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/utils/overlay-color.ts">

<violation number="1" location="packages/react-grab/src/utils/overlay-color.ts:15">
P1: HDR_BRIGHTNESS_MULTIPLIER=4 + dynamic-range-limit:no-limit produces page-dimming blowout, contradicting the PR's constraint-based design.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

const SRGB_COMPONENTS = "210, 57, 192";
const P3_COMPONENTS = "0.84 0.19 0.78";
const P3_BASE_COMPONENTS = [0.84, 0.19, 0.78] as const;
const HDR_BRIGHTNESS_MULTIPLIER = 4;

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.

P1: HDR_BRIGHTNESS_MULTIPLIER=4 + dynamic-range-limit:no-limit produces page-dimming blowout, contradicting the PR's constraint-based design.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/utils/overlay-color.ts, line 15:

<comment>HDR_BRIGHTNESS_MULTIPLIER=4 + dynamic-range-limit:no-limit produces page-dimming blowout, contradicting the PR's constraint-based design.</comment>

<file context>
@@ -4,15 +4,15 @@ import { supportsHdr } from "./supports-hdr.js";
 const SRGB_COMPONENTS = "210, 57, 192";
 const P3_BASE_COMPONENTS = [0.84, 0.19, 0.78] as const;
-const HDR_BRIGHTNESS_MULTIPLIER = 2;
+const HDR_BRIGHTNESS_MULTIPLIER = 4;
 
 export const overlayColor = (alpha: number, extendedRange = false): string => {
</file context>

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