Skip to content
CoDdleCoDoodle home
Pixel ArtIntermediateHTML + CSS + JS

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
~88

the steps

Build it, one change at a time

  1. Set up the pixel scene grid

    One scene div holds the shelf, brick pixels, sparkles, and an empty 12-column potion grid. Every piece is aligned to the same 12px unit, and image-rendering: pixelated keeps the tiny squares crisp instead of blurry.

    <div id="potion-scene" aria-label="Pixel potion on a dungeon shelf">
      <span class="brick brick-1"></span>
      <span class="brick brick-2"></span>
      <span class="brick brick-3"></span>
      <span class="sparkle sparkle-1"></span>
      <span class="sparkle sparkle-2"></span>
      <span class="sparkle sparkle-3"></span>
      <span class="shelf"></span>
      <div id="potion"></div>
    </div>
    
    #potion-scene {
      position: relative;
      width: 192px;
      height: 192px;
      background: #FBF2E5;
      image-rendering: pixelated;
    }
    #potion-scene .shelf,
    #potion-scene .brick,
    #potion-scene .sparkle {
      position: absolute;
      display: block;
    }
    #potion-scene .shelf {
      left: 12px;
      top: 156px;
      width: 168px;
      height: 12px;
      background: #2B3A55;
    }
    #potion-scene .brick {
      width: 36px;
      height: 12px;
      background: #E8DCCB;
    }
    #potion-scene .brick-1 {
      left: 0;
      top: 12px;
    }
    #potion-scene .brick-2 {
      left: 24px;
      top: 24px;
      width: 24px;
    }
    #potion-scene .brick-3 {
      right: 0;
      top: 132px;
    }
    #potion-scene .sparkle {
      width: 12px;
      height: 12px;
      background: #FFD84D;
      box-shadow:
        12px 0 #FFFDF7,
        -12px 0 #FFFDF7,
        0 12px #FFD84D,
        0 -12px #FFD84D;
    }
    #potion-scene .sparkle-1 {
      left: 156px;
      top: 36px;
    }
    #potion-scene .sparkle-2 {
      left: 24px;
      top: 84px;
    }
    #potion-scene .sparkle-3 {
      left: 144px;
      top: 108px;
    }
    #potion {
      position: absolute;
      left: 24px;
      top: 12px;
      display: grid;
      grid-template-columns: repeat(12, 12px);
      grid-auto-rows: 12px;
      image-rendering: pixelated;
    }
    

    a paper dungeon backdrop: shelf pixels, brick pixels, sparkles, and an empty potion grid waiting for data.

  2. 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 on the shelf scene.

    // 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 and sitting on the shelf.

  3. 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, tucked into its dungeon-shelf scene.

  4. 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 dungeon-shelf 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-scene" aria-label="Pixel potion on a dungeon shelf">
  <span class="brick brick-1"></span>
  <span class="brick brick-2"></span>
  <span class="brick brick-3"></span>
  <span class="sparkle sparkle-1"></span>
  <span class="sparkle sparkle-2"></span>
  <span class="sparkle sparkle-3"></span>
  <span class="shelf"></span>
  <div id="potion"></div>
</div>
Open in Playground

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.

keep sketching

Pick another small recipe and borrow one technique for your next doodle.