Custom horizontal rules with markdown-it and Eleventy
When I migrated this site over from Gatsby, MDX, and React to Eleventy and markdown I needed solutions for my custom MDX components. One that I wasn’t expecting to pose a problem was my .fancy
horizontal rule.
Horizontal rules in markdown
I’m a heavy emdash user, so it’s no surprise that I styled its content-wide cousin, the horizontal rule. Look, they aren’t related in any way other than appearance, but humor me. Throughout this site—at the time of this writing—you’ll encounter the pulsing wa[1] blob in a few places. Here’s a static screenshot in case the theme has changed.
As much as I love the effect as a hero image on the homepage, it brings me much more joy as an <hr />
replacement. In the previous build of the site, MDX would interpret the markdown syntax and render out a
<canvas>
element for the animation instead.
Eleventy documents how to substitute your own library to process markdown. I’m using markdown-it
, as it is well supported and commonly referenced in the 11ty community.
Overriding the renderer
It isn’t clear from the readme, but the architecture documentation on GitHub helps explain how the renderer
method works. If you’re using Eleventy, your markdown setup in .js
may look similar to the following:
// ...
// Import markdown-it
const markdownIt = require("markdown-it")
const mdSetup = markdownIt()
// any additional plugins here
module.exports = function (eleventyConfig) {
// set the markdown renderer to markdown-it
eleventyConfig.setLibrary("md", mdSetup)
}
The library’s renderer
method lets you target each individual element type. For hr
, you don’t need knowledge of the inner workings of markdown-it
, so it acts as a nice gateway into the process. Here’s what the next step looks like:
//..
const mdSetup = markdownIt()
mdSetup.renderer.rules.hr = (tokens, idx, options, env, self) => {
// return new HTML
}
// ...
We’re overriding the library’s rendering function for all hr
tags. We don’t need any of the arguments, as we aren’t using any data beyond the existence of horizontal rule in the markdown. Let’s return a new string of HTML.
mdSetup.renderer.rules.hr = (tokens, idx, options, env, self) => {
return `
<hr class="sr-only" />
<canvas class="wa hr" aria-hidden="true"></canvas>
`
}
Here I’m returning an <hr />
with my screen-reader-only CSS class, along with a <canvas>
element with the classes I use to target and style the element. aria-hidden=true
ensures that screen readers don’t try to interpret the element.
Eleventy will now render this new HTML instead of <hr />
. This only applies to markdown passed through the renderer itself. As an example, the markdown-it-footnote
plugin creates a horizontal rule before the footnotes with its own custom HTML rather than ours.
Here is the full code:
const markdownIt = require("markdown-it")
const mdSetup = markdownIt()
mdSetup.renderer.rules.hr = (tokens, idx, options, env, self) => {
return `
<hr class="sr-only" />
<canvas class="wa hr" aria-hidden="true"></canvas>
`
}
module.exports = function (eleventyConfig) {
eleventyConfig.setLibrary("md", mdSetup)
}
Further explorations
Overriding markdown rendering is the entrypoint for responsive images, custom code blocks, and endless customization. You may ask: Why not use a shortcode instead? The answer is future-proofing. By keeping the content as markdown, we prevent any lock-in to Eleventy or the renderer. Worst case scenario, we lose our nice horizontal rule visual, but semantically the element remains.
The theme of the site was heavily inspired by Julia Hasting’s beautiful cover design for “Wa: The Essence of Japanese Design”. ↩︎