Skip to main content
#guide3 min read
views

Generate Blog Heading Anchors in React-Markdown

Amir Ardalan
Important

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>
  )
}
tsx

Create 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
typescript

Import 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.tsx
import 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>
)
},
...
tsx

Here, 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;
}
scss

Final 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.