Styling Images and SVG Icons: the img Reset, object-position, image-rendering, fill, stroke and currentColor
The gap under an image comes from img being inline; a reset of display: block, max-inline-size: 100% and block-size: auto removes it, keeps the ratio and still lets the width/height attributes reserve space. The post measures object-position for the focal point, image-rendering and image-orientation, fill, stroke and currentColor in inline SVG, Bookstore's icon set from a single sprite, and logo colors with light-dark() in dark mode.
Version note: This post follows the CSS Images Level 3/4 and CSS Color Level 5 specifications and the November 2026 state of Baseline (web-features 3.46.0); the examples were run in Chromium 158 and WebKit 27.2 (Playwright 1.65), and Firefox support comes from web-features and MDN browser-compat-data 8.1.9.
Guess First
Bookstore, the online bookstore used as the shared example across this site's series, is now full of cover images, an author photo and a few icons. In this post we bring order to them: one reset for all images, a crop that shows the right part of the photo, and a single SVG sprite for the icons in the catalog. First, an old question:
/* 1) an inline img sits on the baseline */
#inline-img img { inline-size: 100px; block-size: 100px; }
#block-img img { display: block; inline-size: 100px; block-size: 100px; }
Both gray boxes contain a 100×100 image and nothing else. How tall are the boxes? The font is 20px, line-height is 1.5. Write your answer down.
The Inline Behavior of img, and Its Reset
img is an inline element by default; it sits in the line like a letter, and its bottom edge rests on the line's baseline (the line the letters stand on). Below the baseline there is room reserved for the parts of letters such as "g" and "y" that reach down. The measurement in the lab:
node scripts/09-images.mjs
--- chromium
inline-img frame 300x108
block-img frame 300x100
--- webkit
inline-img frame 300x107.44
block-img frame 300x100
The box with the inline image is 108 pixels tall in Chromium and 107.44 in WebKit; there is a 7 to 8 pixel gap under the image. With display: block the image leaves the line and the box is exactly 100 pixels. The size of the gap depends on the font and on line-height; the small difference between the engines comes from font metrics. vertical-align: middle or bottom also removes the gap (we used that property for icon alignment in the second post of the series), but you rarely want an image to be part of a line. That is why most projects make images block from the start.
The second problem is size. The width and height attributes in HTML are needed so the layout can reserve the image's space before the file arrives (the reasoning is in post 08 of the HTML5 and Semantic HTML series), but if CSS does not control them, they keep the image at that size. I put a 1200×400 banner in a 300px wide box:
/* 2) the reset: never wider than the container, height from the aspect ratio */
#max-only img { max-inline-size: 100%; }
#reset img, #pending img { display: block; max-inline-size: 100%; block-size: auto; }
max-only img 300x400
reset img 300x100
pending img 300x100 before the file arrives
With only max-inline-size: 100%, the image narrows to 300px but its height stays at the 400px from the height attribute; the ratio breaks. Adding block-size: auto computes the height from the ratio and the image becomes 300×100. The third line matters even more: an image whose file never arrives also takes up 300×100. Browsers turn the width and height attributes into a default aspect-ratio, and block-size: auto uses that ratio. So the reset does not break the job the attributes do in preventing CLS.
We added this reset to Bookstore's base styles and deleted the repeated display: block and block-size: auto lines from the components:
/* images: block, never wider than their container, height from the width/height ratio */
img, picture, video {
display: block;
max-inline-size: 100%;
block-size: auto;
}
We did not put svg in this list: icons that sit inside text, like the one next to the price, must stay inline. You can write max-width instead of max-inline-size; the logical versions follow the writing direction (post 03 of the CSS3 series).
Choosing the Focal Point with object-position
In the previous post we fitted the author photo into a square box with object-fit: cover, and the top of the author's head was cut off. object-fit decides how the image fills the box (post 08 of the CSS3 series); object-position decides which part of the cropped image stays in the box. Its initial value is 50% 50%, the middle:
/* 3) object-fit crops, object-position picks the part that stays */
.portrait { display: block; inline-size: 96px; block-size: 96px; object-fit: cover; }
#focus-top { object-position: top; }
focus-center object-position 50% 50% pixel (48, 12): rgb(180, 121, 91)
focus-top object-position 50% 0% pixel (48, 12): rgb(69, 49, 41)
The 240×360 portrait is scaled to 96 pixels wide, and 48 of its 144 pixels of height are cropped. Cropped from the middle, row 12 shows the face (180, 121, 91); cropped from the top, the same point shows hair (69, 49, 41), so the top of the head is now inside the box. object-position takes the same syntax as background-position: keywords, percentages and offsets from an edge (right 20% bottom 0). In photos the face is usually in the top third, so top or 50% 25% is a good start. We added it to the author photo in Bookstore:
.author-photo {
flex: none;
inline-size: 6rem;
block-size: 6rem;
object-fit: cover;
/* the face is in the top third of the portrait */
object-position: top;
/* same result as border-radius: 50% here, but it also clips outline and shadows */
clip-path: circle(50%);
}
image-rendering and image-orientation
When the browser scales an image up, it fills the space between pixels with smooth transitions. That is right for photos, but images with sharp edges, such as pixel art, a small QR code or a store pickup code, become blurry. image-rendering: pixelated draws each pixel as an enlarged square:
/* 4) a small code image scaled up eight times */
.code { display: block; inline-size: 64px; block-size: 64px; }
#code-pixelated { image-rendering: pixelated; }
--- chromium
code-smooth distinct colors: 91
code-pixelated distinct colors: 2
--- webkit
code-smooth distinct colors: 58
code-pixelated distinct colors: 2
The image has only two colors. Scaled up eight times, the default rendering produces 91 different colors in Chromium and 58 in WebKit: the in-between tones at the edges. With pixelated, only 2 colors remain in both engines.
Photos taken with a phone are often stored sideways, and the right orientation is written in the EXIF data. The default of image-orientation is from-image: the browser rotates the image according to that data.
<img id="photo-auto" src="../img/shelf-photo.jpg" alt="">
<img id="photo-none" src="../img/shelf-photo.jpg" alt="" style="image-orientation: none">
photo-auto image-orientation from-image box 100x200 bottom left pixel rgb(21, 127, 61)
photo-none image-orientation none box 200x100 bottom left pixel rgb(184, 28, 29)
The file is 200×100 and its EXIF says "rotate 90 degrees". With the default value the image is drawn as 100×200 and its bottom left corner is green; with none you get the raw file, 200×100 with a red bottom left corner. There is no reason to change the default for photos uploaded by users; the only use of none is to stop an image from turning twice when you have already rotated it on the server without removing the EXIF data.
Inline SVG: fill, stroke and currentColor
An SVG added to the page with img behaves like a single image; CSS cannot reach the shapes inside it. In an SVG written inline, or pulled from a sprite with <use>, values such as fill, stroke and stroke-width are ordinary CSS properties and they inherit. I compared three hearts in the lab:
<!-- 5) the sprite: symbols are defined once, then used by id -->
<svg hidden>
<symbol id="heart" viewBox="0 0 24 24"><path d="M12 21s-7.5-4.6-9.4-9.3C1.2 8.3 3.4 4.5 7 4.5c2 0 3.6 1.1 5 2.8 1.4-1.7 3-2.8 5-2.8 3.6 0 5.8 3.8 4.4 7.2C19.5 16.4 12 21 12 21z"/></symbol>
<symbol id="heart-fixed" viewBox="0 0 24 24"><path fill="#f59e0b" d="M12 21s-7.5-4.6-9.4-9.3C1.2 8.3 3.4 4.5 7 4.5c2 0 3.6 1.1 5 2.8 1.4-1.7 3-2.8 5-2.8 3.6 0 5.8 3.8 4.4 7.2C19.5 16.4 12 21 12 21z"/></symbol>
</svg>
<svg class="icon" id="icon-use" aria-hidden="true"><use href="#heart"/></svg>
<svg class="icon" id="icon-fixed" aria-hidden="true"><use href="#heart-fixed"/></svg>
<img class="icon" id="icon-img" src="../img/heart.svg" alt="">
<svg class="icon" id="icon-use" aria-hidden="true"><use href="#heart"/></svg>
<svg class="icon" id="icon-fixed" aria-hidden="true"><use href="#heart-fixed"/></svg>
<img class="icon" id="icon-img" src="../img/heart.svg" alt="">
/* 6) inline SVG takes its color from the text */
.icon { display: inline-block; inline-size: 1em; block-size: 1em; fill: currentColor; }
.links { color: #1d4ed8; }
img.icon { fill: #b91c1c; }
icon-use size 20x20 pixel: rgb(29, 78, 216)
icon-fixed size 20x20 pixel: rgb(245, 158, 11)
icon-img size 20x20 pixel: rgb(0, 0, 0)
All three icons are 20×20: 1em equals the font size, so the icon grows with the text. The heart pulled in with <use> takes the section's blue text color (29, 78, 216) thanks to fill: currentColor. The path in the heart-fixed symbol has a fill="#f59e0b" attribute, and the icon stays orange: the path copied into <use> carries its own value, and the value inherited from outside does not reach it. Remove fill and stroke attributes from the shapes in a sprite and leave color to CSS. The heart added with img is black (0, 0, 0) despite the fill: #b91c1c rule: SVG's default fill is black and CSS cannot get inside.
Catalog Icons: One Sprite and currentColor
In Bookstore we collected three icons (cart, heart, price tag) as symbol elements in one file:
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="cart" viewBox="0 0 24 24"><path d="M3 4h2l2.4 10.2a1 1 0 0 0 1 .8h8.9a1 1 0 0 0 1-.8L20 7H6"/><circle cx="9" cy="19" r="1.5"/><circle cx="17" cy="19" r="1.5"/></symbol>
<symbol id="heart" viewBox="0 0 24 24"><path d="M12 21s-7.5-4.6-9.4-9.3C1.2 8.3 3.4 4.5 7 4.5c2 0 3.6 1.1 5 2.8 1.4-1.7 3-2.8 5-2.8 3.6 0 5.8 3.8 4.4 7.2C19.5 16.4 12 21 12 21z"/></symbol>
<symbol id="tag" viewBox="0 0 24 24"><path d="M3 3h8l10 10-8 8L3 11z"/></symbol>
</svg>
/* icons from img/icons.svg: as big as the text, painted in the text color */
.icon {
inline-size: 1em;
block-size: 1em;
flex: none;
fill: currentColor;
}
/* line icons: no fill, the stroke takes the text color */
.icon-line {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linejoin: round;
}
<p class="price"><svg class="icon" aria-hidden="true"><use href="img/icons.svg#tag"/></svg> ₺420.00</p>
<button type="button" class="fav" aria-pressed="false" aria-label="Favorite: Dune"><svg class="icon icon-line" aria-hidden="true"><use href="img/icons.svg#heart"/></svg></button>
.fav {
margin-block-start: 0.5rem;
padding: 0.25rem;
border: 0;
background: none;
color: var(--color-muted);
font-size: 1.5rem;
line-height: 0;
cursor: pointer;
}
.fav:hover, .fav[aria-pressed="true"] { color: var(--color-accent); }
/* the same symbol, filled: fill is a CSS property */
.fav[aria-pressed="true"] .icon { fill: currentColor; }
Filled icons (the price tag) are painted with fill: currentColor, line icons (cart, heart) with icon-line. The heart does not need a second icon to fill up when it is a favorite: we give the same symbol a fill. The button's state lives in aria-pressed and the look depends on it; JavaScript only changes the attribute. The measurement on the page:
node scripts/09-pages.mjs
--- chromium
cover frame height 289.796875, img height 289.796875
price icon 16x16, sprite requests: 1
heart normal fill none, stroke rgb(75, 85, 99), center pixel rgb(255, 255, 255)
heart hover fill none, stroke rgb(185, 28, 28), center pixel rgb(255, 255, 255)
heart pressed fill rgb(185, 28, 28), stroke rgb(185, 28, 28), center pixel rgb(185, 28, 28)
aria-pressed: true
author photo object-position 50% 0%, pixel (48, 12): rgb(69, 49, 41)
--- webkit
cover frame height 289.796875, img height 289.796875
price icon 16x16, sprite requests: 1
heart normal fill none, stroke rgb(75, 85, 99), center pixel rgb(255, 255, 255)
heart hover fill none, stroke rgb(185, 28, 28), center pixel rgb(255, 255, 255)
heart pressed fill rgb(185, 28, 28), stroke rgb(185, 28, 28), center pixel rgb(185, 28, 28)
aria-pressed: true
author photo object-position 50% 0%, pixel (48, 12): rgb(69, 50, 40)
The cover frame and the image have the same height (289.80); the gap below is gone thanks to the reset. The price icon is 16×16, the same as the 1rem text. The sprite, used in nine places on the page (four price tags, four hearts, one cart), is requested only once. The heart is a gray line (75, 85, 99) by default with the card's white in the middle; on hover the line turns red; when pressed, fill turns red too and the middle pixel is (185, 28, 28). The last line is about the author photo: object-position: 50% 0% and hair in row 12.
An external sprite (icons.svg#cart) has one condition: <use> only accepts files from the same origin. If your icons are served from a CDN, you have to embed the sprite inline in the page. The benefit of the external file is that it stays in the browser cache: pages do not carry the same SVG in every HTML response.
Images and Logos in Dark Mode
For a colored logo or icon in a dark theme, filter: invert(1) looks like the easiest route, but it inverts the brand color too. light-dark(), which picks the color for the theme, gives a better result:
/* 7) a logo color for each color scheme, and what invert() does instead */
.logo { inline-size: 48px; block-size: 48px; fill: light-dark(#b91c1c, #fca5a5); }
#logo-inverted { fill: #b91c1c; filter: invert(1); }
--- chromium, light: logo rgb(185, 28, 28), inverted logo rgb(70, 227, 227)
--- chromium, dark: logo rgb(252, 165, 165), inverted logo rgb(70, 227, 227)
The logo with light-dark() is red (185, 28, 28) in the light theme and light red (252, 165, 165) in the dark theme: a shade of the same color that reads well on a dark background. invert(1) turns the red into cyan (70, 227, 227), and does so in both themes, because a filter knows nothing about the theme. light-dark() works only on a page whose color-scheme is light dark; the function itself and theme tokens are in post 10 of the CSS3 series. For images CSS cannot paint, such as photos or multi-colored logos, use a separate source: a <source> with media="(prefers-color-scheme: dark)" inside <picture>.
In Production
Use SVG instead of icon fonts (icons delivered as characters in a font file). When an icon font fails to load, or the user forces their own font, the icons turn into meaningless characters, screen readers may try to read those characters, and the font makes the browser download hundreds of icons the page does not use. In an SVG sprite each icon is a separate symbol and only the used ones are drawn.
Give decorative icons aria-hidden="true"; for an icon that carries information (such as a button that consists only of an icon), the accessible name must be on the button itself (aria-label or visually hidden text). The favorite buttons in the catalog carry aria-label="Favorite: Dune".
Never remove the width and height attributes from images; together with the reset, they provide the right ratio and the reserved space. Responsive image selection (srcset, sizes, <picture>) is on the markup side, in post 08 of the HTML5 series.
Common Pitfalls
First, trying to close the gap under an image with margins or negative values. The cause is the inline behavior; display: block is enough.
Second, writing max-inline-size: 100% in the reset and forgetting block-size: auto. The image narrows, but its height stays at the attribute value and the ratio breaks.
Third, leaving fill and stroke attributes on the shapes in a sprite or an inline SVG. The currentColor in your CSS does not reach those shapes.
Fourth, adding an SVG whose color you want to change with CSS through img. An SVG inside img cannot be styled; use inline SVG, <use>, or the mask method from the previous post.
Fifth, giving colored images filter: invert() for dark mode. Brand colors break; use light-dark() or a separate source.
Baseline Status
node scripts/baseline.mjs object-fit object-position image-rendering image-orientation currentcolor light-dark svg
object-fit widely 2022-07-15 Chrome 32, Firefox 36, Safari 10, iOS 10
object-position widely 2022-07-15 Chrome 32, Firefox 36, Safari 10, iOS 10
image-rendering widely 2024-04-05 Chrome 41, Firefox 93, Safari 10, iOS 10
image-orientation widely 2022-10-13 Chrome 81, Firefox 26, Safari 13.1, iOS 13.4
currentcolor widely 2018-01-29 Chrome 1, Firefox 1.5, Safari 4, iOS 3.2
light-dark newly 2024-05-13 Chrome 123, Firefox 120, Safari 17.5, iOS 17.5
svg widely 2022-07-15 Chrome 1, Firefox 3, Safari 3, iOS 2
| Feature | Status | Note |
|---|---|---|
object-fit, object-position | Widely available | Same syntax as background-position |
image-rendering: pixelated | Widely available | Newly in 2021-10, widely in 2024-04 |
image-orientation | Widely available | The default is from-image |
SVG, <use href>, currentColor | Widely available | External sprites only from the same origin |
light-dark() | Newly available (2024-05) | Needs color-scheme |
None of the features in this post is in the "not Baseline" category. A small detail: according to browser-compat-data, the image-rendering: crisp-edges value reached Chrome without a prefix only in version 148; for pixel art and codes, pixelated is the older and safer choice.
A Small Experiment
★☆☆ Remove the gap in the lab's #inline-img with vertical-align, without changing display, and verify with 09-images.mjs that the box drops to 100 pixels.
★★☆ Add a star icon to the sprite and put it next to the rating ring on the book page. Its color should come from currentColor; without writing a fill attribute on the symbol's path, measure with a line you add to 09-pages.mjs that its color changes on hover.
★★★ Add a dark theme to Bookstore (color-scheme: light dark): on a page opened with colorScheme: 'dark', show with pixel measurements that the favorite heart, the price icon and the cart icon stay readable on the dark background.
In the next post we move on to generated content: ::before and ::after, the values of content and its alternative text, counters that number the checkout steps, @counter-style, ::marker and list-style.