Skip to main content

Sprite

A pixel character built from swappable parts, that walks, sits and works.

1'use client';
2
3import { Sprite } from '@/components/ui/sprite';
4
5export function Default() {
6 return <Sprite seed="nachui" size={72} />;
7}
Every sprite is five slots — skin, hair, eyes, outfit, accessory — drawn on one fixed skeleton: the face box, the eye line, the shoulders and the hip. Because the skeleton never moves, any combination of parts inherits the animations for free.

Installation

pnpm dlx nachui add sprite
Installing by hand works the same as any other component: copy the file from the Manual tab and adjust the import paths. The animations travel inside the file as SPRITE_CSS, so nothing else needs copying — either render with withStyles or inject the string once:
1<style>{SPRITE_CSS}</style>

Anatomy

1import { Sprite } from '@/components/ui/sprite';
1<Sprite seed="nachui" />

Seeds

Any string builds a sprite, and the same string always builds the same one — so a username or an email works as an avatar with nothing stored.
nachui
figueroaignacio
mate
bit
vera
lupe
1'use client';
2
3import { Sprite } from '@/components/ui/sprite';
4
5const seeds = ['nachui', 'figueroaignacio', 'mate', 'bit', 'vera', 'lupe'];
6
7export function Seeds() {
8 return (
9 <div className="flex flex-wrap items-end gap-8">
10 {seeds.map((seed) => (
11 <div key={seed} className="flex flex-col items-center gap-3">
12 <Sprite seed={seed} size={64} />
13 <span className="text-muted-foreground font-mono text-xs">{seed}</span>
14 </div>
15 ))}
16 </div>
17 );
18}

States

idle breathes and blinks, walk runs the two-frame walk cycle, work opens a laptop, and loop plays the whole thing: walk in, sit down, work, get up.
idle
walk
work
loop
1'use client';
2
3import { Sprite } from '@/components/ui/sprite';
4
5const states = [
6 { state: 'idle', label: 'idle' },
7 { state: 'walk', label: 'walk' },
8 { state: 'work', label: 'work' },
9 { state: 'loop', label: 'loop' },
10] as const;
11
12export function States() {
13 return (
14 <div className="flex flex-wrap items-end gap-8">
15 {states.map(({ state, label }) => (
16 <div key={state} className="flex flex-col items-center gap-3">
17 <Sprite seed="nachui" state={state} size={72} />
18 <span className="text-muted-foreground font-mono text-xs">{label}</span>
19 </div>
20 ))}
21 </div>
22 );
23}

Parts

Pass parts to override whatever the seed picked, slot by slot. Colour is a separate axis from shape, so six hair shapes and five colours are thirty heads.
1'use client';
2
3import { Sprite } from '@/components/ui/sprite';
4
5export function Parts() {
6 return (
7 <div className="flex flex-wrap items-end gap-8">
8 <Sprite
9 seed="nachui"
10 size={64}
11 parts={{ hair: 'bun', hairColor: 'copper', eyes: 'wide', accessory: 'glasses' }}
12 />
13 <Sprite
14 seed="nachui"
15 size={64}
16 parts={{ skin: 'umber', outfit: 'overalls', outfitMain: '#4E6E58', accessory: 'none' }}
17 />
18 <Sprite
19 seed="nachui"
20 size={64}
21 parts={{ eyes: 'visor', eyeFill: '#A8E6D7', hair: 'buzz', hairColor: 'ash' }}
22 />
23 </div>
24 );
25}

Standalone

spriteToSvg returns the whole sprite as one SVG string, animations included, so it works anywhere a page's stylesheet cannot reach — a server response, a saved file, a README. On this site every sprite is also a URL:
1/api/sprite/{seed}.svg?state=loop&size=96
The same seed always answers with the same image, so the response is cached as immutable.

Props

PropTypeDefaultDescription
seedstringnachuiAny string. The same string builds the same sprite.
partsPartial<SpriteParts>Overrides the seed, slot by slot.
statebust | idle | walk | work | loopidleWhat it is doing.
sizenumber48Width in pixels; height follows the grid.

Accessibility

The sprite is decorative and carries no label of its own. When it stands for a person, put the name next to it in text rather than inside the drawing.
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue
Ctrl+I