Skip to content

CSS Sprite Generator — Pixel-Sharp Sprite Sheet and Classes

For web games and sites: one image request instead of dozens. Each frame becomes a CSS class with its background position and size; the preview page shows them all, pixel-sharp.

A CSS sprite is one image that holds many icons or frames; each element shows its piece by being exactly that piece's size and moving the sheet with background-position set to minus the piece's position. Nerulio packs your images into one PNG and writes a .css with one class per image plus a preview .html. Trim and rotation stay off, because an element's box cannot restore a trim offset or turn a piece back, and image-rendering: pixelated keeps pixel art sharp when scaled. Every class was drawn in Chromium and compared.

Drop frame images, a GIF or an .aseprite file to pack
Runs in your browser. Files are not uploaded.
Studio Pack & Export: the packed atlas page with every frame outlined, the used area and the export targets
The real Studio with a CC0 asset (OpenGameArt).
Exports to
  • CSS spritesVerified· Chromium
  • Godot 4Verified· Godot 4.7.2
  • Unity 6Verified· Unity 6000.5.3f1
  • Phaser 3 / 4Verified· Phaser 3.90 + 4.2
  • PixiJS 8Verified· PixiJS 8.21
  • Aseprite JSONVerified· Phaser load.aseprite · PixiJS 8.21
  • .asepriteVerified· Aseprite 1.3.18
  • Spine / libGDXVerified· spine-canvas 4.2
  • Starling / Sparrow XMLVerified· Phaser 4.2
  • LÖVEVerified· LÖVE 11.5
  • DefoldBuilt· Defold bob.jar 1.13.1
  • GameMakerUNVERIFIED
  • GIF / APNGDecoded· Pillow

How a CSS sprite shows one piece

The element is exactly as large as one piece, and the sheet is its background, shifted left and up so the piece lands inside the box: a piece at x = 42, y = 31 needs background-position: -42px -31px. The rest of the sheet lies outside the box and is not painted.

Because the box is the whole visible area, a trimmed piece would lose its transparent margin and shift, and a rotated piece cannot be turned back with background properties alone. The CSS preset therefore packs full, upright images, with 2 px of padding so that scaling does not reach into a neighbour.

To draw a sprite larger, scale all of its numbers together (width, height, background-size, background-position), or scale the element as the preview page does with zoom: 4. image-rendering: pixelated tells the browser to scale by nearest neighbour to whole multiples instead of smoothing.

background-position
Offset of the sheet inside the element: the negative position of the piece.
background-size
The sheet's size in CSS pixels; written per page, so scaling stays predictable.
image-rendering: pixelated
Nearest-neighbour scaling for pixel art; it also applies to background images.

Example: six 40 × 29 frames as CSS classes

run.png 82 × 91 (6 frames of 40 × 29, trim off, padding 2)

.sprite{display:inline-block;background-repeat:no-repeat;image-rendering:pixelated;}
.sprite-page-0{background-image:url("run.png");background-size:82px 91px;}
.sprite-run_3{width:40px;height:29px;background-position:-42px -31px;}

HTML element: class="sprite sprite-page-0 sprite-run_3"

the same frame at 2× without zoom:
  width 80px   height 58px   background-size 164px 182px   background-position -84px -62px

run_3 starts 42 px from the left (40 px of run_2 plus 2 px padding) and 31 px from the top (29 px of the first row plus 2 px), so those are its negative offsets.

What it does

  • Sprite sheet PNG plus a .css with one class per frame.
  • A preview .html that draws every class from the shipped stylesheet.
  • image-rendering: pixelated for crisp pixel art at any zoom.
  • Trim and rotation are off for CSS (an element box cannot hold a trim offset); checked in Chromium class by class.

How to do it

  1. Drop the images or frames.
  2. In Pack & Export choose CSS sprites.
  3. Open the preview page to see every class.
  4. Link the .css and use the classes on your elements.

Use it on a page

  1. Copy run.png and run.css to your site and link run.css from the page head with a stylesheet link element.
  2. Give each element three classes: sprite, the page class such as sprite-page-0, and the image class such as sprite-run_3.
  3. For a frame animation, switch the image class from JavaScript on a timer, one class per frame.
  4. When a sprite is a button icon, give the button an accessible label; the sprite element itself has no text.

Check the result

  • Open the exported run.html: every class is drawn at 4× with its name below it.
  • Zoom the browser: with image-rendering: pixelated the edges stay square; a sliver of a neighbour at the border means background-size was changed without the positions, or padding was set to 0.

Troubleshooting

SymptomLikely causeHow to checkFix
A piece shows part of its neighbourThe element is larger than the piece, or background-size changed without the positionsCompare the element's width and height with its classScale every number by the same factor, or scale the element with zoom or transform
Pixel art looks blurryimage-rendering is not applied because the element lacks the sprite classDevTools: the computed image-renderingAdd the sprite class
Nothing showsThe image path is wrong: url("run.png") is resolved relative to the CSS fileThe network panel shows a 404 for run.pngKeep the PNG next to the CSS, or edit the url
A frame shows a piece of another frameThe element has sprite-page-0 but its frame is on page 1The page class used for that frame in the preview HTMLUse the page class the preview gives for that frame

Other ways to do it

OptionWhen it is the better choice
Separate PNG files, or SVG for iconsA few images that change often are simpler to update as separate files; vector icons belong in SVG, not in a PNG sprite.
A game engine atlasFor a canvas or WebGL game, load a JSON atlas instead: Phaser or PixiJS.
Upscale the art firstIf the pixel art must appear at a fixed larger size without CSS scaling, upscale it by a whole factor before packing.

Engine exports and how each was checked

"Verified" means the exported files were loaded by the real engine or tool on real CC0 assets and the result was compared with the source. Anything that was not loaded is labelled as such.

TargetFilesCheck
Godot 4.tres SpriteFrames · .tscn AnimatedSprite2D · .png.importVerified Godot 4.7.2Loaded by Godot; every animation frame drawn and compared with the source file. The scene uses Nearest filtering; rotation is never used.
Unity 6.unity.json · Editor/NerulioSpriteImporter.cs · AnimationClipVerified Unity 6000.5.3f1Batch mode: sprite rects, pivots, pixels, clip keys and durations checked.
Phaser 3 / 4atlas JSON (multiatlas) · .anims.jsonVerified Phaser 3.90 + 4.2Frames and animations drawn by both versions.
PixiJS 8spritesheet JSON · animations · anchor · meta.scaleVerified PixiJS 8.21Rotated and multi-page atlases included.
Aseprite JSONhash / array · frameTags · slicesVerified Phaser load.aseprite · PixiJS 8.21Tags keep their direction and repeat count.
.asepritelayers · tags · durations · slicesVerified Aseprite 1.3.18Opened and rendered by the Aseprite command line.
Spine / libGDX.atlasVerified spine-canvas 4.2Every region drawn by the Spine runtime, rotated and trimmed; libGDX itself was not run.
Starling / Sparrow XMLXML per pageVerified Phaser 4.2Phaser 3.90 draws trimmed XML frames wrongly (its own bug); the "Sparrow XML (Phaser 3)" preset turns trim off and passes in 3.90 and 4.2.
LÖVELua quads · nerulio_atlas.lua · main.luaVerified LÖVE 11.5Every frame drawn and every animation stepped through the shipped helper.
Defold.atlas · .tilesourceBuilt Defold bob.jar 1.13.1Built by Defold's command-line builder; the built texture's UVs were read back.
GameMakerspr_<name>_stripN.png · gamemaker.jsonUNVERIFIEDNot loaded in GameMaker (no headless GameMaker). The strips are cut the way its strip importer cuts them.
CSS sprites.css · preview .htmlVerified ChromiumEvery class drawn from the shipped stylesheet.
GIF / APNGone file per animationDecoded PillowEvery frame and delay decoded again after export; APNG is exact.

Evidence

Exports: 49 runs in the real engines on real CC0 assets, 47 pass — the 2 failures are Phaser 3.90's trimmed-XML bug, which the Phaser 3 preset avoids. Packer head-to-head on 5 real frame sets (CodeAndWeb free web packer, GAPTools, free-tex-packer): smallest sheet on all 5 with rotation allowed, 4 of 5 without; every frame restored pixel-exact.

Limits

  • Polygon (mesh) packing is not offered: none of the verified engines draws a mesh atlas from exported data.
  • GameMaker strips are UNVERIFIED in GameMaker itself.
  • Scale variants (@0.5x–@4x) use nearest-neighbour only.
  • Trimmed Starling XML is drawn wrongly by Phaser 3.90; use the "Sparrow XML (Phaser 3)" preset there.

Versions and sources

Chromium (Playwright) linked the exported stylesheet as is, drew one element per .sprite-… rule and the screenshot was compared with the source images. Other browsers were not part of that run; image-rendering and background-position behave as described by MDN.

Official documentation used

Questions

Why is trimming turned off?

A CSS element box cannot carry a trim offset, so trimmed frames would shift; the CSS target always keeps full frames.

Which browsers were checked?

Every class was drawn in Chromium from the shipped stylesheet and compared with a screenshot.

Is it free, and do I need an account?

Everything on these pages is free and works without an account. There is no watermark and no export that is held back.

Are my sprites uploaded?

No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.

Does it need Aseprite or an engine plugin installed?

No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).