Pokémon as HTML list bullets using SASS functions

aileenr

Aileen Rae

Posted on July 25, 2022

Pokémon as HTML list bullets using SASS functions

This is a chatty post describing my step-by-step development process. You can skip to the end result here.

Earlier this year, one of CodePen's weekly challenges was all about list styles. If you have not already, I highly recommend subscribing to Codepen's weekly challenges. They're a great inspiration for little frontend dev exercises.

Screenshot of the Codepen List Styles Challenge page.

Back to list styles - I was immediately feeling inspired to code up something frivolous and fun. I decided I wanted to replace the traditional numbers of an ordered list with images that esoterically represent numbers. Specifically, I decided to use Pokémon in Pokédex order.

Yes, I am a Pokémon nerd, but so are a lot of developers in the community. There are a lot of great Pokémon resources for dev side projects such as PokéAPI. For this little project, I chose PokéSprite sprite images to be my ordered list bullets.

Screenshot of the Codepen List Styles Challenge page.

How I built it

Step 1: Incrementing numbers in CSS rules

I started thinking abstractly about what I needed the CSS to do. Replacing a single list number marker with an image would require the following CSS:

ol li::marker {
  content: url("image_url_here");
}
Enter fullscreen mode Exit fullscreen mode

In order to have a different image for each list item, I needed a different rule for each child using the nth-of-type CSS pseudo-class.

ol li:nth-of-type(1)::marker {
  content: url("image_001");
}

ol li:nth-of-type(2)::marker {
  content: url("image_002");
}

ol li:nth-of-type(3)::marker {
  content: url("image_003");
}

/* etc etc */
Enter fullscreen mode Exit fullscreen mode

Copy-pasting this for each list item would quickly become tedious. There are now nearly 1,000 Pokémon. No way: I wanted a programmatic solution.

At this point I wanted to style this with vanilla CSS if possible, so I began playing with the counter() CSS function to programatically generate the numbers. Something like this:

counter-increment: idx;

ol li:nth-of-type(idx)::marker {
  content: url("image_{idx}"); /* This is invalid 😡 */
}
Enter fullscreen mode Exit fullscreen mode

But, after several attempts and furious googling on how to put the counter value in an image URL, I found this (unecessarily downvoted) Stack Overflow question:

Can I insert css counter in content url?

The answer is no. Bummer. 😞

On the plus side, the answer to that Stack Overflow question provided the solution: a SASS @for loop.

With that I had the CSS plumbing ready for my Pokémon sprite image URLs:

ol li {
  @for $i from 1 through 908 {
    &:nth-of-type(#{$i})::marker {
      content: url("#{$i}.png");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Preparing the image files

Now the only thing missing was the Pokémon image files themselves. I downloaded this collection of sprites from PokéSprites and quickly noticed I had made a breaking assumption: the files were named after the Pokémon rather than their dex number, e.g. bulbasaur.png.

Not to worry though, PokéSprite also comes with a structured JSON data file through which I could map the filename to the dex number. I got cracking on writing a script I could run to rename the hundreds of files for me. I'm not a Node expert so after a lot of googling about Node FS, I ended up with this:

// extractOrderedListOfPokemon.mjs
import * as input from './pokemon.json';
import * as fs from 'fs';

const dirPath = './by-nat-dex-number';
const data = input.default;
const filesToDelete = [];

const errCallback = err => {
  if (err) {
    console.error('ERROR: ' + err);
  }
}

const dir = await fs.promises.opendir(dirPath);
for await (const dirent of dir) {
  const filename = dirent.name;
  const pokemonSlug = filename.replace(".png", "");
  const dexNumber = Object.values(data).find(pokemon => pokemon.slug.eng === pokemonSlug)?.idx;

  if (dexNumber) {
    fs.rename(`${dirPath}/${filename}`, `${dirPath}/${dexNumber}.png`, errCallback);
  } else {
    filesToDelete.push(`${dirPath}/${filename}`);
  }
}

filesToDelete.forEach(file => {
  fs.unlink(file, errCallback);
});
Enter fullscreen mode Exit fullscreen mode

I ran this as a node script in my terminal. After learning I needed the --experimental-json-modules option for the .json import to work, I had the files I needed named as 001.png, 002.png etc, all the way up to 908.png. I uploaded them all to an AWS S3 bucket for hosting.

Step 3: A SASS zerofill function

All I thought was left to do was to plug my image URLs into my code like so:

ol li {
  @for $i from 1 through 908 {
    &:nth-of-type(#{$i})::marker {
      content: url("https://my-s3-bucket-domain.com/#{$i}.png");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

except:

An HTML list with Pokémon icons as bullets, but there are no images before the 100th list item.

Oops, my image urls are zerofilled, or padded with zeroes at the start so that each filename is the same length of characters. My CSS code is looking for 1.png when it needs 001.png.

Once again, Stack Overflow had my solution, and I adapted my code to:

@function zerofill($i) {
  @return #{str-slice("000", 0, 3 - str-length(#{$i}))}#{$i};
}

ol li {
  @for $i from 1 through 908 {
    &:nth-of-type(#{$i})::marker {
      $i: zerofill($i);
      content: url("https://my-s3-bucket-domain.com/#{$i}.png");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The result

And ta-da, 🎉 my Pokémon ordered list!

⚠ If you're a Safari or iOS user, unfortunately you won't see the Pokémon bullets. At the time of writing, Safari has limited support for the ::marker pseudo-element. You can check out the bug ticket for more details.

💖 💪 🙅 🚩
aileenr
Aileen Rae

Posted on July 25, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related