SVG viewBox and preserveAspectRatio, Explained With the Math
The viewBox attribute, written as four numbers (min-x, min-y, width, height), sets the rectangle of drawing coordinates you want to show. The width and height attributes set the size of the box on the page. The browser maps the first rectangle onto the second with one scale and one translate, and preserveAspectRatio picks both. The default, xMidYMid meet, fits the whole drawing and centres it.
A concrete case makes that mapping visible. Put viewBox="0 0 200 100" inside a 300 by 300 pixel box. The drawing is twice as wide as it is tall, the box is square, so the browser cannot use one scale for both axes and fill it. Under the default it scales by 1.5 (300 divided by 200), which makes the drawing 300 by 150 pixels, then moves it down 75 pixels so the empty space splits evenly above and below. Every other value of preserveAspectRatio changes only those two numbers, the scale and the shift, and the tables below give them for all 19 values in two box shapes.
Two Rectangles, Not One
Most viewBox confusion comes from treating width, height and viewBox as three ways of saying the same thing. They describe two different rectangles.
The viewBox lives in the drawing's own coordinate system, which the SVG 2 specification calls user space. MDN describes its value as "a list of four numbers separated by whitespace and/or a comma: min-x, min-y, width, and height." The first two numbers are the top-left corner of the area you want to show. The last two are how much of the drawing, in user units, that area covers. Nothing in a viewBox is in pixels. An icon drawn on a 24-unit grid has viewBox="0 0 24 24" whether it is shown at 16 pixels or 400.
The viewport is the box on the page. For the outermost svg element, the SVG 2 specification says the initial viewport's width "must be the value of the width presentation attribute," and the same for height, and a unitless number is "assumed to be equivalent to the same number of px units." MDN's svg element reference describes width as "the displayed width of the rectangular viewport (not the width of its coordinate system)."
Seen that way, the attributes stop overlapping. width="48" height="48" viewBox="0 0 24 24" means: show user units 0 to 24 on both axes, inside a 48 pixel square. The scale is 2. Change width and height to 96 and the scale is 4, with the drawing unchanged. Change the viewBox to "0 0 12 12" and you zoom in on the top-left quarter of the icon, still in a 48 pixel box.
A few rules about the viewBox itself, taken from the specification and MDN:
- It applies to five elements: svg, symbol, marker, pattern and view, per MDN's attribute reference. Of those, the SVG 2 coordinate chapter says only svg and a symbol instanced by a use element establish a new viewport.
- A width or height of zero "disables rendering of the element," per SVG 2, while a negative width or height "is an error" that makes the viewBox attribute invalid, so the spec ignores the viewBox rather than blanking the element. MDN's note groups the two cases and says values "lower or equal to 0 disable rendering." A zero size blanks the element under both, so a viewBox like "0 0 0 24" from a broken export shows up as a blank icon, not a distorted one; a negative size may instead render the drawing unscaled, depending on how the browser follows the spec.
- The separators can be spaces, commas, or both. "0,0,24,24" and "0 0 24 24" are the same value.
- min-x and min-y can be any number, including negatives. They move the window over the drawing. They do not move the drawing on the page.
That last point is worth a short example. viewBox="-100 -50 200 100" shows the same amount of drawing as "0 0 200 100", but puts user-space point (0, 0) in the middle of the window instead of the corner. Some designers draw symmetrical marks around the origin for exactly this reason: the coordinate of the centre is always 0, 0. In a 300 by 300 box with the default alignment, that point lands at pixel (150, 150), the centre of the box. The arithmetic for that is in the next section.
The Spec's 14 Steps, as Arithmetic
SVG 2 defines the mapping in a short algorithm, "Computing the equivalent transform of an SVG viewport." It reads as code, and it is the most direct way to understand every value of preserveAspectRatio. Here it is in order, using the spec's own names:
- vb-x, vb-y, vb-width, vb-height are the four viewBox numbers.
- e-x, e-y, e-width, e-height are the position and size of the viewport.
- align is the first keyword of preserveAspectRatio, or xMidYMid if the attribute is not set.
- meetOrSlice is the second keyword, or meet if it is missing.
- scale-x starts as e-width / vb-width.
- scale-y starts as e-height / vb-height.
- If align is not none and the mode is meet, the larger scale is set to the smaller one.
- If align is not none and the mode is slice, the smaller scale is set to the larger one.
- translate-x starts as e-x minus vb-x times scale-x.
- translate-y starts as e-y minus vb-y times scale-y.
- If align contains xMid, add (e-width minus vb-width times scale-x) / 2 to translate-x.
- If align contains xMax, add (e-width minus vb-width times scale-x) to translate-x.
- If align contains YMid, add (e-height minus vb-height times scale-y) / 2 to translate-y.
- If align contains YMax, add (e-height minus vb-height times scale-y) to translate-y.
The result is the transform translate(translate-x, translate-y) scale(scale-x, scale-y). Every point (x, y) in the drawing lands at pixel (translate-x + x times scale-x, translate-y + y times scale-y).
Three things in that list explain most behaviour people find surprising.
Steps 7 and 8 are the only place meet and slice differ. Meet takes the smaller of the two scales, so the whole viewBox fits and space is left over on one axis. Slice takes the larger, so the viewport is covered and some of the viewBox is pushed outside it. The specification's own wording: meet scales so "the entire viewBox is visible within the SVG viewport," slice so "the entire SVG viewport is covered by the viewBox."
Steps 9 and 10 are where min-x and min-y matter. A non-zero viewBox origin is subtracted after scaling, which is why moving the origin shifts what you see without changing the zoom.
Steps 11 to 14 only ever add the leftover space, (viewport size minus scaled viewBox size), in full, in half, or not at all. Min adds nothing. Mid adds half. Max adds all of it. With meet, the leftover is zero or positive. With slice, it is zero or negative, so Mid and Max move the drawing left or up, cropping from the other side.
The negative origin example, worked
Take viewBox="-100 -50 200 100" in a 300 by 300 viewport at position (0, 0), with no preserveAspectRatio, so xMidYMid meet.
- Step 5: scale-x = 300 / 200 = 1.5. Step 6: scale-y = 300 / 100 = 3.
- Step 7: meet, so the larger (3) becomes the smaller. Both scales are 1.5.
- Step 9: translate-x = 0 minus (-100 times 1.5) = 150. Step 10: translate-y = 0 minus (-50 times 1.5) = 75.
- Step 11: xMid adds (300 minus 200 times 1.5) / 2 = 0. translate-x stays 150.
- Step 13: YMid adds (300 minus 100 times 1.5) / 2 = 75. translate-y becomes 150.
User point (0, 0) lands at 150 + 0, 150 + 0, the centre of the box. The left edge of the viewBox, x = -100, lands at 150 minus 150 = 0, the left edge of the viewport, as it should with no horizontal space to spare.
Table One: viewBox 0 0 200 100 in a 300 by 300 Box
This is the "logo in a square slot" case: a wide 2:1 drawing in a square viewport, which is taller relative to its width than the drawing is. Each row applies the 14 steps above. The last column says, in pixels or in user units, what the reader sees.
| preserveAspectRatio | Scale | Translate (x, y) | Drawing lands at (px) | Result |
|---|---|---|---|---|
| xMinYMin meet | 1.5 | (0, 0) | 0,0 to 300,150 | Top of box; 150 px empty below |
| xMidYMin meet | 1.5 | (0, 0) | 0,0 to 300,150 | Same as above |
| xMaxYMin meet | 1.5 | (0, 0) | 0,0 to 300,150 | Same as above |
| xMinYMid meet | 1.5 | (0, 75) | 0,75 to 300,225 | Centred; 75 px empty above and below |
| xMidYMid meet (default) | 1.5 | (0, 75) | 0,75 to 300,225 | Same as above |
| xMaxYMid meet | 1.5 | (0, 75) | 0,75 to 300,225 | Same as above |
| xMinYMax meet | 1.5 | (0, 150) | 0,150 to 300,300 | Bottom of box; 150 px empty above |
| xMidYMax meet | 1.5 | (0, 150) | 0,150 to 300,300 | Same as above |
| xMaxYMax meet | 1.5 | (0, 150) | 0,150 to 300,300 | Same as above |
| xMinYMin slice | 3 | (0, 0) | 0,0 to 600,300 | User x 0 to 100 visible; right half cropped |
| xMinYMid slice | 3 | (0, 0) | 0,0 to 600,300 | Same as above |
| xMinYMax slice | 3 | (0, 0) | 0,0 to 600,300 | Same as above |
| xMidYMin slice | 3 | (-150, 0) | -150,0 to 450,300 | User x 50 to 150 visible; a quarter cropped each side |
| xMidYMid slice | 3 | (-150, 0) | -150,0 to 450,300 | Same as above |
| xMidYMax slice | 3 | (-150, 0) | -150,0 to 450,300 | Same as above |
| xMaxYMin slice | 3 | (-300, 0) | -300,0 to 300,300 | User x 100 to 200 visible; left half cropped |
| xMaxYMid slice | 3 | (-300, 0) | -300,0 to 300,300 | Same as above |
| xMaxYMax slice | 3 | (-300, 0) | -300,0 to 300,300 | Same as above |
| none | 1.5 across, 3 down | (0, 0) | 0,0 to 300,300 | Fills the box; everything drawn twice as tall as it is wide relative to the original |
The pattern in the rows is not a coincidence. With meet, only the Y half of the keyword moves anything, because the leftover space is all vertical. With slice, only the X half moves anything, because the overflow is all horizontal. Eighteen keyword combinations produce three different results for each mode.
Show the data as a table
| Mode | Scale | Translate (x, y) | Drawing size in px | What is lost or left over |
|---|---|---|---|---|
| xMidYMid meet | 1.5 | (0, 75) | 300 by 150 | 75 px empty above and below |
| xMidYMid slice | 3 | (-150, 0) | 600 by 300 | 150 px cropped left and right; user x 50 to 150 visible |
Table Two: the Same viewBox in a 400 by 100 Box
Now the box is a banner, wider relative to its height than the drawing (4:1 against 2:1). The axes swap roles.
| preserveAspectRatio | Scale | Translate (x, y) | Drawing lands at (px) | Result |
|---|---|---|---|---|
| xMinYMin meet | 1 | (0, 0) | 0,0 to 200,100 | Left of box; 200 px empty on the right |
| xMinYMid meet | 1 | (0, 0) | 0,0 to 200,100 | Same as above |
| xMinYMax meet | 1 | (0, 0) | 0,0 to 200,100 | Same as above |
| xMidYMin meet | 1 | (100, 0) | 100,0 to 300,100 | Centred; 100 px empty each side |
| xMidYMid meet (default) | 1 | (100, 0) | 100,0 to 300,100 | Same as above |
| xMidYMax meet | 1 | (100, 0) | 100,0 to 300,100 | Same as above |
| xMaxYMin meet | 1 | (200, 0) | 200,0 to 400,100 | Right of box; 200 px empty on the left |
| xMaxYMid meet | 1 | (200, 0) | 200,0 to 400,100 | Same as above |
| xMaxYMax meet | 1 | (200, 0) | 200,0 to 400,100 | Same as above |
| xMinYMin slice | 2 | (0, 0) | 0,0 to 400,200 | User y 0 to 50 visible; bottom half cropped |
| xMidYMin slice | 2 | (0, 0) | 0,0 to 400,200 | Same as above |
| xMaxYMin slice | 2 | (0, 0) | 0,0 to 400,200 | Same as above |
| xMinYMid slice | 2 | (0, -50) | 0,-50 to 400,150 | User y 25 to 75 visible; a quarter cropped top and bottom |
| xMidYMid slice | 2 | (0, -50) | 0,-50 to 400,150 | Same as above |
| xMaxYMid slice | 2 | (0, -50) | 0,-50 to 400,150 | Same as above |
| xMinYMax slice | 2 | (0, -100) | 0,-100 to 400,100 | User y 50 to 100 visible; top half cropped |
| xMidYMax slice | 2 | (0, -100) | 0,-100 to 400,100 | Same as above |
| xMaxYMax slice | 2 | (0, -100) | 0,-100 to 400,100 | Same as above |
| none | 2 across, 1 down | (0, 0) | 0,0 to 400,100 | Fills the banner; everything twice as wide relative to the original |
We checked both tables against a real renderer rather than trusting the arithmetic alone. Each of the 38 combinations was rendered on November 13, 2026 with librsvg 2.58.0 (rsvg-convert), once with a rectangle filling the whole viewBox and once with a small 20 by 20 unit marker at user position (120, 20), and the pixel bounds were read back from the PNG. In all 38 cases the rectangle and the marker landed on the pixels the table predicts, and in the six cases where the arithmetic puts the marker entirely outside the viewport (xMin slice in the square box, YMax slice in the banner), the rendered image had no marker pixels at all. That is one rendering engine, not a browser survey, but it is an independent implementation of the same spec algorithm.
Which Half of the Keyword Does Anything
The two tables lead to a rule that saves trial and error. Compare two ratios: the viewBox's width divided by its height, and the viewport's width divided by its height.
| Viewport compared with viewBox | meet: what moves the drawing | slice: what picks the crop | Ignored |
|---|---|---|---|
| Same ratio | Nothing; leftover space is zero | Nothing; overflow is zero | Both halves |
| Viewport relatively taller (e.g. 2:1 drawing in a square) | The Y keyword (YMin, YMid, YMax) | The X keyword (xMin, xMid, xMax) | The other half |
| Viewport relatively wider (e.g. 2:1 drawing in a 4:1 banner) | The X keyword | The Y keyword | The other half |
Put another way: meet places the drawing along the axis that has room left over, and slice chooses which part to keep along the axis that overflows. That is why "I changed xMin to xMax and nothing happened" is such a common bug report. In a box taller than the drawing, under meet, the x keyword has nothing to act on. The same markup in a wider box would respond at once.
This also matters for responsive layouts. A hero illustration in a container whose height is fixed but whose width follows the screen can switch sides of this table at a breakpoint. On a phone, the box may be relatively taller than the drawing; on a desktop, relatively wider. A value such as xMinYMid slice keeps the left part of the drawing on a narrow screen, where the overflow is horizontal, and centres it vertically on a wide screen, where the overflow is vertical. Choose both halves on purpose, because each one takes over at some screen width.
none: When Stretching Is the Point
With align set to none, steps 7 and 8 are skipped and each axis keeps its own scale. The specification is short about it: none means "do not force uniform scaling," and when align is none, "the optional meetOrSlice value is ignored." In the square box above, the scales stay 1.5 across and 3 down, so a circle drawn in the viewBox becomes an ellipse twice as tall as it is wide, relative to its original shape.
That is wrong for any icon, logo, letterform or illustration. It is right for a small set of shapes that have no proportions of their own:
- A wave or curve divider meant to span a section edge to edge at any width.
- A background gradient or texture panel that should fill a box exactly.
- A sparkline or bar fill where the vertical axis is the data and the width is whatever the layout gives.
In those cases, none avoids the empty bands meet would leave and the cropping slice would cause. Be aware that strokes are scaled too: under none, a line drawn 2 units thick becomes 3 pixels thick across and 6 pixels thick down in the square box, so a stroked divider looks heavier on its vertical parts than on its horizontal ones.
The CSS object-fit Comparison
If you already know CSS object-fit, the three behaviours map closely. CSS Images Level 3 defines contain as sizing that keeps "its natural aspect ratio while fitting within the element's content box," cover as keeping the ratio "while filling the element's entire content box," and fill as sizing the content "to fill the element's content box," with no ratio kept. Those match meet, slice and none.
| SVG preserveAspectRatio | Closest CSS | Default in that system |
|---|---|---|
| meet | object-fit: contain | SVG defaults to xMidYMid meet |
| slice | object-fit: cover | |
| none | object-fit: fill | CSS defaults to fill |
| xMid YMid alignment | object-position: 50% 50% | CSS object-position defaults to 50% 50% |
The one difference that bites is the defaults. An HTML img defaults to object-fit: fill, which stretches. An SVG's own content defaults to xMidYMid meet, which never stretches. When an SVG file is shown through an img whose width and height attributes have a different ratio from the file's viewBox, two layers each have a say: CSS object-fit on the img element, and the file's own preserveAspectRatio inside it. The reliable fix is not to work out which one wins in which browser. Make the img's width and height attributes match the viewBox ratio, so neither layer has anything to decide.
viewBox, width and height: What Each Combination Does
This is the "svg viewbox vs width height" question in table form. The intrinsic size and ratio rules come from section 8.12 of the SVG 2 coordinate chapter; the "auto means 100%" rule for the outermost svg comes from the SVG 2 geometry chapter's sizing table; the fallback sizes come from CSS 2.2 section 10.3.2.
| Markup on the svg element | Intrinsic ratio | Intrinsic size | Inline in HTML | As an image file (img or CSS background) |
|---|---|---|---|---|
| viewBox only | From the viewBox | None | width auto resolves to 100% of the container; height follows the ratio | No natural size; the page's CSS decides. CSS 2.2 suggests the containing block's width and falls back to 300 px |
| width and height only | From width / height | width by height | Fixed size; the drawing is not scaled, so CSS resizing crops or leaves space | Natural size width by height; with no viewBox, the SVG rules give no mapping to any other size, so do not rely on it scaling |
| width, height and viewBox, same ratio | width / height | width by height | Renders at that size; CSS can resize it cleanly | Natural size width by height; resizes cleanly |
| width, height and viewBox, different ratio | width / height (wins over the viewBox) | width by height | The drawing is letterboxed or cropped inside the box by preserveAspectRatio | Same |
| Neither | None | None | 100% wide, and with no ratio the height is left to CSS and fallbacks | CSS 2.2's last resort is a 300 by 150 box |
Two rows deserve a sentence each. In the "different ratio" row, the specification's intrinsic ratio algorithm checks width and height first: "If the width and height sizing properties on the svg element are both absolute values: return width / height." Only if they are not does it fall back to "viewbox.width / viewbox.height." So width="24" height="24" on a 200 by 100 viewBox makes a square image with the drawing letterboxed inside it, even when every other part of the file says 2:1.
In the "width and height only" row, preserveAspectRatio does nothing at all. The spec says that if "viewBox is not provided, then preserveAspectRatio is ignored." There is no second rectangle to fit, so there is nothing to align. This is the reason the pillar guide on SVG optimization tells you to switch off any optimizer setting that removes the viewBox: with width and height left alone, the file renders, but it can no longer scale.
Why CSS can override the attributes
In SVG 2, width and height on the svg element are presentation attributes: the specification's styling chapter lists them for svg, image, rect, use and others. Presentation attributes "contribute to the author level of the cascade, followed by all other author-level style sheets," and they "have specificity 0." In practice, any CSS rule that sets width or height on the element replaces the attribute value, even a plain .icon selector. The attribute stays useful as a default for whenever no rule applies, which is the basis of the decision rule further down.
Layout Shift: Where Missing Dimensions Cost You
Cumulative Layout Shift measures content that moves after it is painted. web.dev's guidance on reducing it starts with images: "Always include width and height size attributes on your images and video elements." It adds that browsers "add a default aspect ratio based on the element's existing width and height attributes," which Chrome's style panel shows as, for example, aspect-ratio: auto 640 / 360. The HTML Standard defines the same mapping in its rendering section: the width and height attributes "map to the aspect-ratio property (using dimension rules) on img and video elements," and on canvas.
That list does not include the svg element, and the omission is not a gap you need to fill, because the two ways of putting an SVG on a page behave differently.
SVG as an img. An img pointing at logo.svg is an ordinary image to the layout engine. Until the file arrives, the browser knows nothing about it except what the img tag says. With no width and height on the img, there is no reserved box, and when the file loads and turns out to be a 2:1 logo, everything below it moves. In a 320 pixel column with CSS width: 100%, that 2:1 logo is 160 pixels tall once it renders, and if nothing was reserved, the text below jumps 160 pixels. With width="200" height="100" on the img and the same CSS, the browser reserves 320 by 160 from the first layout and nothing moves. The attribute numbers do not need to match the displayed size, only the ratio. How to score a jump like that is worked out in the CLS worked examples guide.
Inline svg. An inline svg element is part of the HTML, so its viewBox is read when the markup is parsed, with no network request. Per the SVG 2 intrinsic sizing rule, a correctly specified viewBox gives the element its ratio, and the specification requires that "in all host contexts, the intrinsic aspect ratio, where available, must be respected when sizing the SVG viewport." The box is known on the first layout. The shift risk for inline icons is a different one: the icon is sized by a CSS rule that arrives after first paint. Without width and height attributes, an inline svg with only a viewBox resolves to 100% of its container. If the rule that sizes it to 24 pixels comes from a stylesheet loaded late, a script that adds a class, or a component that upgrades after load, the first paint shows a container-wide icon, and the page then collapses around it. A render-blocking stylesheet avoids the problem; width and height attributes avoid it whatever loads when.
SVG as a CSS background. A background image never sizes its box. The element's own CSS does. A background SVG cannot cause a shift by itself, though the element around it can if its height depends on content that loads later.
The Decision Rule for Icons and Logos
Keep the viewBox in every case. The question is only what to do with width and height.
- Used through an img tag (logos in headers, illustrations in articles): set width and height on the img, with the same ratio as the file's viewBox. This is the CLS fix and it costs a few bytes. Inside the file, keep width and height too if the file might be opened or embedded without CSS, for example in email, documentation tools or a CMS preview. They give it a natural size instead of an unpredictable fallback.
- Inline interface icon (buttons, menus, form fields): keep width and height at the design size, such as 24 by 24, matching a viewBox of 0 0 24 24. Size the icon in CSS as usual; your rule overrides the attributes because they are presentation attributes with specificity 0. The attributes only take effect when no CSS applies, which is exactly when you want a small icon rather than a container-wide one.
- Inline fluid artwork (a logo or illustration meant to fill its column): you can drop width and height and rely on the viewBox for the ratio, since auto resolves to 100% width. Alternatively keep them and add CSS width: 100% and height: auto. Pick the first if the markup will only ever live inside your own styled pages, the second if it might be copied elsewhere.
- Attributes that disagree with the viewBox: fix the attributes. Setting preserveAspectRatio="none" to make a 2:1 drawing fill a square box distorts it. A mismatch is almost always an export error, often from an artboard that was not trimmed.
- symbol and use sprites: put the viewBox on each symbol, where the drawing is, and put width and height (or a CSS size) on the svg element that holds the use reference, where the box is. A symbol without a viewBox has nothing to scale.
A short way to remember it: the viewBox belongs to the drawing and should never be removed; width and height belong to the box on the page and should be present unless CSS you control always sizes that box before first paint.
Two markup examples
An inline icon that follows the rule:
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" class="icon">
<path d="M4 12h16M12 4v16" stroke="currentColor" stroke-width="2"/>
</svg>
A logo through an img, with the ratio reserved:
<img src="/logo.svg" width="200" height="100" alt="Company name">
In the second example, the logo file's own viewBox would be "0 0 200 100" or any multiple of it. If the file's viewBox were "0 0 400 120", the img should say width="400" height="120" (or 200 by 60), not 200 by 100.
Scale Factors, Pixel Grids and Blurry Edges
The viewBox math also explains why an icon can look crisp at one size and soft at another. A 24-unit icon has a scale of 1 at 24 pixels, 2 at 48, and about 1.333 at 32. At a whole-number scale, a line drawn on whole units starts and ends on pixel edges. At 1.333, it does not.
We measured this in the same librsvg run. A 2-unit-wide vertical bar at x = 11 in a 24 by 24 viewBox covers exactly two fully opaque pixel columns at 24 pixels and four at 48. At 32 pixels it spans pixel positions 14.67 to 17.33, and the renderer drew two full columns plus one column on each side at about one third opacity (alpha 85 out of 255). At 36 pixels, a scale of 1.5, the edge columns came out at half opacity (alpha 128). The bar is the same width in every case, but at the fractional scales its edges are anti-aliased, which reads as softness on a 1x screen.
The practical consequences:
- Ship icons at multiples of their grid when you can: a 24-unit icon at 24, 48 or 72 pixels; a 16-unit icon at 16 or 32.
- If a design calls for 20-pixel icons, draw them on a 20-unit grid, or accept the soft edges on low-density screens.
- On a 2x or 3x screen, the device pixel ratio multiplies the scale again, so a scale of 1.333 in CSS pixels is 2.667 device pixels on a 2x display, and the softness is much harder to see.
Grid and stroke choices for icon sets are covered in more depth in the icon design principles guide, and the relation between CSS pixels and device pixels is set out in the DPI and PPI guide.
Edge Cases Inside the Arithmetic
A few situations follow directly from the 14 steps but rarely get written down.
A drawing that pokes outside its viewBox. The viewBox is a window, not a crop of the file's contents. Anything drawn outside it, for example a stroke centred on the edge of the art, sits outside the mapped area. With meet and leftover space, that part can show in the empty band; at a matching ratio, it lies beyond the edge of the element's box, where it can be cut off. A 2-unit stroke centred on x = 0 has its outer half in that zone. The fix is to pad the viewBox (for example "-1 -1 26 26") or keep strokes inside the grid, not to change preserveAspectRatio.
The viewBox origin after an export. A file can arrive with a viewBox such as "312 140 24 24", for example when the icon was drawn away from the origin of a larger artboard. It renders correctly, since step 9 subtracts the origin. It is only harder to edit by hand and to compare across a set. Normalising the origin to 0 0 by moving the artwork is a safe cleanup; changing the viewBox numbers alone moves the window off the drawing.
preserveAspectRatio on other elements. The SVG 2 specification applies preserveAspectRatio to every element that establishes a viewport, plus image, marker, pattern and view. On an image element inside an SVG it controls how the referenced picture fits the image's box, using the same keywords.
Nested svg elements. An svg inside another svg is a new viewport with its own viewBox and its own preserveAspectRatio. The inner mapping happens inside the outer one, so the final scale is the product of both. A 24-unit icon nested at width 48 inside an outer svg that is itself scaled by 0.5 renders at 24 screen pixels.
Rounded results. The arithmetic gives exact numbers, but a box sized by CSS percentages rarely comes out to whole pixels. Translates such as 37.5 pixels are normal and do not indicate a bug.
Checking Your Own File in Two Minutes
You can do the arithmetic for any icon or logo on paper, or in a browser console with the element's measured size:
- Read the four viewBox numbers and the element's rendered width and height (in DevTools, the box model panel).
- Divide width by vb-width and height by vb-height. If the two results match, preserveAspectRatio has no effect and you are done.
- If they differ, take the smaller for meet or the larger for slice.
- Multiply vb-width and vb-height by that scale. The difference from the element's size is the leftover space (positive) or overflow (negative).
- Apply the alignment keyword to the axis that has leftover or overflow: Min adds nothing, Mid adds half, Max adds all of it.
If the result does not match what you see on screen, look for one of three causes: a CSS rule overriding the width or height attributes, a parent svg adding its own scale, or a viewBox with a non-zero origin you did not notice.
After the Coordinates Are Right
Once the viewBox is correct and the box is sized on purpose, the remaining work on an SVG is weight and delivery. The SVG optimization guide takes one icon from 819 bytes to 200 and lists the optimizer settings that must stay off, viewBox removal first among them. Read it next if your icons are correct but heavier than they need to be.
viewBox and preserveAspectRatio Questions
What does viewBox="0 0 24 24" mean?
It means the drawing uses a coordinate system 24 units wide and 24 units tall, starting at 0, 0 in the top-left corner, and that the whole of that area should be fitted into the element's box on the page. The numbers are not pixels. At width="48" the scale is 2; at width="16" it is about 0.667.
What is the difference between viewBox and width and height?
The viewBox describes which part of the drawing to show, in the drawing's own units. width and height describe how big the box on the page is, in CSS pixels. The browser maps the first onto the second with a scale and a translate. Removing the viewBox leaves a fixed-size image that cannot scale; removing width and height leaves a drawing that takes its size from CSS or, inline, fills its container.
Why does changing xMin to xMax do nothing?
Because that half of the keyword only acts on an axis with leftover space (under meet) or overflow (under slice). If your viewport is relatively taller than the viewBox and the mode is meet, all the spare space is vertical, so only YMin, YMid or YMax changes anything. Switch to a wider box, or to slice, and the x keyword takes effect.
Is the default preserveAspectRatio meet or slice?
Meet, with xMidYMid alignment. SVG 2 lists "xMidYMid meet" as the initial value, and both the align and the meet defaults apply separately: leaving out the second keyword gives meet, and leaving out the attribute gives xMidYMid meet.
Do inline SVG icons need width and height for CLS?
They do not need them for the ratio, because the browser reads the viewBox from the HTML without a download. They do help when the CSS that sizes the icon arrives after first paint, since without attributes the icon starts at 100% of its container. Keeping width="24" height="24" costs a few bytes and removes that risk.
Should I set preserveAspectRatio="none" on a logo?
No. none scales each axis separately, which distorts the mark whenever the box and the viewBox have different ratios. If a logo does not fill its box, fix the box's ratio or the file's viewBox. Keep none for backgrounds, dividers and data fills that have no proportions of their own.
Specifications and References Checked
- W3C, Scalable Vector Graphics (SVG) 2, chapter 8, "Coordinate Systems, Transformations and Units", https://www.w3.org/TR/SVG/coords.html: the 14-step "Computing the equivalent transform of an SVG viewport" algorithm, meet and slice definitions, xMidYMid meet initial value, the rule that none ignores meetOrSlice, zero and negative viewBox values, which elements take preserveAspectRatio, the rule that preserveAspectRatio is ignored without a viewBox, and section 8.12 on intrinsic size and aspect ratio. Checked November 13, 2026.
- W3C, SVG 2, chapter 7, "Geometry Properties", https://www.w3.org/TR/SVG/geometry.html: the sizing table where auto on the svg element resolves to 100%. Checked November 13, 2026.
- W3C, SVG 2, chapter 6, "Styling", https://www.w3.org/TR/SVG/styling.html: presentation attributes at the author level with specificity 0; width and height listed as presentation attributes on svg. Checked November 13, 2026.
- MDN Web Docs, "viewBox" (last modified April 7, 2026), https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/viewBox: value syntax, the five elements it applies to, zero or negative sizes. Checked November 13, 2026.
- MDN Web Docs, "svg" element (last modified October 31, 2025), https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/svg: width and height as the displayed viewport, default auto, preserveAspectRatio default xMidYMid meet. Checked November 13, 2026.
- W3C, CSS Images Module Level 3 (Candidate Recommendation Draft, December 18, 2023), https://www.w3.org/TR/css-images-3/: object-fit fill, contain and cover definitions, initial values of object-fit and object-position. Checked November 13, 2026.
- W3C, CSS 2.2, section 10, "Visual formatting model details", https://www.w3.org/TR/CSS22/visudet.html: sizing replaced elements that have a ratio but no intrinsic size, and the 300 by 150 fallback. Checked November 13, 2026.
- WHATWG, HTML Living Standard, "Rendering", section 15.4.3, https://html.spec.whatwg.org/multipage/rendering.html: width and height mapped to aspect-ratio on img, video and canvas. Checked November 13, 2026.
- web.dev, "Optimize Cumulative Layout Shift" (last updated February 7, 2025), https://web.dev/articles/optimize-cls: always set width and height on images, the default aspect ratio browsers derive from them. Checked November 13, 2026.
Both scale and translate tables, the axis rule and the anti-aliasing figures are our own work from the algorithm above. The 38 placement checks and the pixel opacity readings were rendered on November 13, 2026 with librsvg 2.58.0 (rsvg-convert) and read back from the PNG output; they show one renderer's behaviour, not every browser's.