@remotion/web-renderer: Add support for text shadows#6607
Conversation
Closes #6606 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds support for the text-shadow CSS property to the web renderer by parsing text-shadow values and applying them using the Canvas 2D shadow API.
Changes:
- Implemented
parseTextShadow()function to parse CSS text-shadow values including multiple shadows, colors, offsets, and blur radii - Modified
drawText()to apply parsed shadows before rendering text on the canvas - Added comprehensive visual regression test covering various shadow scenarios (simple, colored, multiple, hard-edge, glow)
Reviewed changes
Copilot reviewed 5 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/web-renderer/src/drawing/text/parse-text-shadow.ts | New parser to extract text-shadow properties from CSS strings |
| packages/web-renderer/src/drawing/text/draw-text.ts | Applies parsed text shadows to canvas context before drawing text |
| packages/web-renderer/src/test/fixtures/text/text-shadow.tsx | Test fixture demonstrating various text-shadow use cases |
| packages/web-renderer/src/test/text-shadow.test.tsx | Visual regression test for text-shadow rendering |
| packages/web-renderer/src/test/Root.tsx | Registers the text-shadow test composition |
Comments suppressed due to low confidence (1)
packages/web-renderer/src/drawing/text/parse-text-shadow.ts:43
- The regex pattern matches units like
em,rem, and%, butparseFloat()on line 44 will fail to convert these relative units to absolute pixel values. Text-shadow offsets and blur radius should be converted to pixels based on the element's computed font size for relative units.
const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || [];
There was a problem hiding this comment.
Clean PR that follows the established parseBoxShadow pattern closely. The Canvas 2D shadow approach is sound for opaque text. Two observations below — neither is blocking.
| for (let i = textShadows.length - 1; i >= 0; i--) { | ||
| const shadow = textShadows[i]; | ||
| contextToDraw.shadowColor = shadow.color; | ||
| contextToDraw.shadowBlur = shadow.blurRadius; | ||
| contextToDraw.shadowOffsetX = shadow.offsetX; | ||
| contextToDraw.shadowOffsetY = shadow.offsetY; | ||
| contextToDraw.fillText(token.text, x, y); | ||
| } |
There was a problem hiding this comment.
Heads up: each shadow pass calls fillText, which draws both the shadow and the text body. So for N shadows the text body is composited N+1 times. For opaque text this is visually correct, but for semi-transparent webkitTextFillColor the text body would appear more opaque than CSS text-shadow renders it.
The box-shadow implementation works around this with destination-out compositing on an offscreen canvas. A similar technique could be applied here for pixel-perfect fidelity, but it's a reasonable trade-off for v1.
- Extract shared `parseShadowValues` from box-shadow and text-shadow parsers - Update limitations.mdx to mark text-shadow as supported - Update web-renderer-test skill to mention limitations.mdx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Summary
text-shadowCSS property support to the web rendererCloses #6606
Test plan
text-shadow.test.tsxpasses on all 3 browsers (chromium, firefox, webkit)bun run buildpassesbun run stylecheckpasses🤖 Generated with Claude Code