@m-kgl/dnd-pixel-sprites
v0.4.0
Published
20 animated pixel-art chibi sprites of the 10 classic D&D 5e classes (male + female), with an engine-agnostic TypeScript runtime and a React component. Zero runtime dependencies.
Maintainers
Readme
@m-kgl/dnd-pixel-sprites
Animated pixel-art sprites for the 10 classic D&D 5e classes — with typed React components, a character picker, health display, runtime recoloring and zero runtime dependencies.
- 64×64 chibi-style sprites, 6-frame idle animation, male + female variants (20 sprites)
- React components that work out of the box — built-in theme, no classNames wiring needed
- English API — UI texts default to English, optional German via
language="de" - Engine-agnostic vanilla canvas API underneath
- Yarn PnP / Vite / Webpack compatible (assets resolved via
exportsmap +import.meta.url)
Classes
barbarian · bard · cleric · druid · fighter · monk · paladin · ranger · rogue · wizard
Installation
npm install @m-kgl/dnd-pixel-sprites
# or
yarn add @m-kgl/dnd-pixel-sprites
# or
pnpm add @m-kgl/dnd-pixel-spritesReact ≥ 18 is an optional peer dependency — the vanilla API works without it.
Quickstart — React
import { DndSprite, HealthBar, DndCharacterPicker } from "@m-kgl/dnd-pixel-sprites/react";
import "@m-kgl/dnd-pixel-sprites/react/theme.css"; // once, e.g. in main.tsx
// Animated sprite — styled and working immediately
<DndSprite characterClass="wizard" gender="female" scale={4} />
// Health bar
<HealthBar value={13} max={20} editable />
// Full character picker (carousel + gender toggle + colors)
<DndCharacterPicker showColorPicker onConfirm={({ characterClass, gender, colors }) => ...} />Every component is individually usable: import only what you need, the rest is tree-shaken. All components ship with built-in theme classes (dnd-* namespace) — pass unstyled to opt out, or override per element via className / classNames.
Language (i18n)
The code API is always English (props, types, class ids, callbacks). Displayed UI texts default to English and can be switched to German per component:
<DndCharacterPicker language="de" /> // "Barbar", "Auswählen", "männlich", ...
<HealthBar language="de" /> // "LP", aria: "Lebenspunkte: 13 von 20"
<ClassAttributes characterClass="wizard" language="de" /> // "Trefferwürfel", German descriptionsAvailable: language="en" (default) | "de". Dictionaries are exported as UI_TEXTS / getTexts(language) if you need them.
React API (@m-kgl/dnd-pixel-sprites/react)
<DndSprite> — the core component
<DndSprite characterClass="wizard" gender="female" scale={4} />| Prop | Type | Default | Description |
|---|---|---|---|
| characterClass | DndClass | — | Required. One of the 10 class ids |
| gender | "male" \| "female" | "male" | Character gender |
| colors | SpriteColors | — | Custom colors per region (palette swap), e.g. { hair: "#b8442a" } — see Color selection |
| scale | number | 4 | Pixel scaling (1 = native 64×64, 4 = 256×256) |
| flipX | boolean | false | Mirror horizontally |
| alpha | number | 1 | Opacity 0..1 (ghost/stealth effects) |
| paused | boolean | false | Freeze the animation |
| animation | string | "idle" | Animation name (currently only "idle") |
| hp | number | — | Current hit points → drives health effects (tint/animation). Without hp the sprite stays normal |
| maxHp | number | 20 | Maximum hit points (for the state calculation) |
| healthState | HealthState | — | Set the state explicitly (overrides the hp calculation) |
| healthEffects | boolean | true | Health reaction (tint + animation) on/off |
| healthFlash | boolean | true | Short damage/heal flash on hp change |
| showHitbox | boolean | false | Debug: red hitbox overlay |
| background | string \| null | null | Canvas background color (null = transparent) |
| src | string \| URL \| HTMLImageElement | built-in asset | Custom sprite URL (overrides the default PNG) |
| onLoad / onError | callbacks | — | Load lifecycle |
| fallback | ReactElement | — | Rendered while loading |
| hideSkeleton | boolean | false | Disable the loading skeleton |
| smoothTransition | boolean | true | Fade-in on load/class change |
| language | "en" \| "de" | "en" | Language for aria texts |
| className, style, id, aria-label, role, mouse/pointer events | — | — | Pass-through to the <canvas> |
Forwards ref to the <canvas> (e.g. for your own getContext() work).
10 typed convenience components
import { Barbarian, Wizard, Ranger, Paladin } from "@m-kgl/dnd-pixel-sprites/react";
<Barbarian scale={4} />
<Wizard gender="female" paused />
<Ranger flipX scale={6} onClick={...} />
<Paladin showHitbox alpha={0.5} />All exported: Barbarian, Bard, Cleric, Druid, Fighter, Monk, Paladin, Ranger, Rogue, Wizard. Props identical to <DndSprite> minus characterClass.
Sprite reacts to health
Give the sprite hp (and optionally maxHp, default 20) — image and idle animation adapt automatically across 5 states:
const [hp, setHp] = useState(20);
<DndSprite characterClass="fighter" hp={hp} maxHp={20} scale={4} />
<HealthBar value={hp} max={20} onChange={setHp} editable />| State | HP (of 20) | Look | Animation |
|---|---|---|---|
| full | 15–20 | normal | normal idle |
| healthy | 10–14 | normal | normal idle |
| wounded | 5–9 | slightly desaturated + red shimmer | a bit slower |
| critical | 3–4 | red tint, darker | slow + occasional wince |
| dying | 0–2 | grayscale, faded, red vignette | very slow + slumping |
- Damage/heal (hp decreases/increases) trigger a short flash + shake or heal bounce → disable with
healthFlash={false}. - Disable everything with
healthEffects={false}, or pin a state withhealthState="dying". - The tint (filter/opacity) is applied inline — works even without
theme.css. The motion animations (wince/slump/flash) come fromtheme.cssand respectprefers-reduced-motion.
Color selection
Every character can be recolored via palette swap — pixel-perfect, shading preserved, outline and ground shadow untouched:
<DndSprite
characterClass="wizard"
gender="female"
colors={{ hair: "#b8442a", skin: "#8d5524", outfit: "#2e7a3e" }}
/>8 color slots (SpriteColors): skin, hair, outfit, outfitAccent, metal, weapon, weaponAccent, accent. Unset slots keep the class's original colors. A color change is just a canvas repaint — no PNG reload.
<ColorPicker> — ready-made picker UI (swatches per slot + free color + reset):
import { ColorPicker, DndSprite, type SpriteColors } from "@m-kgl/dnd-pixel-sprites/react";
const [colors, setColors] = useState<SpriteColors>({});
<DndSprite characterClass="fighter" colors={colors} />
<ColorPicker colors={colors} onChange={setColors} slots={["skin", "hair", "outfit"]} />| Prop | Type | Default | Description |
|---|---|---|---|
| colors / defaultColors | SpriteColors | — | Controlled / uncontrolled |
| onChange | (colors) => void | — | Provides the complete colors object |
| slots | ColorSlot[] | skin, hair, outfit, outfitAccent | Which rows appear |
| presets | Record<ColorSlot, string[]> | curated palettes | Custom swatches per slot |
| labels | Record<ColorSlot, string> | localized | Custom labels |
| allowCustomColor | boolean | true | <input type="color"> for free colors |
| showReset | boolean | true | "Auto" per row + "Reset" for all |
| language | "en" \| "de" | "en" | Label language |
| unstyled | boolean | false | Drop built-in theme classes |
Inside <DndCharacterPicker> the color picker is integrated (showColorPicker); the chosen colors come back in onChange/onConfirm:
<DndCharacterPicker
showColorPicker
colorSlots={["skin", "hair", "outfit"]}
onConfirm={({ characterClass, gender, colors }) => save(characterClass, gender, colors)}
/>Vanilla (no React): sprite.setColors({ hair: "#b8442a" }) / sprite.getColors(), or directly createSprite("wizard", { colors: {...} }). Low level: recolorSprite(image, meta.colorSlots, colors).
Note: slots that aren't visible on a sprite change nothing — e.g. the fighter wears a helmet, so
hairhas no effect there. If two slots share the same original color (e.g. gold accents on the cleric), the last one set wins.
<HealthBar> — hit point display
<HealthBar value={13} max={20} onChange={setHp} editable />5 states (auto, based on %): full ≥75% · healthy ≥50% · wounded ≥25% · critical >10% · dying ≤10% — exported as getHealthState(value, max).
3 variants:
<HealthBar variant="bar" value={13} max={20} /> {/* default — modern gradient bar */}
<HealthBar variant="hearts" value={13} max={20} /> {/* pixel-art hearts */}
<HealthBar variant="pips" value={13} max={20} /> {/* compact dots (HUD-friendly) */}Built-in UX (all automatic): damage/heal flash, ghost fill (briefly shows lost HP), floating "-3"/"+5" popup, critical pulse, dying shake, rapid-click accumulation, role="meter" accessibility.
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue | number | — / max | Controlled / uncontrolled |
| max / min | number | 20 / 0 | Range |
| onChange | (value) => void | — | Fired on +/- |
| variant | "bar" \| "hearts" \| "pips" | "bar" | Display form |
| editable | boolean | false | Show +/- controls |
| showValue | boolean | true | Show "13 / 20" |
| label | string \| false | localized "HP" | Header label |
| animations | boolean | true | Flash/pulse/shake on/off |
| size | "sm" \| "md" \| "lg" | "md" | Size |
| language | "en" \| "de" | "en" | Label/aria language |
| unstyled | boolean | false | Drop built-in theme classes |
| renderSymbol | (props) => ReactNode | — | Custom heart/pip shape |
<DndCharacterPicker> — carousel + gender toggle + colors
<DndCharacterPicker
onChange={({ characterClass, gender, colors }) => ...} // every change
onConfirm={(selection) => ...} // confirm button
showConfirmButton
showColorPicker
/>| Prop | Type | Default | Description |
|---|---|---|---|
| initialClass / initialGender | — | first / "male" | Uncontrolled start |
| characterClass / gender | — | — | Controlled mode |
| onClassChange / onGenderChange / onColorsChange | callbacks | — | Granular listeners |
| onChange | (SelectionResult) => void | — | Combined listener |
| onConfirm | (SelectionResult) => void | — | With showConfirmButton |
| classes | DndClass[] | all 10 | Subset/order |
| showSlider | boolean | false | Range slider |
| showDots | boolean | true | Dots indicator |
| showNavButtons | boolean | true | ‹ › buttons |
| showGenderButton | boolean | true | ♂/♀ toggle |
| showConfirmButton | boolean | false | Confirm button |
| showColorPicker | boolean | false | Integrated color picker |
| colorSlots | ColorSlot[] | 4 defaults | Slots in the color picker |
| colors / defaultColors | SpriteColors | — | Controlled / uncontrolled colors |
| hideTitle / hidePosition / showKeyboardHint / swipeEnabled | boolean | — | UI toggles |
| hideAttributes / onlyAttributeFields / hiddenAttributeFields / attributesOverride | — | — | Attributes section |
| renderSprite / renderGenderButton | render props | — | Custom rendering |
| language | "en" \| "de" | "en" | UI language |
| unstyled | boolean | false | Drop built-in theme classes |
Keyboard: ← → switch class, G toggles gender. Touch: swipe left/right. SelectionResult = { characterClass, gender, colors? }.
<DndSpriteCard> — sprite + attributes (+ HealthBar)
<DndSpriteCard
characterClass="wizard"
gender="female"
scale={3}
onlyAttributeFields={["hitDie", "primaryAbility", "specialResource"]}
attributesOverride={{ displayName: "Mira the Wise" }}
/>With integrated health — hp feeds the sprite (tint/animation react) and healthBar renders the bar below; "editable" adds +/-:
const [hp, setHp] = useState(20);
<DndSpriteCard
characterClass="fighter"
hp={hp}
maxHp={20}
healthBar="editable" // true = display only
onHpChange={setHp}
hideAttributes
/>Additional props: colors, spriteProps, healthBarProps (e.g. { variant: "hearts" }), hideTitle, hideSubtitle, hideDescription, renderTitle, renderSubtitle, language, unstyled, classNames.
<ClassAttributes> — stats block (no sprite)
<ClassAttributes
characterClass="wizard"
onlyFields={["hitDie", "primaryAbility", "abilities"]}
attributes={{ specialResource: { name: "Spell slots", max: 4, value: 2 } }}
/>Fields: hitDie, primaryAbility, savingThrows, armor, weapons, spellcasting, specialResource, abilities, tags, description. Data access without rendering: getClassAttributes("wizard", "en" | "de") / DEFAULT_CLASS_ATTRIBUTES.
Hook: useDndSprite()
const { sprite, loaded, error } = useDndSprite({ characterClass: "wizard", gender: "female", colors });
// plus useSpriteAnimationLoop({ sprite, canvas, scale, speed, ... }) for your own canvasStyling
All components ship with default classes in the dnd-* namespace, styled by theme.css:
import "@m-kgl/dnd-pixel-sprites/react/theme.css";Three levels of customization:
- CSS variables (global theming):
:root { --dnd-bg: #20202a; --dnd-accent: #4f7cff; --dnd-radius: 12px; --dnd-font: system-ui, sans-serif; /* HP colors: --dnd-hp-* */ } className/classNamesper component — your classes are appended to the defaults (extend/override specific rules).unstyled— drops all built-in classes; only yourclassNamesapply (e.g. for Tailwind).
State hooks for your own CSS: data-state (HealthBar), data-health + data-hit (sprite), data-active (swatches/symbols), data-variant, data-size, data-slot.
Mobile-ready out of the box: 44px touch targets, swipe gestures, responsive breakpoints (≤640px, ≤360px), prefers-reduced-motion respected.
Vanilla / engine-agnostic API
import { createSprite } from "@m-kgl/dnd-pixel-sprites";
const wizard = createSprite("wizard", { gender: "female", colors: { hair: "#b8442a" } });
await wizard.load();
const ctx = canvas.getContext("2d")!;
let last = performance.now();
requestAnimationFrame(function tick(now) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
wizard.animate(ctx, 128, 230, now - last, { scale: 4 });
last = now;
requestAnimationFrame(tick);
});createSprite(characterClass, { gender?, colors? })/createAllSprites(gender?)- Class constructors:
WizardSprite,FighterSprite, ... (new WizardSprite("female")) DndSpritemethods:load(src?),setAnimation(name),update(dt),draw(ctx, x, y, opts),animate(...),getHitbox(x, y, scale),setColors(colors),getColors(),isLoaded()- Recolor utilities:
recolorSprite(image, colorSlots, colors),createColorMap(...),parseHexColor(hex) - Constants:
DND_CLASSES,GENDERS,CLASS_DISPLAY_NAMES,CLASS_DISPLAY_NAMES_DE,SPRITE_URLS,SPRITE_META - Custom sprite source:
wizard.load(myUrl)or asset imports:import wizardPng from "@m-kgl/dnd-pixel-sprites/sprites/wizard-female.png"; import wizardMeta from "@m-kgl/dnd-pixel-sprites/sprites/wizard-female.json";
Asset format
Per class + gender: sprites/<class>-<gender>.png (384×64 sheet, 6 frames @ 64×64, 8 fps) + <class>-<gender>.json metadata:
{
"id": "wizard-female",
"className": "wizard",
"gender": "female",
"frameWidth": 64, "frameHeight": 64, "frameCount": 6, "fps": 8,
"animations": { "idle": { "from": 0, "to": 5, "fps": 8 } },
"anchor": { "x": 32, "y": 60 },
"hitbox": { "x": 22, "y": 24, "w": 20, "h": 34 },
"palette": ["#f0d2af", "..."],
"colorSlots": { "skin": ["#f0d2af", "#c3a078"], "hair": ["..."], "...": ["..."] },
"tags": ["dnd5e", "class", "spellcaster", "female"]
}sprites/manifest.json lists all 20 sprites. All pixels are fully opaque (no anti-aliasing) except the semi-transparent ground shadow — that's what makes the palette swap exact.
Regenerating sprites
The sprites are generated programmatically (no binary editing needed):
npm run generate # writes sprites/*.png + *.json + manifest.json
npm run verify # sanity check: dimensions, animation, metadata
npm run build # tsup → dist/ (ESM + CJS + .d.ts)Generator source lives in scripts/ (palettes, body parts, per-class composition) and is not shipped in the package.
Demos
npm run demo
# → http://localhost:5234 vanilla canvas grid (all 20 sprites)
# → /react.html <DndSprite> playground
# → /picker.html picker + card + language switch (EN/DE)
# → /health.html HealthBar (all variants/states)
# → /sprite-health.html sprite reacting to HP
# → /colors.html color picker + palette swapGender differences
Female variants are not recolors — they have distinct geometry: narrower V-shaped head with eyelashes and lips, slimmer neck and arms, waisted torso, wider hips, class-specific outfit cuts (waist + belt accents) and hairstyles (e.g. ponytail). Male variants have broader heads, square jaws, straight torsos and beards on several classes.
Migration from 0.3.x (German API)
The whole API switched to English in 0.4.0. Mapping (old → new):
| 0.3.x (German) | 0.4.0 (English) |
|---|---|
| klasse / DndKlasse / "barbar", "kaempfer", "magier", ... | characterClass / DndClass / "barbarian", "fighter", "wizard", ... |
| geschlecht / "maennlich" / "weiblich" | gender / "male" / "female" |
| farben / SpriteFarben / FarbAuswahl | colors / SpriteColors / ColorPicker |
| Slots haut, haare, metall, ... | skin, hair, metal, ... |
| zustand / HealthZustand / "voll"..."sterbend" | healthState / HealthState / "full"..."dying" |
| KlassenAttribute / attributeDaten | ClassAttributes / attributeData |
| sliderAnzeigen, dotsAnzeigen, bestaetigenButtonAnzeigen, ... | showSlider, showDots, showConfirmButton, ... |
| AuswahlErgebnis / onAuswahl | SelectionResult / onConfirm |
| setFarben() / faerbeSpriteUm() | setColors() / recolorSprite() |
| Asset ids barbar-maennlich.png | barbarian-male.png |
| CSS .dnd-hp__fuellung, .dnd-farben, data-zustand, ... | .dnd-hp__fill, .dnd-colors, data-state, ... |
Also new in 0.4.0: components are styled by default (previously you had to wire classNames manually) — pass unstyled to get the old unstyled behavior, and language="de" for German UI texts.
Architecture
src/
├── index.ts # vanilla entry
├── DndSprite.ts # abstract sprite class (load/update/draw/recolor)
├── registry.ts # DND_CLASSES, createSprite()
├── recolor.ts # palette swap engine
├── assets.ts # PNG/JSON references (import.meta.url)
├── classes/ # 10 thin subclasses
└── react/
├── index.ts # react entry
├── DndSprite.tsx # canvas component (+ health effects)
├── classes.tsx # 10 typed components
├── DndCharacterPicker.tsx
├── DndSpriteCard.tsx
├── HealthBar.tsx
├── ColorPicker.tsx
├── ClassAttributes.tsx
├── attributeData.ts # 5e class data (EN + DE)
├── i18n.ts # UI text dictionaries
├── useDndSprite.ts # hooks
└── theme.css # default theme (dnd-* namespace)License
MIT
