---
description: Add GitHub-style hover link icons to Astro Markdown headings using
  rehype-autolink-headings, hastscript, and Tailwind CSS for better navigation
  and shareable sections.
keywords:
  - astro
  - rehype-autolink-headings
  - github style links
  - markdown
  - hastscript
  - astro markdown
publishDate: 2025-08-11
shortTitle: GitHub-Style Heading Links in Astro
tags:
  - Astro
  - Markdown
  - CSS
  - Frontend
title: GitHub-Style Hover Links in Astro with rehype-autolink-headings
---

## Overview

I wanted the headings in my [Astro Markdown](https://docs.astro.build/en/guides/markdown-content) site to behave like GitHub's:  
_hover to reveal a link icon for deep-linking to that heading_.

After reading [`rehype-autolink-headings`](https://github.com/rehypejs/rehype-autolink-headings)' docs on using [`hastscript`](https://github.com/syntax-tree/hastscript),  
I realized I could replicate the effect with some [`tailwindcss`](https://tailwindcss.com) magic.

## Before and after

Here's how it looked before and after adding the configuration.

Without any changes, hovering over a heading does nothing. There's no visual cue that it's linkable.

![Screenshot of Astro heading without hover link](@/assets/images/before-heading.png)

Now when you hover over a heading, a subtle link icon appears.  
Clicking it jumps directly to that heading's URL.

![Screenshot of Astro heading with hover link icon](@/assets/images/after-heading.png)

## How it works

- [`rehype-slug`](https://github.com/rehypejs/rehype-slug) generates `id`s for each heading.
- [`rehype-autolink-headings`](https://github.com/rehypejs/rehype-autolink-headings) wraps them in links.
- [`hastscript`](https://github.com/syntax-tree/hastscript) builds the icon element.
- Tailwind's [`group-hover`](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state) fades the icon in.

## Example configuration

```ts title="astro.config.ts"
import { defineConfig } from "astro/config";
import { h } from "hastscript";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";

const autoLinkHeadingOpts = {
  behavior: "wrap",
  content: [
    h("span", {
      "aria-hidden": "true",
      "class":
        "icon-[lucide--link] absolute left-0 top-1/2 -translate-y-1/2 " +
        "opacity-0 group-hover:opacity-100 transition-opacity duration-150 " +
        "text-base-content/50 h-[1em] w-[1em]",
    }),
  ],
  properties: {
    className: "group relative block pl-[1.5em] -ml-[1.5em]",
    tabindex: "-1",
  },
};

export default defineConfig({
  markdown: {
    gfm: true,
    rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, autoLinkHeadingOpts]],
  },
});
```

## Breaking down the configuration

The `autoLinkHeadingOpts` object controls how `rehype-autolink-headings` wraps and styles your headings.

### `content`

```ts
content: [
  h("span", {
    "aria-hidden": "true",
    class:
      "icon-[lucide--link] absolute left-0 top-1/2 -translate-y-1/2 " +
      "opacity-0 group-hover:opacity-100 transition-opacity duration-150 " +
      "text-base-content/50 h-[1em] w-[1em]",
  }),
],
```

- `h` comes from `hastscript`. It creates a `<span>` element in [hast](https://github.com/syntax-tree/hast) syntax, which is how [rehype](https://github.com/rehypejs/rehype) represents HTML.
- `aria-hidden="true"` hides the icon from screen readers since it's decorative.
- `icon-[lucide--link]` is the [Iconify class](#note-on-icons) for the link icon.
- `absolute left-0 top-1/2 -translate-y-1/2` positions the icon to the left of the heading and vertically centers it.
- `opacity-0 group-hover:opacity-100 transition-opacity duration-150` hides the icon until you hover over the heading, then fades it in smoothly.
- `text-base-content/50` makes the icon a muted, theme-aware color. In this case [daisyUI](https://daisyui.com/docs/colors/#list-of-all-daisyui-color-names) is being used.
- `h-[1em] w-[1em]` sizes the icon to match the heading's font size.

### `properties`

```ts
properties: {
  className: "group relative block pl-[1.5em] -ml-[1.5em]",
  tabindex: "-1",
},
```

- `group` lets us use `group-hover` inside the icon's classes.
- `relative` makes the heading container the positioning reference for the absolutely positioned icon.
- `block` ensures consistent spacing and padding behavior.
- `pl-[1.5em] -ml-[1.5em]` adds space for the icon while keeping the text aligned with other elements.
- `tabindex="-1"` removes the link from the tab order so keyboard navigation focuses on the heading text instead.

## Note on icons

The `icon-[lucide--link]` class comes from [Iconify](https://iconify.design/) with the Tailwind 4 plugin.

### Installing the plugin

```sh
bun add -D @iconify/tailwind4 @iconify-json/lucide
```

```css title="globals.css"
@plugin "@iconify/tailwind4";
```

### Using a different icon

Swap `lucide--link` for any other Lucide icon or from another installed Iconify collection.
Or, use your own icon setup such as SVGs, React components, or any approach you prefer.

## Wrapping up

With just a couple of `rehype` plugins, a sprinkle of `hastscript`, and some Tailwind magic, you can make your Astro headings more interactive, just like GitHub's.

![Gif of Astro heading with hover link icon](@/assets/images/after-heading.gif)