Building fancy list items in Astro
You probably saw many websites using cool bullet points instead of plain old-boring ones. How do they do it? Is there an effective and simple way to create fancy bullets while writing really simple code?
The answer is there is — with Astro, it's possible. You can simply write markdown and out comes a nicely formatted bullet you made with SVGs.
Before we begin
I'm going to assume you know how to use Astro and you know how to integrate MDX into Astro. If you don't, don't worry. Just follow through and get the concept of how things work, then go figure out how to use Astro and MDX.
(I'm going to create a course for it soon because Astro is fantastic — it's the best static site generator I've ever used).
Steps
If you've been using Astro, you can probably infer these steps from the example I showed you above. They are:
- You create a
<List>
component that takes in your bullets in the<slot>
content. - You grab these bullets in the
<List>
component. - You loop through each list item and prepend an SVG before the bullet.
Here's what the markup looks like
---
const { bullet } = Astro.props
---
<ul class='List'>
{
listItems.map(item => (
<li>
<!-- Your SVG here -->
<Fragment set:html={item.innerHTML} />
</li>
))
}
</ul>
The cool thing about this is you can add extra properties like fill
, stroke
, and all sorts of things to change the SVG so it renders with enough uniqueness.
Here are some examples where I changed the SVG fill
for Magical Dev School.
I'm going to show you how to build a simple version that lets you insert an SVG you want. We won't be going through the advanced stuff like adding fill
and stroke
and other conditions though!
Grabbing the bullets in Astro
Astro lets you get the contents of a <slot>
element inside your component.
You can get this content by calling a render
function. You'll see a string
that contains the HTML which will be created.
---
const html = await Astro.slots.render('default')
console.log(html)
---
Next, this is where the magic happens
You can parse this HTML string and get the list items — just like doing a standard document.querySelectorAll
with JavaScript.
To do this, you need to parse the HTML string into a DOM-like structure.
There are many parsers you can use here. The one I've chosen to use for my projects is node-html-parser
. (Because it's fast).
---
import { parse } from 'node-html-parser'
const html = await Astro.slots.render('default')
const root = parse(html)
---
After parsing the HTML, you can grab the list items by using querySelectorAll
.
---
// ...
const listItems = root.querySelectorAll('li')
---
You can then map through the list items and build your fancy list.
---
// ...
---
<ul class='List'>
{
listItems.map(item => (
<li>
<!-- Your SVG goes here -->
<Fragment set:html={item.innerHTML} />
</li>
))
}
</ul>
Adding SVGs
The simplest way to add SVGs into the component is to use inline SVGs.
If you want to switch between different types of list items, you can ask the user to pass in a bullet
property. Then, you inline a different SVG depending on the bullet
value.
---
const { bullet } = Astro.props
// ...
---
<ul class='List'>
{
listItems.map(item => (
<li>
{ bullet === 'green-check' && <svg> ... </svg> }
{ bullet === 'red-cross' && <svg> ... </svg> }
{ bullet === 'star' && <svg> ... </svg> }
<Fragment set:html={item.innerHTML} />
</li>
))
}
</ul>
Of course, this isn't the best way...
Better way to add SVGs
The better way is to add an SVG through a component. When you do this, you can simply pass in an SVG and let the SVG component handle the SVG logic.
---
import SVG from './SVG.astro'
const { bullet } = Astro.props
// ...
---
<ul class='List'>
{
listItems.map(item => (
<li>
<SVG name={bullet} />
<Fragment set:html={item.innerHTML} />
</li>
))
}
</ul>
The simplest way to build this SVG component is to inline SVG code within it, like in the example shown above.
---
// SVG component
const { name } = Astro.props
---
{ bullet === 'green-check' && <svg> ... </svg> }
{ bullet === 'red-cross' && <svg> ... </svg> }
{ bullet === 'star' && <svg> ... </svg> }
But this isn't the best way because you still have to use conditional statements...
And SVG can be notoriously big and complex so you'll end up creating something unwieldy instead...
The best way to build the SVG component
The best way is to:
- Keep the SVG in an
svgs
folder - Grab the SVG from this folder when you pass the
name
into the<SVG>
component
How to do this is best left for another lesson since we'll go deep into another topic.
If you'd like to find out how I build the SVG
component, along with how I use Astro and Svelte, scroll down and leave your email in the form below. I'd love to share more with you!
Fancy Validation
What's pretty cool about this approach is you can even use Astro to validate the type of bullet
you pass into <List>
, the fill
, the stroke
, and other properties you want.
Of course, you can do this with Typescript as well. I'm just showing you that you can use normal JS if you want to keep things simple.
const validators = {
bullet: [{
type: 'star',
fill: ['yellow', 'green', 'red', 'blue'],
stroke: ['black', 'transparent'],
}]
}
validate({ ...Astro.props })
function validate () {
// Check props against validators
}
That's it!
Hope you learned something new today!
By the way, this article is originally written on my blog. Feel free to visit that if you want to have these articles delivered to your email first-hand whenever they're released! 🙂.