Eduardo Cortez
1 October, 2025
Every image you load creates a request to the server. Even if HTTP/2/3 reduces that overhead, fewer requests still mean:
One tried-and-true technique for this is called a CSS sprite: combining many small icons into a single image, then showing the right piece with CSS. In this article we’ll:
We’ll use a 120×120 PNG sprite (iconos.png) with 9 icons (40×40 each) in a 3×3 grid:
Download the sprite (PNG)
Icons: LinkedIn, Instagram, Google | GitHub, Figma, Facebook | Clubhouse, Discord, Dribbble.
<i class="sicon sicon--linkedin"></i> <i class="sicon sicon--instagram"></i> <i class="sicon sicon--google"></i>
:root {
--icon-size: 40px;
--sprite-cols: 3;
--sprite-rows: 3;
--sprite-url: url('/assets/iconos.png');
}
.sicon {
display:inline-block;
width:var(--icon-size);
height:var(--icon-size);
background-image:var(--sprite-url);
background-repeat:no-repeat;
background-size: calc(var(--sprite-cols) * var(--icon-size))
calc(var(--sprite-rows) * var(--icon-size));
background-position:
calc(var(--col,0) * -1 * var(--icon-size))
calc(var(--row,0) * -1 * var(--icon-size));
}
.sicon--linkedin { --col:0; --row:0; }
.sicon--instagram { --col:1; --row:0; }
.sicon--google { --col:2; --row:0; }
Scaling: override --icon-size.
.sicon-mask {
width: var(--icon-size);
height: var(--icon-size);
background-color: currentColor;
-webkit-mask-image: var(--sprite-url);
mask-image: var(--sprite-url);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: calc(3 * var(--icon-size)) calc(3 * var(--icon-size));
mask-size: calc(3 * var(--icon-size)) calc(3 * var(--icon-size));
-webkit-mask-position:
calc(var(--col,0) * -1 * var(--icon-size))
calc(var(--row,0) * -1 * var(--icon-size));
mask-position:
calc(var(--col,0) * -1 * var(--icon-size))
calc(var(--row,0) * -1 * var(--icon-size));
}
<i class="sicon-mask sicon--instagram" style="color:#E1306C"></i>
A neat benefit of sprites: hover states cost zero extra requests.
Place two icon states (default + hover) stacked vertically in the same cell height, then switch background-position on hover.
Example: normal LinkedIn icon above, hover state below in the sprite.
.sicon--linkedin {
--col:0;
--row:0; /* default */
}
.sicon--linkedin:hover {
--row:1; /* hover state right below */
}
That way, you only load one file for both states, keeping performance high and alignment pixel-perfect.
You don’t need to calculate positions by hand. Here’s a vanilla JS custom element <sprite-icon> and a utility for dataset attributes.
(We covered the full code earlier with accessibility fixes, fallbacks, and input guards — see snippet in article body.)
Usage:
HTML
<sprite-icon src="/assets/iconos.png" cols="3" rows="3" col="1" row="0" size="24"></sprite-icon> <sprite-icon src="/assets/iconos.png" cols="3" rows="3" col="1" row="0" size="24" mode="mask" tint="#E1306C"></sprite-icon>
Our JS component auto-handles this correctly.
Reducing requests is still a core web performance win. CSS sprites—though an older name—remain a simple, powerful tool for icons:
If you’re working with a fixed set of raster icons (like social media logos), this is one of the fastest optimizations you can make in 2025.