Description
CanvasTextRenderer rasterizes all text with a hardcoded context.fillStyle = 'white' (CanvasTextRenderer.ts:57 equivalent). Emoji glyphs bypass fillStyle and render with their browser-native color fonts, producing a canvas texture with white text pixels + natively colored emoji pixels. The text node is then drawn through the standard GPU shader, which multiplies the texture by the node's color prop (propagated to colorTl/Tr/Bl/Br) as a tint.
That works fine when node.color is near-white — white × near-white = near-white for text glyphs, and emoji × near-white barely changes. It breaks visibly when node.color is dark: text glyphs tint to the desired color, but emoji pixels get multiplied down to near-black silhouettes, losing all their native color information.
The practical trigger is any multi-theme app — in dark theme text color is typically near-white and emoji look correct, but in light theme text color is near-black and every color emoji flattens to a black blob.
Reproduction URL
Minimal scenario (no playground; <Text> with color + emoji suffices):
// Dark-theme path — looks correct, near-native emoji color
<Text color={0xf1f5f9ff}>🔥 hello</Text>
// Light-theme path — emoji renders as a near-black silhouette
<Text color={0x0f172aff}>🔥 hello</Text>
Reproduction steps
1. In any @solidtv/solid app using CanvasTextRenderer, render a <Text color={...}> containing a color emoji
2. Set color to a near-white value (e.g. 0xf1f5f9ff) — emoji appears roughly natural (slight dimming)
3. Set color to a near-black value (e.g. 0x0f172aff) — emoji flattens to a near-black silhouette
Screenshots
Browsers
Browser version
Chrome 140+ (dev). Also reproducible on WebOS Chromium builds.
OS
Additional context (root cause + options we considered)
Two moving parts:
1. CanvasTextRenderer hardcodes fillStyle = 'white'. Since emoji ignore fillStyle, the canvas texture ends up mixing "white glyphs for text" with "native-color pixels for emoji". All text coloring then has to come from the shader tint.
2. The shader tint for a text node is the same color prop users set to color their text. There's no way to say "tint text to X but leave emoji alone".
Architectural footnote we hit while patching locally: CoreNode's constructor does this.props = {} and field-copies the incoming props. CoreTextNode.textProps keeps a reference to the original incoming props object, so after the first color setter runs, this.textProps.color and this.props.color diverge — CanvasTextRenderer reads the stale one. That's a separate pre-existing issue but any fix to the emoji problem needs to deal with it (or the rasterizer reads the wrong color).
We ruled out a client-side workaround (splitting emoji runs out of Text into separate untinted nodes) because it breaks inline flow and requires threading through every <Text> site.
Possible fix directions — deferring to maintainer's preference on API shape before proposing a PR:
- (a) Bake node.color into CanvasTextRenderer's fillStyle and default the shader tint to white for canvas text nodes. Fixes emoji cleanly; text rendering is mathematically equivalent (white × color vs. color × white). Behavior change: gradient-tinted text (non-uniform colorTl/Tr/Bl/Br) would lose the gradient, since corners are forced white.
- (b) Add a separate opt-in prop (e.g.
textColor, or a preserveEmojiColor flag) that switches to (a)'s behavior per-node. Opt-in, no existing behavior changes.
- (c) Leave as-is; document the tradeoff and let apps handle emoji text separately.
Option (a) matches what apps almost always want; option (b) is the safe incremental path.
Description
CanvasTextRendererrasterizes all text with a hardcodedcontext.fillStyle = 'white'(CanvasTextRenderer.ts:57equivalent). Emoji glyphs bypassfillStyleand render with their browser-native color fonts, producing a canvas texture with white text pixels + natively colored emoji pixels. The text node is then drawn through the standard GPU shader, which multiplies the texture by the node'scolorprop (propagated tocolorTl/Tr/Bl/Br) as a tint.That works fine when
node.coloris near-white — white × near-white = near-white for text glyphs, and emoji × near-white barely changes. It breaks visibly whennode.coloris dark: text glyphs tint to the desired color, but emoji pixels get multiplied down to near-black silhouettes, losing all their native color information.The practical trigger is any multi-theme app — in dark theme text color is typically near-white and emoji look correct, but in light theme text color is near-black and every color emoji flattens to a black blob.
Reproduction URL
Minimal scenario (no playground;
<Text>with color + emoji suffices):Reproduction steps
Screenshots
Browsers
Browser version
OS
Additional context (root cause + options we considered)
Two moving parts:
1.
CanvasTextRendererhardcodesfillStyle = 'white'. Since emoji ignore fillStyle, the canvas texture ends up mixing "white glyphs for text" with "native-color pixels for emoji". All text coloring then has to come from the shader tint.2. The shader tint for a text node is the same
colorprop users set to color their text. There's no way to say "tint text to X but leave emoji alone".Architectural footnote we hit while patching locally:
CoreNode's constructor doesthis.props = {}and field-copies the incoming props.CoreTextNode.textPropskeeps a reference to the original incoming props object, so after the firstcolorsetter runs,this.textProps.colorandthis.props.colordiverge —CanvasTextRendererreads the stale one. That's a separate pre-existing issue but any fix to the emoji problem needs to deal with it (or the rasterizer reads the wrong color).We ruled out a client-side workaround (splitting emoji runs out of Text into separate untinted nodes) because it breaks inline flow and requires threading through every
<Text>site.Possible fix directions — deferring to maintainer's preference on API shape before proposing a PR:
textColor, or apreserveEmojiColorflag) that switches to (a)'s behavior per-node. Opt-in, no existing behavior changes.Option (a) matches what apps almost always want; option (b) is the safe incremental path.