MDX autolink headings
Tags: mdx, react, contentlayer, tailwindcss
Published: 2023-01-04 08:20:06

---
title: MDX autolink headings
published: true
description: Learn to configure MDX so that headings get anchor links to themselves
tags: mdx,react,contentlayer,tailwindcss
cover_image: https://images.unsplash.com/photo-1425326452142-67c31f601d2f?fit=crop&w=1000&h=420
canonical_url: https://sdorra.dev/posts/2022-12-07-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.

```tsx
<h1 id="hello-world">Hello World</h1>
```

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](https://mdxjs.com/).
We can use the [rehype-slug](https://github.com/rehypejs/rehype-slug) plugin, which does the whole work for us.

```bash
# pnpm
pnpm add -D rehype-slug
# yarn
yarn add -D rehype-slug
# npm
npm install --save-dev rehype-slug
```

After the installation we have to tell [MDX](https://mdxjs.com/) that it should use the plugin.
The example below shows a [contentlayer](https://www.contentlayer.dev/) config,
but it should work with every [MDX](https://mdxjs.com/) setup.

```javascript
import rehypeSlug from "rehype-slug";

export default makeSource({
  contentDirPath: "content/posts",
  documentTypes: [Post],
  mdx: {
    rehypePlugins: [rehypeSlug],
    remarkPlugins: [],
  },
});
```

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](https://github.com/rehypejs/rehype-autolink-headings) for this.
But if we want to do some styling with [TailwindCSS](https://tailwindcss.com/), we have to wrap the heading manually.

To wrap our headings into links,
we need to override the default [MDX](https://mdxjs.com/) heading components.

```tsx
const Markdown = ({ code }: Props) => {
  const MDXComponent = useMDXComponent(code);
  return (
    <section className="prose">
      <MDXComponent
        components={{
          h1: heading("h1"),
          h2: heading("h2"),
          h3: heading("h3"),
          h4: heading("h4"),
          h5: heading("h5"),
          h6: heading("h6"),
        }}
      />
    </section>
  );
};
```

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

```tsx
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>
    </a>
  );
  Heading.displayName = As;
  return Heading;
};
```

The heading component renders an `a` tag,
with an `href` pointing to the `id` which was generated by the [rehype-slug](https://github.com/rehypejs/rehype-slug) and passed as a prop to our component.
On the `a` tag we use the [group](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state) 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](https://lucide.dev/docs/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](https://mdxjs.com/) 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.