DEV Community

Cover image for MDX autolink headings
Sebastian Sdorra
Sebastian Sdorra

Posted on • Updated on • Originally published at

MDX autolink headings

Sometimes it is useful to create a link to a specific section of an article.
So it is common that headings of articles have an id which can be used to create an anchor link.

<h1 id="hello-world">Hello World</h1>
Enter fullscreen mode Exit fullscreen mode

If we want to create a link which scrolls directly to that heading,
we can append the following anchor to the url of the article #hello-world.

So much for the theory, but how can we automate this?

Generating ids

Generating ids for each heading is easy with MDX.
We can use the rehype-slug plugin, which does the whole work for us.

# pnpm
pnpm add -D rehype-slug
# yarn
yarn add -D rehype-slug
# npm
npm install --save-dev rehype-slug
Enter fullscreen mode Exit fullscreen mode

After the installation we have to tell MDX that it should use the plugin.
The example below shows a contentlayer config,
but it should work with every MDX setup.

import rehypeSlug from "rehype-slug";

export default makeSource({
  contentDirPath: "content/posts",
  documentTypes: [Post],
  mdx: {
    rehypePlugins: [rehypeSlug],
    remarkPlugins: [],
Enter fullscreen mode Exit fullscreen mode

That's it.
Now every heading in our articles should have an id and we could create links for them.
But how do we create those links?
Do we need to look up the id of the heading from the source code?
This sounds too complicated, there must be an easier way.

Self link

The simplest way to make it easier to generate links to our headings,
is to turn the headings into links which point to themselves.
Doing so, we can click them and copy the url from the browser address bar afterwards.

We could use rehype-autolink-headings for this.
But if we want to do some styling with TailwindCSS, we have to wrap the heading manually.

To wrap our headings into links,
we need to override the default MDX heading components.

const Markdown = ({ code }: Props) => {
  const MDXComponent = useMDXComponent(code);
  return (
    <section className="prose">
          h1: heading("h1"),
          h2: heading("h2"),
          h3: heading("h3"),
          h4: heading("h4"),
          h5: heading("h5"),
          h6: heading("h6"),
Enter fullscreen mode Exit fullscreen mode

We use a simple factory function to create a component for each heading:

import { Hash } from "lucide-react";
import { ReactNode } from "react";

type HeadingProps = {
  id?: string;
  children?: ReactNode;

const heading = (As: "h1", `h2`, `h3`, `h4`, `h5`, `h6`) => {
  const Heading = ({ id, children }: HeadingProps) => (
    <a href={`#${id}`} className="group relative no-underline">
      <Hash className="absolute -left-6 hidden group-hover:block p-1 pink-cyan-500 h-full" />
      <As id={id}>{children}</As>
  Heading.displayName = As;
  return Heading;
Enter fullscreen mode Exit fullscreen mode

The heading component renders an a tag,
with an href pointing to the id which was generated by the rehype-slug and passed as a prop to our component.
On the a tag we use the group class
which allows us to apply styling to children if the parent is hovered.
Also we use the relative class, because we want to position an icon absolute to the left of the heading.

Inside of the a tag we use a Hash icon from the lucide-react package.
The icon is positioned absolute with -1.5rem to the left of its normal position (absolute -left-6).
The icon is hidden by default, but gets displayed as block if the group is hovered (hidden group-hover:block).

The last child of the a tag is the heading itself which is one of h1, h2, h3, h4, h5 or h6 depending on the parameter with which the factory function was called.
The heading renders the id where the href of the a tag points to and the children which is the text of the heading.

And that's it, we can now hover over our MDX headings and should see a hash to the left.
If we click a heading, we should see the id appended to the url in the browser address bar.

Top comments (0)