Skip to content

BuildNot yet confirmed elsewhere1 publisher3 min readPublished

The stroke width that never rendered: SVG attributes lose every cascade fight

A stylesheet rule beat a zoom-compensating presentation attribute for months, silently. The takeaway from one icon editor's postmortem: dynamic SVG styling belongs in custom properties.

The Engineer · Build desk

How we use AISend a correction

What happened

  • A zoom-compensating stroke width written with setAttribute never rendered, because a stylesheet rule naming the same property outranks a presentation attribute.
  • Chromium computed 0.35px from an attribute that asked for 0.05, and nothing in the toolchain raised an error for months.
  • The rewrite publishes 1 / zoom as a custom property on the layer and lets the stylesheet consume it inside calc().
  • Geometry attributes such as r and width were never affected, having no CSS property to shadow them.

Why it matters

  • constraint Any value you set from script through an attribute is one stylesheet rule away from being ignored, which rules the attribute channel out for anything that changes at runtime.
  • decision Choosing the custom-property route moves the arithmetic into the stylesheet and leaves the script publishing a single number, so there is one place to read when geometry looks wrong.
  • cost This class of fault bills in wrong fixes rather than broken builds: four rounds of thinning the wrong element before a computed-style read settled it.
  • contradiction Treating attributes as the dumb channel predicts the wrong failure; they resolve var() perfectly and lose only on priority, so the risk is override, not evaluation.

The reason this one survives months of use is that nothing in it is invalid. `setAttribute('stroke-width', '0.05')` writes a legal presentation attribute, the browser resolves it against `.handle { stroke-width: 0.35 }` from the stylesheet, and the attribute loses because a presentation attribute ranks below every ordinary declaration [1][2]. No parse error, no console warning, and any test that inspects the DOM reads back exactly the number the code wrote [4].

The arithmetic is what carries it past review. The compensation is `0.35 / zoom`, so at zoom 1 it returns 0.35, which is the number the stylesheet already holds, and the default view is correct [17]. The measurement the author reports, an attribute of 0.05 computing to `0.35px` in Chromium, is the state at zoom 7, where the handles were drawn seven times their intended width [3][16]. An error that is zero at rest and grows with interaction reads as a quirk in the zoom code, not as a dead line in the styling code.

Custom properties fix it for a structural reason rather than a stylistic one. `--px` set on the layer is an ordinary declaration, so it enters the cascade at a level the attribute cannot reach, and the stylesheet does the multiplication inside `calc()` [5]. The division of labour is the real gain: the script publishes one scalar per layer and owns no geometry at all.

The part that should correct the mental model is the finding that turned out to be a non-bug. The author had left a comment claiming `var()` is not substituted in presentation attributes; in fact an attribute and an inline style resolve `oklch(var(--band-l) var(--band-c) 47)` to the same computed colour, including on a node built detached and appended afterwards [7]. So attributes are not an inert side channel. They evaluate custom properties normally and then lose on priority, which means the failure to design against is a silent override, not a failure to compute. That comment cleared a full test suite and a deploy [8], which is roughly what a claim about a browser is worth when nobody pointed a browser at it.

The rest of the writeup has the same shape: authored values agreeing with themselves while computed values say otherwise. Chrome's own focus ring, measured at `outline-width: 2.72727px` in user units, put a blue disc around every clicked point [10], and it took four reports and four wrong fixes before a single `getComputedStyle` call named it [11]; the author's rule covered only `:focus-visible`, the keyboard path where he already draws a ring of his own [12]. The contrast test stayed green because it read the band's raw `stroke`, and a raw colour says nothing about opacity [15], while a 74% opaque band over white sat at 2.29:1, under the 3:1 that WCAG 1.4.11 asks of a graphic [13]. No single lightness cleared both themes at that opacity, so lightness became a theme token, 0.52 on light and 0.74 on dark [14]. The same measuring habit applied to twelve palettes labelled "soft" put them at 5.6:1 to 6.1:1 on a dark ground and 1.03:1 to 1.08:1 on white [18]; on white, the best of them reaches about a third of that 3:1 [19].

What to watch

  • Whether other engines compute the same 0.35px from a 0.05 stroke-width attribute; the measurement reported here is Chromium only.
  • Any future CSS property that names an SVG geometry attribute would push r and width into the same losing position as stroke-width.
  • Whether the compositing contrast check holds up when a third theme or a different band opacity is added, given no single lightness cleared two themes at 74%.

Clarity's read

What the record supports and how the coverage leans. The claims behind it follow.

Reality

Evidence52
Adoption12
Hype gap+12
Incentives58
Confidence46
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [1]

    The author scaled handle stroke widths by 1 / zoom and wrote the result as a presentation attribute, handle.setAttribute('stroke-width', String(0.35 / zoom)); the value was never used.

    ReportedSupportedView cited source
  2. [2]

    A stylesheet rule of .handle { stroke-width: 0.35 } outranks the presentation attribute, because a presentation attribute sits at the very bottom of the CSS cascade.

    ReportedSupportedView cited source
  3. [3]

    Measured in Chromium, a stroke-width attribute of 0.05 computed as 0.35px.

    ReportedSupportedView cited source

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · August 22, 2026

    Four things SVG and CSS did that I did not expect

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Topics

  • Browser Default UI BehaviourFollow
  • SVG Styling and RenderingFollow
  • Front-end Debugging PracticeFollow
  • Accessibility Contrast RequirementsFollow
  • CSS Cascade and SpecificityFollow

Entities

Loading related stories