Skip to content
crocodillon.
← All writing

From the field notes

Giving Markdown a lead paragraph with remark

Dillon de Voor3 min read

Tailwind Typography already knows how to style a lead paragraph. I just needed a way to give the first paragraph in my Markdown the right class.

I was using react-markdown1 together with Tailwind’s Typography plugin2 in one of my projects. Wrapping the rendered content in an element with the prose class took care of most of the styling. For the opening paragraph, though, I wanted something a little more prominent.

Typography supports that through the lead class: put it on a paragraph inside your prose container and it gets a larger font size and its own spacing. I couldn’t find a built-in option to add that class to the first paragraph automatically, so I wrote a tiny remark plugin.

If you just inspected the first paragraph of this article: nope, I admit I haven’t eaten my own dog food on this blog. That one has a manually added intro class. I do use the plugin on Reeflings, though!

The plugin

Anyway, without further ado, here it is. Save this as remark-lead-paragraph.ts:

import type { Root } from 'mdast'
import type { Plugin } from 'unified'
import { visit } from 'unist-util-visit'
 
const remarkLeadParagraph: Plugin<[], Root> = () => tree => {
  let foundFirstParagraph = false
 
  visit(tree, 'paragraph', node => {
    if (!foundFirstParagraph) {
      node.data ??= {}
      node.data.hProperties ??= {}
      node.data.hProperties.className = 'lead'
 
      foundFirstParagraph = true
    }
  })
}
 
export default remarkLeadParagraph

Remark plugins work on the Markdown syntax tree. Here, visit finds paragraph nodes, and the flag makes sure we only change the first one. The flag lives inside the transformer, so it starts fresh for each document.

The useful bit is data.hProperties. When remark-rehype converts the Markdown tree into an HTML tree, it carries these properties over to the resulting element.3 By the time React renders the paragraph, it has className="lead". There’s no need for a separate rehype plugin.

Using it with react-markdown

Assuming you already have react-markdown and Tailwind Typography set up, add the visitor utility and the types used by the plugin:

pnpm add unist-util-visit
pnpm add -D @types/mdast unified

Then pass the plugin through remarkPlugins:

import ReactMarkdown from 'react-markdown'
import remarkLeadParagraph from './remark-lead-paragraph'
 
const content = `## A little introduction
 
This paragraph gets the lead class.
 
This one is a regular paragraph.`
 
export default function Article() {
  return (
    <div className="prose">
      <ReactMarkdown remarkPlugins={[remarkLeadParagraph]}>
        {content}
      </ReactMarkdown>
    </div>
  )
}

The heading stays a heading, the first paragraph gets lead, and the second paragraph stays as it is. Tailwind Typography handles the styling. If you’ve supplied a custom p component through components, make sure it passes className through to the rendered paragraph too.

One detail to keep in mind: this finds the first paragraph anywhere in the tree, including inside a blockquote or list. If you only want the first paragraph directly under the document root, you’ll want to check the node’s parent as well. It also replaces any existing hProperties.className, so merge classes instead if another plugin adds them.

If this helped you, let me know in the comments!

Footnotes

  1. react-markdown renders Markdown as React elements. Under the hood it uses remark for the Markdown tree, remark-rehype to convert that to an HTML tree, and rehype plugins before rendering. ↩

  2. Tailwind CSS Typography provides the prose class for styling rendered content. Its styles for lead set the introductory text’s size, line height, and spacing. ↩

  3. The mdast-util-to-hast documentation explains how data.hProperties supplies properties for the resulting HTML element. This is the conversion utility used by remark-rehype. ↩

Continue the conversation

Questions, corrections, or something to add? Join the discussion below. Comments are powered by Disqus.