DEV Community

Cover image for Modern CSS Spritesheets with Native HTML Elements
Björn Crombach
Björn Crombach

Posted on

Modern CSS Spritesheets with Native HTML Elements

The common approach to using sprite sheets involves applying CSS classes with pre-calculated coordinates to control the positioning within the sheet. This typically relies on background-image, which does not natively support features like alt text or lazy loading that come built-in with <img> elements. Additionally, using <img> can improve accessibility and semantics depending on how the image is used.

This approach builds upon an article on css-tricks, and expands it by introducing scaling of the individual sprites.

Skip to Codepen Demo

object-fit: none and object-position: x y allow us to select specific areas of an image which is the basis of this idea. The object-position values need to be pre-calculated, just like in the class-based approach. This could be stored as an object literal in a .ts file which would also generate typing for this object by using keyof and typeof.

// Automatically generated file!
export const Pokemon = {
    '0001-bulbasaur': {
        sheet: 'sp1',
        pos: { x: 0, y: 0 }
    },
    '0002-ivysaur': {
        sheet: 'sp1',
        pos: { x: -96, y: 0 }
    },
    '0003-venusaur': {
        sheet: 'sp1',
        pos: { x: -192, y: 0 }
    } // ...
} as const

export type PokemonType = keyof typeof Pokemon
Enter fullscreen mode Exit fullscreen mode

To dynamically set the object-position on the <img> element, a helper function can be used:

export function setObjectPosition(pos: { x: number; y: number }) {
    return `object-position: ${pos.x}px ${pos.y}px;`
}
Enter fullscreen mode Exit fullscreen mode
<!-- Svelte code -->
{#each Object.entries(Pokemon) as [name, data]}
    <div class="container">
        <img
            src={`/spritesheets/${data.sheet}.webp`}
            style={setObjectPosition(data.pos)}
            alt={name}
            loading="lazy"
        />
    </div>
{/each}
Enter fullscreen mode Exit fullscreen mode
.container img {
    width: calc(var(--original-width) * 1px); /* 96 * 1px */
    height: calc(var(--original-height) * 1px); /* 96 * 1px */

    object-fit: none;
    /* object-position: x y; is directly set on the <img> element */
}
Enter fullscreen mode Exit fullscreen mode

This will render a page with each element in the pre-calculated values Object as a separate image, cut out from the sprite sheet.

Image description

In order to change the size of these elements while maintaining the correct container size, we need to know the original size of the original single image before it was added to the sprite sheet. In this example it is 96px * 96px.

If we now want to display a smaller 64px * 64px version of this, we can use transform: scale(). It is important here to also use transform-origin: top left, as it would otherwise scale the image towards the center, leaving unwanted spacing around the image.

.container img {
    width: calc(var(--original-width) * 1px);
    height: calc(var(--original-height) * 1px);

    object-fit: none;
    /* object-position: x y; is directly set on the <img> element */

    transform: scaleX(var(--scale-factor-x)) scaleY(var(--scale-factor-y));
    transform-origin: top left;
}
Enter fullscreen mode Exit fullscreen mode

Image description

This has made the images visually smaller, but the container size is still like before, to fix this we can set width and height on the parent element.

.container {
    max-width: calc(var(--target-width) * 1px);
    max-height: calc(var(--target-height) * 1px);
}
Enter fullscreen mode Exit fullscreen mode

Image description

To use variables for width, height and scale, they must be defined without a unit, because scale needs a unitless value, while width and height need a length value, which can be created by multiplying by 1 length unit e.g. *1 px.

:root {
    --target-width: 64;
    --target-height: 64;
    --original-width: 96;
    --original-height: 96;
    --scale-factor-x: calc(var(--target-width) / var(--original-width));
    --scale-factor-y: calc(var(--target-height) / var(--original-height));
}
Enter fullscreen mode Exit fullscreen mode

Plain HTML, CSS, JS Demo

Conclusion

Benefits:

  • Using sprite sheets reduces the number of HTTP requests and can therefore improve performance, particularly in HTTP/1.1 environments where request overhead is more significant.
  • Sprite sheets also eliminate the "pop-in" effect of individual images, as all images are available immediately after a single request.

Downsides:

  • Generating the sprite sheets and maintaining a synchronized coordinate data structure adds an extra step to the build process.
  • Handling sprite sheets introduces significantly more code complexity compared to using regular images.
  • Sprite sheets are generally less flexible than standalone images.

Alternatives:

  • Depending on the style and use case, an icon font can offer a simpler and more reliable way to reduce requests.
  • HTTP/2 and HTTP/3 multiplexing, combined with good caching strategies, can help mitigate the performance impact of serving many individual images.
  • If sprite sheets are necessary, the traditional CSS class-based approach remains a valid and often simpler alternative.

Top comments (0)