Overview
I wanted the headings in my Astro Markdown site to behave like GitHub’s:
hover to reveal a link icon for deep-linking to that heading.
After reading rehype-autolink-headings’ docs on using hastscript,
I realized I could replicate the effect with some tailwindcss 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.
Now when you hover over a heading, a subtle link icon appears.
Clicking it jumps directly to that heading’s URL.
How it works
rehype-sluggeneratesids for each heading.rehype-autolink-headingswraps them in links.hastscriptbuilds the icon element.- Tailwind’s
group-hoverfades the icon in.
Example configuration
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
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]", }),],hcomes fromhastscript. It creates a<span>element in hast syntax, which is how rehype represents HTML.aria-hidden="true"hides the icon from screen readers since it’s decorative.icon-[lucide--link]is the Iconify class for the link icon.absolute left-0 top-1/2 -translate-y-1/2positions the icon to the left of the heading and vertically centers it.opacity-0 group-hover:opacity-100 transition-opacity duration-150hides the icon until you hover over the heading, then fades it in smoothly.text-base-content/50makes the icon a muted, theme-aware color. In this case daisyUI is being used.h-[1em] w-[1em]sizes the icon to match the heading’s font size.
properties
properties: { className: "group relative block pl-[1.5em] -ml-[1.5em]", tabindex: "-1",},grouplets us usegroup-hoverinside the icon’s classes.relativemakes the heading container the positioning reference for the absolutely positioned icon.blockensures 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 with the Tailwind 4 plugin.
Installing the plugin
bun add -D @iconify/tailwind4 @iconify-json/lucide@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.