astro-sprites
v0.1.1
Published
Build-time sprite atlas generation for instant Astro UI states.
Maintainers
Readme
astro-sprites
Build-time sprite sheets for Astro interfaces where many finite UI states need to feel instant.
astro-sprites takes a known set of images, packs them into content-hashed WebP atlas sheets during the Astro build, emits a typed manifest, and lets the browser select frames by stable item ID. Runtime work stays tiny: item lookup, sheet URL, frame coordinates, CSS transform.
It is not an Astro Image replacement, not a runtime image optimizer, and not a hover plugin. The primitive is:
stable item ID -> generated sheet -> frame coordinates -> UI stateInstall
Use the package manager your Astro project already uses:
npm install astro-spritespnpm add astro-spritesyarn add astro-spritesbun add astro-spritesIf you use the Cloudflare R2/S3-compatible output adapter, also install the AWS SDK:
npm install @aws-sdk/client-s3
pnpm add @aws-sdk/client-s3
yarn add @aws-sdk/client-s3
bun add @aws-sdk/client-s3Basic Astro Setup
import { defineConfig } from 'astro/config';
import sprites, { sprite } from 'astro-sprites';
import { files } from 'astro-sprites/adapters/source';
export default defineConfig({
integrations: [
sprites({
collections: {
projects: sprite({
source: files('./src/assets/work/*.webp'),
frame: { width: 400, height: 300, fit: 'cover' },
layout: { strategy: 'grid', columns: 'auto' },
}),
},
}),
],
});Then render the selected sheet and let the runtime update coordinates:
---
import SpriteFrame from 'astro-sprites/components/SpriteFrame.astro';
import projects from 'virtual:astro-sprites/projects';
const first = projects.first();
---
<section data-sprite-root>
{projects.itemsList.map((project) => (
<button type="button" {...projects.trigger(project)}>{project.title}</button>
))}
{first && <SpriteFrame manifest={projects} item={first} />}
</section>
<script>
import projects from 'virtual:astro-sprites/projects';
import { mountSpriteSelector } from 'astro-sprites/runtime';
const root = document.querySelector('[data-sprite-root]');
if (root) {
mountSpriteSelector(root, {
manifest: projects,
trigger: 'hover-focus-click',
initialItem: projects.first(),
});
}
</script>CSS Contract
The frame owns the crop window. The image stays in atlas pixel space.
[data-sprite-frame] {
aspect-ratio: var(--sprite-aspect-ratio);
width: min(var(--sprite-frame-width), 100%);
overflow: hidden;
}
[data-sprite-image] {
display: block;
max-width: none;
width: calc(var(--sprite-sheet-width) / var(--sprite-dpr, 1) * 1px);
height: calc(var(--sprite-sheet-height) / var(--sprite-dpr, 1) * 1px);
transform: translate(
calc(var(--sprite-x, 0) * -1px),
calc(var(--sprite-y, 0) * -1px)
);
}Do not squeeze the crop frame without also scaling the atlas image and coordinates. If a parent grid is narrower than the compiled frame, reserve space or use a deliberate scaled recipe. Random responsive shrinking causes gaps, clipped frames, and false compositor bugs.
For baseDpr atlases, the manifest emits logical item coordinates and includes frame.dpr. The runtime and SpriteFrame.astro render the sheet image at sheet.width / frame.dpr by sheet.height / frame.dpr; do the same if you hand-write the DOM.
Use Cases
Good fits are finite visual state systems:
- Work indexes where hover/focus changes a large preview instantly.
- CMS-driven editorial rails with stable item IDs.
- Product swatches, material finishes, trim selectors, and small configurators.
- Character rosters with one portrait atlas and one thumbnail atlas.
- Icon/state atlases for dashboards and status-heavy tools.
- Documentation demos where selected examples need screenshots.
- Cursor trails or custom interactions that reuse one generated sheet.
- Transparent or irregular generated assets where alpha and crop rules must be tested.
Bad fits:
- Article hero images.
- Open-ended user uploads.
- Huge unbounded galleries.
- Runtime image transformation.
- General responsive image delivery.
Use Astro Image, Cloudflare Images, Imgix, or your normal image service for those.
Source Models
The public API accepts simple sprite inputs and normalizes them into internal SpriteItem[] records at build time. Where that list comes from is your architecture choice.
Collection keys are the public collection IDs. In other words, collections.projects
becomes virtual:astro-sprites/projects; do not repeat id: 'projects' unless you
are using the older array form.
type SpriteInputItem =
| string
| URL
| {
src: string | URL;
id?: string;
name?: string;
title?: string;
cacheKey?: string;
alt?: string;
href?: string;
};The dumb-interface path is:
const projects = [
'./src/assets/work/north-observatory.webp',
{
src: './src/assets/work/courtyard-house.webp',
name: 'Courtyard House',
href: '/work/courtyard-house',
},
];The compiler infers id from src, uses name/title as display metadata, and defaults cacheKey to the source URL/path. Add explicit id only when the generated ID is not the contract you want, and explicit cacheKey when a CMS/CDN can change pixels while keeping the same URL.
The normalized internal shape remains:
type SpriteItem = {
id: string;
title?: string;
href?: string;
image: {
src: string | URL;
cacheKey: string;
alt?: string;
};
};Supported source patterns:
files()for local image globs where filenames can be the source of truth.- Plain arrays for local files.
- Async loaders for JSON, databases, or internal services.
defineSource()for custom build-time loaders.mapSource()andcmsSource()for CMS-shaped records.dataAttributeSource()for static Astro/HTML markup scanning.
The important rule: normalize CMS, CDN, R2, Bunny, S3, or database concepts at build time. Do not leak provider-specific data models into UI components.
Delivery: Astro, Cloudflare, CDNs, CMS, or No CDN
astro-sprites is Astro and build-time first.
Think in layers:
source records -> Astro build -> generated sheets -> origin -> optional CDN- No CDN: write sheets to
public/sprites/**and deploy them with the site. - Cloudflare Pages: same static output model, deployed with the Astro artifact.
- Cloudflare R2: upload generated sheets to object storage and serve them from a custom domain.
- Cloudflare Worker: serve R2 or another origin, but do not compile sprites per request.
- Bunny / S3 / Fastly / CloudFront / KeyCDN: upload hashed sheets to the provider's storage/origin and point
publicBaseUrlat the CDN URL. - CMS: use the CMS as a source adapter. The output can still be local, R2, Bunny, S3, or any other adapter.
- Fastify/custom Node server: serve generated files as an origin. Fastify is not the CDN and not the compiler.
See the provider guide in docs/src/content/docs/docs/delivery.md.
Compared With astro-sprite
astro-sprite is an existing Astro package for small icon sprite generation. It scans a folder, creates one sprite image, writes CSS classes, and can emit a preload component.
astro-sprites is positioned differently: it emits typed atlas manifests for interactive UI state. The runtime selects frames by item ID, swaps sheets safely, supports source-image fallback, handles DPR-aware coordinates, and accepts source adapters for CMS/CDN/local records.
Run the local comparison:
npm run benchmark:astro-spriteThe benchmark installs [email protected] into .temp, generates identical PNG inputs, and measures the common path both packages share: finite image packing into a WebP sheet. On the local Windows dev machine, the current result is astro-sprites at 32.0ms vs astro-sprite at 179.4ms for 16 small icons, and 163.9ms vs 250.2ms for 64 medium thumbnails. Treat benchmark numbers as machine-local; treat the benchmark command as the contract.
Edge Cases
The demo surface covers cases that break lazy atlas implementations:
- Transparent triangles and circles: alpha must survive packing.
- Irregular shapes: padding must be deliberate.
- Intentional crops: crop metadata belongs in source config, not hand-written CSS.
- Multi-sheet output: runtime must swap sheet URL and coordinates together.
- Missing sheet at the edge: fallback can recover with the selected source image.
- CMS URLs changing content under the same URL: add
cacheKeyupdate metadata.
Runtime Fallback
Runtime fallback is opt-in recovery for a generated sheet that fails after deployment.
mountSpriteSelector(root, {
manifest,
trigger: 'click',
fallback: 'source-image',
initialItem: manifest.item('north-observatory'),
});The selector first tries the sheet. If that sheet fails, it uses manifest.items[itemId].source.src for the active item and emits astro-sprites:fallback.
Development
This repository currently uses npm for contributor installs and CI because package-lock.json is committed.
npm ci
npm run typecheck
npm run lint
npm run test
npm auditDocs dev server:
npm --prefix docs install
npm run docs:devPackage dry-run without rebuilding:
npm pack --dry-run --json --ignore-scriptsFull package validation, including build and package smoke test, runs in CI and in the release workflow. Locally, do not treat a stale dist/ folder as proof that the package is publishable.
CI/CD and Release Ceremony
Release should be boring:
- Make sure
mainis clean. - Confirm docs and package metadata are accurate.
- Run local non-build checks:
npm run typecheck npm run lint npm run test npm audit npm pack --dry-run --json --ignore-scripts - Commit with a conventional commit.
- Push and wait for CI.
- Create an annotated tag:
git tag -a v0.1.0 -m "release: v0.1.0" git push origin v0.1.0 - Create the GitHub release from the tag.
- Run the
Releaseworkflow with that tag, or publish with provenance from a clean checkout. - Verify npm:
npm view astro-sprites version npm view astro-sprites dist-tags
The release workflow builds, tests, smoke-tests the package, and publishes with npm provenance. That is the right place for build work.
Package Exports
import sprites from 'astro-sprites';
import { mountSpriteSelector } from 'astro-sprites/runtime';
import { cmsSource, defineSource, files, mapSource } from 'astro-sprites/adapters/source';Astro component:
---
import SpriteFrame from 'astro-sprites/components/SpriteFrame.astro';
---Stability
astro-sprites targets Astro 6 and Node 22.12.0+.
The contracts to protect are:
- manifest v1 shape
- stable item ID selection
- output adapter contract
- runtime selector behavior
- CSS frame/image coordinate contract
License
MIT
