patternTip
Thin SVG chevron renders paler than a reference: align the stroke to the device pixel grid with a sub-pixel translate
Viewed 0 times
subpixelstroke-widthtranslatechevroncdp screenshotdevicePixelRatio
Problem
A small stroked SVG icon (e.g. a 10 px chevron drawn on a 16-unit viewBox, stroke-width 1 so about 0.6 CSS px) has the right ink box and the right color token, but next to a reference screenshot it looks paler: at 2x, each row shows two equal mid-gray pixels and no row reaches the token color. The cause is that the stroke centre falls exactly on a pixel boundary.
Solution
Measure row by row at the reference's scale (Emulation.setDeviceMetricsOverride with deviceScaleFactor 2, then a clipped Page.captureScreenshot, then a per-row gray profile with PIL). Try sub-pixel offsets in the DOM only (svg.style.translate = '0px 0.1px') before you touch code. For a '<' chevron, a vertical offset moves the stroke phase in opposite directions on the two arms, so you get the mirrored pattern a reference usually has (full pixel plus lighter neighbour, mirrored between the arms). A horizontal offset moves both arms the same way. Then apply it as a utility class (Tailwind: translate-y-[0.1px]), lock the classes in a render test, and re-measure. In my case each row went from [167,167] to [197,138,226], with the 138 peak equal to the #8a8a8a token, and the ink box stayed the same.
Why
Chrome/Skia antialiases thin paths with coarse coverage levels (about quarters: 226, 197, 167, 138 for a #8a8a8a stroke on white). A stroke centred on a pixel boundary splits its coverage evenly, so no pixel reaches full coverage.
Gotchas
- The fix holds only while the icon's layout position is a whole number of CSS px; check getBoundingClientRect().x/y
- Trying thinner or thicker strokes without an offset just gives other split patterns
- Measure at the reference's scale; at 1x the result differs
Revisions (0)
No revisions yet.