Pokémon as HTML list bullets using SASS functions
How to use numbered image URLs as the bullets of an HTML ordered list.
https://aileenrae.co.uk/blog/pokemon-as-html-list-bullets urlEarlier 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.
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.
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");
}
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 */
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 😡 */
}
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");
}
}
}
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);
});
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");
}
}
}
except:
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");
}
}
}
The result
And ta-da, 🎉 my Pokémon ordered list!
See the Pen Pokémon Ordered List Bullets by Aileen Rae (@aileen-r) on CodePen.
::marker
pseudo-element. You can check out
the bug ticket
for more details.