Pixel Potion
Pixel art drawn from a string map — edit the letters, change the sprite.
← you, in 35 minutes.
what you'll build
Here's the plan
A potion bottle built from a single string array — each letter in the map becomes one colored pixel. Edit a letter, redraw the sprite. HTML, CSS, and a few lines of JavaScript.
You’ll learn
- Drawing pixel art from a data array instead of hand-placing every pixel
- Using CSS grid (grid-template-columns + grid-auto-rows) as a pixel canvas
- image-rendering: pixelated to keep small sprites crisp instead of blurry
- Layering color in passes — outline first, fills second — the way a real sketch works
- Skill level
- Intermediate
- Time
- ~35 min
- Tools
- Just a browser
- Code type
- HTML + CSS + JS
- Lines
- ~48
the steps
Build it, one change at a time
Set up the pixel grid
One empty div becomes a 12-column grid. image-rendering: pixelated is the property doing the real job here — it keeps every pixel crisp instead of the browser smoothing tiny squares into a blur.
<div id="potion"></div> #potion { display: grid; grid-template-columns: repeat(12, 12px); grid-auto-rows: 12px; image-rendering: pixelated; }an empty grid, 12 pixels wide. Nothing to see — the sprite lives in the data, not here yet.
Draw the sprite from a map
The sprite is a string array — one character per pixel, a dot for empty space. A short loop reads every row and character, and creates one <span> per non-dot pixel with a class matching its letter. Only the outline color (k) is styled so far, so just the ink sketch shows through.
// the sprite lives in this map — edit letters, redraw the potion const MAP = [ "....cccc....", "....cccc....", "....kkkk....", "....kggk....", "...kkggkk...", "..kggggggk..", ".kgghgggggk.", ".kgllllllgk.", ".kllllllllk.", ".kllllllllk.", "..kllllllk..", "...kkkkkk...", ]; const el = document.getElementById("potion"); for (const row of MAP) { for (const ch of row) { const px = document.createElement("span"); if (ch !== ".") px.className = ch; el.appendChild(px); } }just the ink outline — the sprite's sketch layer, drawn straight from the letters in MAP.
Fill in the colors
Every remaining letter in the map already has a matching CSS class waiting for it — cork, glass, shine, liquid. Adding their colors is the whole step; the JS loop from the last step never changes.
#potion span.c { background: #FFA94D; } /* cork */ #potion span.g { background: #E6EFFF; } /* glass */ #potion span.h { background: #FFFDF7; } /* shine */ #potion span.l { background: #FF6B5E; } /* liquid */full color. A little potion, pixel by pixel.
Make the liquid glow
One keyframe animating filter: brightness on the liquid pixels is enough to sell a magical glow — no gradients, no glow filters, no extra elements.
#potion span.l { background: #FF6B5E; animation: glow 2s ease-in-out infinite; } @keyframes glow { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.25); } }the finished potion, glowing like it means it.
the complete code
Everything, in one place
Skipped straight to the end? Welcome. Copy the whole thing, download it, or open it in the Playground to start remixing.
<div id="potion"></div>
tips & gotchas
Mistakes we actually made
If the grid renders as one long row instead of a square, check grid-auto-rows is set — grid-template-columns alone only defines the columns.
The loop skips "." characters on purpose — every dot is empty space, not a pixel.
Class names are single letters (c, k, g, h, l) purely to keep MAP readable as a little picture in the source — nothing stops you from renaming them.
make it yours
Remix it
- New label coloreasy
Change span.c's background for a different cork.
- Bigger spriteeasy
Bump 12px to 20px in both the grid and the pixel sizing for a chunkier sprite.
- Recolor by handmedium
Replace a few "l" letters with "g" in MAP to draw a half-empty bottle.
- New spritemedium
Design your own MAP from scratch: a heart, a mushroom, a tiny sword.
challenge extension
Same map-to-pixels technique, new object: draw a pixel heart that pulses instead of glows — same animation trick, different keyframe values.