Create CSS/SVG image sprites.
0
image-sprites.md
270 lines 7.8 kB View raw View code

Image Sprites#

An image sprite is a collection of images put into a single image or file.

Helps reduce the number of HTTP requests, memory and bandwidth usage.

They are often used to significantly improve performance in emoji selectors.

Raster Sprites#

How to create a WebP emojis sprite.

Download ImageMagick.

Create an image sprite using the Montage tool:

montage png/*.png -background none -geometry +0+0 PNG32:sprites.png

Convert the image to WebP image format:

magick sprites.png -quality 100 -define webp:lossless=true -define webp:method=6 sprites.webp

Suppose we have an image sprite with 72x72 emojis from Unicode 16.0.

Use the following JavaScript code to calculate the background size and position:

const size = 72;     // size of each emoji
const width = 4536;  // image sprite width
const height = 4464; // image sprite height

const horizontal = width / size; // max number of emojis per row (63)
const vertical = height / size;  // max number of emojis per column (62)

// Calculate the background size.
// 100% is the size of the element that has the background image.
// We need to multiply by the number of emojis per row and column.
console.log('Background width:', 100 * horizontal); // 6300%
console.log('Background height:', 100 * vertical);  // 6200%
// CSS -> background-size: 6300% 6200%

// Calculate the emoji position at column 48, row 56.
// Column 1 and row 1 are always 0%, the max is 100%.
const position = calcPosition(48, 56);
console.log('Background position X:', position.x); // 75.806%
console.log('Background position Y:', position.y); // 90.164%
// CSS -> background-position: 75.806% 90.164%;

function calcPosition(col, row, d=3) {
    const x = 100 / (horizontal - 1) * --col;
    const y = 100 / (vertical - 1) * --row;
    return { x: x.toFixed(d), y: y.toFixed(d) };
}

HTML example of how to use the WebP image sprite:

<!DOCTYPE html>
  <head>
    <style>
      html { color-scheme: dark; }
      .emoji {
        /*
          The <img> element can be any size.
          Values greater than the emoji size will make the image look blurry.
          Set 'image-rendering' to 'pixelated' to make the image less blurry.
        */
        width: 72px;
        aspect-ratio: 1;
        image-rendering: pixelated;
        background-size: 6300% 6200%;
        background-image: url('sprites.webp');
      }
      .emoji28x1 { background-position: 43.548% 0.000%; }
      .emoji15x45 { background-position: 22.581% 72.131%; }
      .emoji48x56 { background-position: 75.806% 90.164%; }
    </style>
  </head>
  <body>
    <!-- Argentina Flag -->
    <p class="emoji emoji28x1"></p>
    <!-- Parrot -->
    <p class="emoji emoji15x45"></p>
    <!-- Fingerprint (Unicode 16.0) -->
    <p class="emoji emoji48x56"></p>
  </body>
</html>

Vector Sprites (SVG)#

How to use the <​use> element to reference SVG images.

Use the <use> element to reference only a part of an SVG image defined elsewhere.

<!DOCTYPE html>
  <body>
    <svg style="display: none;">
      <!-- Define the SVG images using the <symbol> element. -->
      <!-- Then you can reference them with the <use> element. -->
      <symbol id="circle" viewBox="0 0 24 24" stroke="currentColor" fill="none">
        <circle cx="12" cy="12" r="10"/>
      </symbol>
      <symbol id="square" viewBox="0 0 24 24" stroke="currentColor" fill="none">
        <rect width="18" height="18" x="3" y="3" rx="2"/>
      </symbol>
    </svg>
    <!-- Circle SVG -->
    <p>
      <svg style="width: 72px; aspect-ratio: 1;">
        <use href="#circle"/>
      </svg>
    </p>
    <!-- Square SVG -->
    <p>
      <svg style="width: 72px; aspect-ratio: 1;">
        <use href="#square"/>
      </svg>
    </p>
  </body>
</html>

The svg-sprite library can be used to create SVG sprites of several types.

The following example combines icons/*/*.svg into a single file for each subdirectory.

import fs from 'node:fs';
import path from 'node:path';
import SVGSpriter from 'svg-sprite';

if (!fs.existsSync('sprites'))
    fs.mkdirSync('sprites');

const iter = fs.globSync('icons/*/*.svg');

const groups = Object.groupBy(
    Array.from(iter),
    icon => icon.split(path.sep).at(-2)
);

for (const [sprite, icons] of Object.entries(groups)) {
    const spriter = new SVGSpriter({
        mode: {
            symbol: {
                inline: true
            }
        }
    });

    for (const icon of icons) {
        const name = `icon:${sprite}.${path.basename(icon)}`;
        spriter.add(name, null, fs.readFileSync(icon, 'utf8'));
    }

    const output = `sprites/${sprite}.svg`;
    const { result } = await spriter.compileAsync();

    for (const mode of Object.values(result))
        fs.writeFileSync(output, mode.sprite.contents, 'utf8');
}

/*
  INPUT
  --------------------------------------------------
  ./icons/
    misc/
      circle.svg
      square.svg
    emojis/
      butterfly.svg

  OUTPUT
  --------------------------------------------------
  ./sprites/
    misc.svg
    emojis.svg

  USAGE
  --------------------------------------------------
  <svg><use href="#icon:misc.circle"/></svg>
  <svg><use href="#icon:misc.square"/></svg>
  <svg><use href="#icon:emojis.butterfly"/></svg>
*/

How to use the <​img> element to display SVG images.

Use the <img> element with a fragment identifier to display only a part of an SVG image.

Using SVG Fragment Identifiers with <img> has a couple of advantages over other methods:

  • Only a single HTML element is required.
  • The <img> element behaves similarly to inline text:
    • When copied, the alt attribute text is included in the clipboard.
    • It can be selected, which makes it ideal for emoji-like icons in text.
  • Natively supports fallback behavior if the image or fragment fails to load.

However, it's worth noting that <svg> with <use> remains valuable for:

  • Reusable SVG symbols in contexts where you need finer control (e.g., altering fill, stroke).
  • Interactive or animated graphics where direct access to the DOM structure of the SVG is needed.
<!-- emojis-sprite.svg -->

<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
    <style>
      g:not(:target) {
        display: none;
      }
    </style>
  </defs>
  <!-- Circle SVG -->
  <svg viewBox="0 0 24 24" stroke="red" fill="none">
    <g id="circle">
      <circle cx="12" cy="12" r="10"/>
    </g>
  </svg>
  <!-- Square SVG -->
  <svg viewBox="0 0 24 24" stroke="red" fill="none">
    <g id="square">
      <rect width="18" height="18" x="3" y="3" rx="2"/>
    </g>
  </svg>
</svg>
<html>
  <body>
    <img src="/emojis-sprite.svg#circle" alt="circle"/>
    <img src="/emojis-sprite.svg#square" alt="square"/>
  </body>
</html>
Sign up or login to add to the discussion