CSS sprites were a staple front-end performance technique for about a decade. They've largely been superseded now, but understanding why they worked — and what replaced them — gives you useful insight into how browsers and HTTP interact with images.
The Problem They Solved
Back when HTTP/1.1 ruled the web, browsers could open only a limited number of parallel connections to any single domain — typically 6. Every separate file on a page consumed one of those connection slots until the download finished.
A toolbar with 20 small icons meant 20 separate HTTP requests. With a cold cache, that could mean 20 round trips, each with its own TCP handshake and DNS lookup cost. On high-latency connections, the delay stacked up noticeably.
The sprite technique was a workaround: instead of serving 20 separate 2KB icon files, you serve one 40KB image containing all 20 icons arranged on a grid. One request, one download, one cached asset. Then you position the single image differently for each element to show only the relevant icon section.
How CSS Sprites Work
The implementation relies on background-position. You set the combined sprite image as the background of an element, then offset it so only the desired icon is visible within the element's bounds.
.icon {
background-image: url('/sprites/icons.png');
background-repeat: no-repeat;
width: 24px;
height: 24px;
}
.icon-home {
background-position: 0 0; /* first icon, top-left */
}
.icon-search {
background-position: -24px 0; /* second icon */
}
.icon-settings {
background-position: -48px 0; /* third icon */
}
The negative offset values are how you "scroll" the background to the correct position. If your sprite image has icons arranged horizontally with 24px spacing, moving -24px shows the second icon in the window.
<a href="/home" class="icon icon-home" aria-label="Home"></a>
<button class="icon icon-search" aria-label="Search"></button>
The element itself acts as a viewport into the larger sprite sheet. Because the background image is shared across all icon elements, browsers only download it once.
The Sprite Workflow
Building sprites manually is tedious. Most teams used build tools — Compass, Grunt plugins, or webpack loaders — that would:
- Scan a folder of individual icon files
- Pack them into a single sprite sheet using a bin-packing algorithm
- Output the sprite image and auto-generate the CSS with correct
background-positionvalues for each icon
This meant the source files stayed as individual icons (easy to maintain), but the build artifact was a single sprite sheet. Updating an icon meant regenerating the whole sprite, which also busted the cache for every icon on the site.
HTTP/2 Changes the Calculus
HTTP/2 introduced request multiplexing: multiple requests and responses travel over a single TCP connection simultaneously, without head-of-line blocking. Connection limits per domain are essentially irrelevant — the browser can request 100 small files and receive them all in parallel over one connection.
With multiplexing, the core motivation for CSS sprites evaporates. Twenty separate 2KB icons are no longer worse than one 40KB sprite. In fact, separate files can be better: if a user visits a page that only needs 3 of your 20 icons, they only download those 3 files. With a sprite, they download all 20 regardless.
Sprites also hurt caching granularity. Change one icon, and every user's cached sprite is invalidated. With separate files, only the changed icon's cache busts.
HTTP/2 is now supported by over 97% of browsers and is the default on virtually every modern hosting platform. Cloudflare, Vercel, Netlify, and nginx with HTTP/2 enabled all multiplex by default.
Modern Alternatives
Several approaches handle icon systems better today:
Inline SVG
The most flexible option. SVG is XML, so you can embed it directly in HTML, style it with CSS (including currentColor for theme-aware coloring), animate it, and manipulate it with JavaScript.
<button class="icon-btn">
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 9l9-7 9 7v11a2 2 0..." fill="currentColor"/>
</svg>
Home
</button>
Icons that use currentColor inherit their parent's color property, so dark mode and theming come for free.
SVG Sprites
A middle ground: define all SVG symbols in a hidden <svg> block at the top of the page, then reference them by ID anywhere in the document.
<!-- Hidden definitions -->
<svg style="display:none">
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="M3 9l9-7 9 7v11a2 2 0..."/>
</symbol>
</svg>
<!-- Usage anywhere in the page -->
<svg width="24" height="24">
<use href="#icon-home"/>
</svg>
This keeps the HTML readable while avoiding duplicate SVG markup. SVG sprites can also be external files referenced with href="/icons.svg#icon-home" and cached separately.
CSS Custom Properties + Masks
For decorative icons where you don't need multi-color SVG paths, CSS masks with custom properties give you theme-aware icon coloring without inline SVG verbosity:
.icon-home {
background-color: currentColor;
-webkit-mask-image: url('/icons/home.svg');
mask-image: url('/icons/home.svg');
width: 24px;
height: 24px;
}
The icon shape comes from the SVG mask, but the color is driven by the element's color property. Works cleanly with CSS variables for theming.
Icon Fonts
Icon fonts (Font Awesome, Material Icons) were popular but have fallen out of favor. They require a full font file download even if you use two icons, they render as text (causing quirks with anti-aliasing and subpixel rendering), and they're accessibility-awkward without careful ARIA handling. Inline SVG is generally preferred today.
When Sprites Still Make Sense
There are still a few legitimate use cases for sprite sheets:
- Canvas-based games: drawing dozens of sprite frames per second from a single texture is a well-established GPU optimization. Game engines use sprite atlases for this reason.
- CSS animations with a filmstrip: animating through frames of a hand-drawn character or loading animation using
background-positionsteps is clean and performant. - HTTP/1.1 environments: if you're maintaining a legacy internal tool that you know runs on HTTP/1.1 infrastructure without upgrade plans, sprites still win.
- Retina display images in email: email clients don't support HTTP/2, and combining multiple product thumbnails into a sprite can reduce request count in newsletter templates.
For most modern web projects, though, reach for inline SVG or SVG symbols first.
Preparing Your Images
Whether you end up with individual files or a sprite, the underlying image quality matters. Before generating any sprite sheet or serving individual icons, run your source assets through Image Compressor to reduce file size. And if you need to convert PNG icons to WebP or SVG-compatible formats, Image Converter handles the format side.
For context on why HTTP/2 multiplexing matters at the rendering level, How Browsers Render a Page covers the full request-to-paint pipeline.
The problem sprites solved was real, and the HTTP/2 solution is elegant. Knowing both helps you reason clearly about any legacy codebase you might encounter, and makes "why are we doing it this way?" a lot easier to answer.
FAQ
Are CSS sprites still worth using in 2026?
For most web work, no. HTTP/2 multiplexing is universal (97%+ of browsers, default on every modern host), so the original justification is gone. The exceptions are canvas-based games using sprite atlases for GPU efficiency, CSS filmstrip animations where background-position steps are the cleanest implementation, and email templates where HTTP/1.1 is still the rule. For anything else — toolbar icons, social icons, decorative graphics — inline SVG or SVG symbols beats sprites in every dimension.
What's the difference between an SVG sprite and a CSS sprite?
A CSS sprite is one bitmap image (PNG/JPEG) with multiple visual sub-images that you reveal via background-position. An SVG sprite is one SVG file containing multiple <symbol> elements that you reference by ID with <use href="#icon-home">. The SVG sprite is smaller, scalable to any DPI without quality loss, themeable via currentColor, and individually targetable in DOM. The CSS sprite is fixed-color, fixed-size, and visually a single image to the browser.
How does HTTP/2 multiplexing actually beat sprites?
HTTP/2 sends multiple requests and responses over a single TCP connection simultaneously, with each stream interleaved frame-by-frame. There's no per-request handshake, no connection limit, no head-of-line blocking. Twenty 2KB icons download in roughly the same time as one 40KB sprite, but you get cache granularity — change one icon and only that file's cache is busted. And users who don't see all icons download fewer bytes overall.
Do I still need to worry about sprites for retina displays?
Not really. The "2x sprite" pattern (creating a separate high-DPI sprite and swapping it via media queries) was needed when you couldn't easily serve different bitmap resolutions per device. Today, the answer is srcset for <img> tags, image-set() in CSS background images, or just SVG (which scales infinitely). Retina-handling is no longer a sprite-specific concern.
Why are icon fonts considered worse than SVG now?
Several reasons. Icon fonts ship the entire alphabet even if you use two glyphs, while SVG sprites can be tree-shaken to only the symbols in use. Fonts render with text antialiasing rules that produce subpixel artifacts at certain sizes. Screen readers can read icon font glyphs as text characters, which is accessibility-hostile without careful aria-hidden and label attributes. SVG renders crisply, can be styled with CSS per element, and is naturally accessible with <title> and ARIA roles.
Can I generate sprite sheets automatically in modern build tools?
Yes — webpack has image-webpack-loader, Vite has vite-plugin-svg-icons for SVG sprites, and standalone tools like svg-sprite or svg-symbol-sprite-loader work in any pipeline. For SVG sprites specifically, the workflow is: drop individual SVG icons in a folder, build step packs them into one sprite file with auto-generated <symbol> IDs, components reference them via <use>. This gets you the "single request" benefit without the maintenance pain of bitmap sprites.
What about CSS animations using `background-position` steps — is that still a valid pattern?
Yes, for filmstrip-style animations. If you have a hand-drawn loading spinner with 12 frames or a sprite-sheet character animation, animating background-position with steps(12) is genuinely the cleanest implementation. CSS animations don't interpolate well between separate images, so the filmstrip-on-one-image pattern stays useful for that specific case.
How big should an SVG sprite get before I split it?
If your SVG sprite is over ~100KB raw or contains icons used in fewer than 5% of pages, consider splitting. Common patterns: a "core" sprite loaded on every page, plus per-route sprites loaded on demand. Or use individual SVG files served from a single domain with HTTP/2 — the request multiplexing makes per-icon files nearly free, and you get cache granularity. The sweet spot for one consolidated sprite is somewhere around 20–60 commonly-used icons.