An SVG marker drawn inside a non-uniformly stretched viewBox renders as an ellipse, not a circle
preserveAspectRatio="none" is the standard way to make an SVG's viewBox stretch to fill a container of arbitrary aspect ratio rather than letterboxing, and a chart component that has to fit both a wide-short card and a taller panel needs exactly that. But stretching to fill an arbitrary box means the viewBox's x-axis and y-axis are scaled by different factors whenever the container's aspect ratio differs from the viewBox's own — and any geometric primitive drawn in that coordinate space that is meant to look uniform (a circle marking a data point, a square icon) is stretched by those same two different factors, becoming an ellipse or rectangle whose eccentricity tracks the container's current width-to-height ratio rather than being a fixed, correctable distortion. The natural first fix is adding vectorEffect="non-scaling-stroke" to the shape, because that attribute is the usual answer to "this shape looks wrong under a transform" — but it only cancels scaling of stroke *width*, not the geometry of the shape's path, so a circle with that attribute is still an ellipse, just with an even-width outline traced around an elliptical path.
Resurfaces when
- building a chart or diagram component meant to render at several different aspect ratios from one implementation
- setting preserveAspectRatio="none" on an SVG viewBox
- adding a data-point marker or icon to an existing line/area chart
- a circle or icon inside an SVG looks stretched or egg-shaped at some container sizes but not others
- a shape's distortion in a chart changes when the component is resized or placed in a different layout
- reaching for vectorEffect to fix a shape that looks wrong under a transform
The lesson’s retrieval contract, weighted three times heavier than its body. A lesson that states when it applies doesn’t need a semantic search to find it.
The lesson
preserveAspectRatio="none" is the right tool whenever one SVG component has to fill containers
of different, unpredictable aspect ratios instead of preserving its own — a chart that must sit in
both a wide, short stat card and a tall panel, drawn once. What it costs is easy to miss: filling
an arbitrary box by stretching means the two axes of the viewBox's coordinate space are no longer
scaled by the same factor. A path drawn from percentages still looks correct, because a line has
no shape of its own to distort — but anything in that space that is meant to read as uniform, most
commonly a circle marking a data point, is stretched by those two different factors and comes out
as an ellipse. The amount of distortion is not fixed; it tracks whatever the container's current
aspect ratio happens to be, so the same component can look fine in one placement and visibly wrong
in another with no change to the component itself.
The trap inside the trap is the fix that looks right and is not. vectorEffect="non-scaling-stroke"
exists precisely to stop a shape's rendering from moving when a transform is applied to it, so it
is the reflexive reach when a shape "looks wrong under a transform." It does stop the stroke from
scaling — the outline stays an even width regardless of the surrounding transform. It does nothing
for the geometry of the shape itself: the circle's path is still defined in the distorted
coordinate space, so it is still traced out as an ellipse, only now outlined evenly instead of with
a stroke that also thins and thickens around its own circumference. Applying it and seeing the
shape still wrong reads as "the fix didn't work," when the more useful read is "this was never a
stroke problem."
The actual fix is to stop asking the distorted coordinate space to hold anything that needs to stay
uniform. A marker's position can be computed as the same percentage used to place it on the path,
and rendered as a separately-positioned element in the box's own screen space — an HTML element
absolutely positioned by percentage, or an SVG layer of its own that is not subject to the parent's
preserveAspectRatio="none" — where a circle is round regardless of what aspect ratio the chart
underneath it is being stretched to fit.
The failure that taught it
A small SVG line-chart component was built to render inside two different layouts from one
implementation: a compact stat card and a taller detail panel. To make one component fill either
container without letterboxing, its viewBox was set to stretch freely — preserveAspectRatio="none"
— which is the standard, correct choice for that requirement. Data points were marked with <circle>
elements positioned in the same viewBox coordinate space as the line itself.
At one container width the markers rendered as visibly fat ellipses rather than circles. The first
instinct was that this was a rendering artifact — a stroke-scaling issue — so
vectorEffect="non-scaling-stroke" was added to the circles. The shape did not change: the markers
were still ellipses, because the attribute addresses stroke width, and the problem was the shape's
geometry, which is defined by the coordinate space it is drawn in, not by how its outline is
stroked. Only checking what preserveAspectRatio="none" actually does to a viewBox's two axes —
rather than guessing at the transform stack — surfaced the real mechanism, and the actual fix was
to take the markers out of that coordinate space entirely and place them as absolutely-positioned
elements in the container's own screen-space percentage coordinates, reusing the same percentages
already used to build the path.
How to apply it
- When an SVG viewBox is set to
preserveAspectRatio="none"(or any value other than the defaultxMidYMid meet), treat every geometric primitive drawn inside it as suspect if it is meant to look uniform — a circle, a square icon, anything whose recognizability depends on unequal axes not being stretched unequally. - Do not reach for
vectorEffect="non-scaling-stroke"as the fix for a shape that looks distorted under a non-uniform viewBox scale. It only holds stroke width constant; it cannot hold a shape's geometry constant. If the distortion is in the shape's outline itself rather than just its stroke weight, this attribute will not touch it. - The general fix: keep anything that must stay uniform out of the distorted coordinate space.
Compute its position in the same units the distorted content uses (often a percentage), then
render it as a sibling in undistorted space — an HTML element layered over the SVG, or an SVG
element that sits outside the
preserveAspectRatio="none"viewBox rather than inside it. - This generalizes past markers on a chart: any icon, avatar, or shape overlaid on an SVG diagram, map, or illustration that stretches to fill an arbitrary container inherits the same risk, and the same fix applies.
Carries a runnable check
It names a grep-able signature — the shape this failure takes on sight. Prose doesn’t prevent recurrence; executable checks do. This one runs today, against every edit, as signature-scan.
Where this claim comes from
- Lessonhigh confidence
An SVG marker drawn inside a non-uniformly stretched viewBox renders as an ellipse, not a circle
- Distillationmechanism stated
Written from the mechanism, not the incident — which is what lets it transfer to code sharing nothing with the original.
- Scar1 occurrence
One recorded failure — weaker evidence, and ranked accordingly rather than presented as settled.
- Evidencenone recorded
No source records recorded — hand-written and migrated lessons predate the pipeline that captures them.
What happened when it was used
- 11
- retrieved
- 1
- acted on
- 1
- held up
- 0
- did not hold up
Applied 1 time. Outcomes move the ranking both ways, which is what makes this improve rather than just grow.