Generate Blog Heading Anchors in React-Markdown
This post is quite old and is no longer being updated. The following information may not work with the latest versions of the tools, libraries, frameworks, or best practices discussed.
What Are Heading Anchors?
If you are not familiar with heading anchors, open almost any README.md on GitHub or hover over a heading in many blog posts. You will often see a small link icon or hash symbol beside the heading, indicating that you can link directly to that section.
To share a specific part of a blog post, you can click the heading you want to reference. The URL will update with an anchor that links directly to that section.
Why Not Use an Existing Package?
There are several established Rehype and Remark plugins for this, including remark-autolink-headings and rehype-slug.
If your goal is to add heading anchors as quickly as possible, those are good options.
The drawback comes from adding a separate dependency for every small feature in a blog. Once you begin loading several plugins into react-markdown, the amount of JavaScript sent to the client can grow quickly.
Because react-markdown is straightforward to extend, implementing simple features directly can help keep the client bundle smaller.
Create Markdown.tsx
In this example, the raw Markdown is passed into the component through the markdown prop:
// Markdown.tsx
import ReactMarkdown from 'react-markdown'
export default function Markdown({ markdown }) {
const MarkdownComponents: object = {
// Code will go here
}
return (
<ReactMarkdown components={MarkdownComponents}>
{markdown.content}
</ReactMarkdown>
)
}
tsxCreate generateSlug.ts in the utils Folder
We will use this function to generate the anchor slug.
// generateSlug.ts
const generateSlug = (str: string) => {
str = str?.replace(/^\s+|\s+$/g, '')
str = str?.toLowerCase()
const from = 'àáãäâèéëêìíïîòóöôùúüûñç·/_,:;'
const to = 'aaaaaeeeeiiiioooouuuunc------'
for (let i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
}
str = str
?.replace(/[^a-z0-9 -]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
return str
}
export default generateSlug
typescriptImport generateSlug.ts and Customize the Heading Node
My blog uses h3 elements for post section headings. Change this to whichever heading level fits your project.
// Markdown.tsximport ReactMarkdown from 'react-markdown'import generateSlug from '@/utils/generateSlug'h3: (props: H3Props) => { const children = Array.isArray(props.children) ? props.children : [props.children] const heading = children .flatMap((element) => typeof element === 'string' ? element : element?.type !== undefined && typeof element.props.children === 'string' ? element.props.children : [] ) .join('') const slug = generateSlug(heading) return ( <h3 id={slug}> <a href={`#${slug}`} {...props}></a> </h3> )},...tsxHere, we override the Markdown h3 renderer and inspect its children.
The flatMap call collects ordinary text along with text nested inside elements such as inline code. Those values are then joined into a single heading string.
We pass that string to generateSlug, then render an h3 with the generated id and a link pointing to the same anchor.
Improve the Experience with CSS
You may want to add smooth scrolling when a user follows an anchor link or clicks one of the headings. This helps make the movement between sections easier to follow.
html {
scroll-behavior: smooth;
}
scssFinal Thoughts
You can wrap the Markdown component in a class or pass a CSS prop directly to ReactMarkdown to style the anchored headings.
From there, you can add a link or hash icon that appears on hover to make it clear that each heading can be linked directly.
Enjoyed this? Like or share.