DEV Community

Cover image for Astro Scroll to Anchor: Smooth Scroll to Heading
Rodney Lab
Rodney Lab

Posted on • Originally published at


Astro Scroll to Anchor: Smooth Scroll to Heading

⚓️ What is Astro and What is Scroll to Anchor?

In this post we will see how to build Astro Scroll to Anchor functionality into your static site. Before we get on to that though, we should take a quick peek at what Astro and scroll to anchor are. Astro is a new static site builder which lets you build fast websites. The secret to its speed is something called partial hydration which means you, as a developer, get more control over when JavaScript on a page loads. You can even ship zero JavaScript when none is needed. Scroll to anchor is a nice feature we have come to expect on websites where a little link icon appears if you hover over a heading. You can click the link to scroll smoothly to that heading, as well as even copy and save it for future reference.

🧱 What we're Building

Astro Scroll to Anchor: Image is zoomed in on a paragraph of text with a heading above.  Following the heading text is a link icon.

Having said that Astro's superpower is partial hydration, we're going to build out the scroll functionality with no hydration at all. Meaning no JavaScript is required for our page; we will add the smooth scrolling and link auto show/hide using CSS. Although you can use Astro with Lit, React, Svelte or Vue, we will create a pure Astro component to add the feature. This will make it easier for you to recycle the code for use in your own Astro project using your preferred framework. Have a look at the post introducing Astro, though if you want to know more about partial hydration and Astro’s islands architecture.

If you are new to Astro, consider this a gentle introduction. If, however, you already have some experience with Astro you will see a new and efficient way to add SVG icons to your Astro app. This will let you pick icons from any icon library you want, just by adding a single dependency.

🚀 Getting Started

The code we will look at can easily be added to an existing project, though if you are new to Astro, just follow along and you can add it to your next project! If you are starting a new project, let’s get the ball rolling in the Terminal:

mkdir astro-scroll-to-anchor && cd $_
pnpm init astro
pnpm install
pnpm run dev
Enter fullscreen mode Exit fullscreen mode

Use yarn or npm if you prefer those to pnpm. Choose Minimal from the list of templates. The Astro dev server will normally run on port 3000 but if there is already something running there, it will find the next available port. The terminal will then tell you which port it settled for:

Astro Scroll to Anchor: Screenshot show Astro C L I terminal output giving the dev server address as http  localhost 3001

Here we have port 3001 and can access our page at the localhost link shown. This is a great feature, just make sure you only run one server at a time though! You can run multiple servers, but a couple of times I have spun up a new dev server while I already had one running in preview mode. Of course, the preview one was open in the browser and I couldn't work out why code changes weren't showing up… a fun way to waste ten minutes!

Anyway, if you are following along, starting from scratch, replace the content in src/pages/index.astro with this:

// frontmatter section - nothing to see here yet

<html lang="en-GB">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Astro Scroll to Anchor</title>
    <main class="container">
      <div class="wrapper">
          <Heading text="Astro Scroll to Anchor" id="astro-scroll-to-anchor" />
        <h2>Lorem ipsum" /></h2>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
          ut labore et dolore magna aliqua. Suscipit adipiscing bibendum est ultricies integer quis.
          Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. At erat pellentesque
          adipiscing commodo elit at imperdiet. Suscipit adipiscing bibendum est ultricies integer
          quis auctor. Velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Imperdiet
          sed euismod nisi porta. Non blandit massa enim nec. Etiam dignissim diam quis enim
          lobortis scelerisque fermentum dui. Suspendisse sed nisi lacus sed viverra tellus in.
          Metus dictum at tempor commodo ullamcorper a. A scelerisque purus semper eget duis at.
          Ultrices dui sapien eget mi proin sed libero. Cursus metus aliquam eleifend mi in nulla
          posuere sollicitudin.
        <h2>Amet porttitor</h2>
          Amet porttitor eget dolor morbi. Ullamcorper eget nulla facilisi etiam dignissim diam quis
          enim. Cras tincidunt lobortis feugiat vivamus at. Eleifend donec pretium vulputate sapien
          nec sagittis aliquam malesuada bibendum. Curabitur gravida arcu ac tortor dignissim.
          Scelerisque purus semper eget duis. Amet nulla facilisi morbi tempus iaculis urna id. Et
          ligula ullamcorper malesuada proin libero. Risus pretium quam vulputate dignissim
          suspendisse in. Nec dui nunc mattis enim ut tellus elementum. At quis risus sed vulputate
          odio. Facilisi cras fermentum odio eu feugiat pretium. Lorem ipsum dolor sit amet
          consectetur. Sit amet massa vitae tortor condimentum lacinia quis. Amet volutpat consequat
          mauris nunc congue nisi vitae suscipit tellus. Posuere lorem ipsum dolor sit amet
          consectetur adipiscing elit duis. Ac turpis egestas integer eget aliquet nibh. In nibh
          mauris cursus mattis.
        <h2>Blandit turpis</h2>
          Blandit turpis cursus in hac habitasse platea. Egestas tellus rutrum tellus pellentesque
          eu. In eu mi bibendum neque. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor.
          Augue mauris augue neque gravida. Tristique nulla aliquet enim tortor at auctor. A
          pellentesque sit amet porttitor. Pharetra pharetra massa massa ultricies mi. Fringilla ut
          morbi tincidunt augue interdum velit euismod in pellentesque. Et leo duis ut diam quam
          nulla porttitor. Pharetra diam sit amet nisl suscipit. Lorem donec massa sapien faucibus.
          Tempor orci eu lobortis elementum nibh tellus. Urna porttitor rhoncus dolor purus non enim
          praesent elementum facilisis. Sed nisi lacus sed viverra tellus in hac habitasse.
          Fermentum leo vel orci porta non pulvinar neque laoreet suspendisse. Enim facilisis
          gravida neque convallis a cras. Enim nunc faucibus a pellentesque sit amet porttitor. Cras
          fermentum odio eu feugiat pretium.
        <h2>Arcu dui</h2>
          Arcu dui vivamus arcu felis bibendum ut tristique. Congue eu consequat ac felis donec et
          odio. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Libero nunc consequat
          interdum varius sit. At volutpat diam ut venenatis. Euismod quis viverra nibh cras.
          Vestibulum morbi blandit cursus risus. Risus viverra adipiscing at in tellus integer
          feugiat scelerisque. Tristique senectus et netus et malesuada fames ac. Amet risus nullam
          eget felis eget nunc lobortis. Nisl pretium fusce id velit ut tortor pretium viverra.
          Turpis egestas sed tempus urna et pharetra pharetra massa massa. Fermentum dui faucibus in
          ornare quam viverra orci sagittis. Nam libero justo laoreet sit. Eget velit aliquet
          sagittis id consectetur purus ut faucibus pulvinar. Nullam ac tortor vitae purus faucibus
          ornare suspendisse.
        <h2>Tellus in hac</h2>
          Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque. Dignissim
          sodales ut eu sem integer vitae justo. Nunc vel risus commodo viverra. Nunc sed blandit
          libero volutpat sed cras. Arcu risus quis varius quam quisque id. Tristique sollicitudin
          nibh sit amet commodo nulla facilisi. Sed vulputate mi sit amet mauris commodo quis
          imperdiet. Tristique sollicitudin nibh sit amet commodo nulla facilisi. Tellus at urna
          condimentum mattis. Feugiat scelerisque varius morbi enim. Sit amet aliquam id diam
          maecenas ultricies mi. Lectus quam id leo in vitae turpis massa sed. Felis donec et odio
          pellentesque diam volutpat commodo sed egestas. Facilisis gravida neque convallis a cras
          semper. Velit laoreet id donec ultrices tincidunt. Sed lectus vestibulum mattis
          ullamcorper velit. Et ultrices neque ornare aenean euismod elementum nisi quis eleifend.

  /* raleway-regular - latin */
  @font-face {
    font-family: 'Raleway';
    font-style: normal;
    font-weight: 400;
    src: local(''), url('/fonts/raleway-v26-latin-regular.woff2') format('woff2');

  @font-face {
    font-family: 'Raleway';
    font-style: normal;
    font-weight: 700;
    src: local(''), url('/fonts/raleway-v26-latin-700.woff2') format('woff2');

  @font-face {
    font-family: 'Raleway';
    font-style: normal;
    font-weight: 900;
    src: local(''), url('/fonts/raleway-v26-latin-900.woff2') format('woff2');

  :global(html) {
    --colour-background-hue: 47.36;
    --colour-background-saturation: 100%;
    --colour-background-luminance: 52.55%;

    --colour-text-hue: 282.86;
    --colour-text-saturation: 53.85%;
    --colour-text-luminance: 35.69%;

    --font-family-heading: Montserrat;
    --font-family-body: Raleway;

    --font-size-1: 1rem;
    --font-size-5: 2.441rem;
    --font-size-6: 3.052rem;

    --font-weight-bold: 700;
    --font-weight-black: 900;

    --line-height-relaxed: 1.75;

    --max-width-full: 100%;
    --max-width-wrapper: 38rem;

    --spacing-20: 5rem;

    background-color: hsl(
      var(--colour-background-hue) var(--colour-background-saturation)
    color: hsl(var(--colour-text-hue) var(--colour-text-saturation) var(--colour-text-luminance));

  :global(h2) {
    font-family: var(--font-family-heading);

  :global(h1) {
    font-size: var(--font-size-6);
    font-weight: var(--font-weight-black);
  :global(h2) {
    font-size: var(--font-size-5);
    font-weight: var(--font-weight-bold);

  :global(p) {
    font-family: var(--font-family-body);
    font-size: var(--font-size-1);
    line-height: var(--line-height-relaxed);

  .container {
    display: flex;
    align-items: center;
    padding-bottom: var(--spacing-20);

  .wrapper {
    width: var(--max-width-full);
    max-width: var(--max-width-wrapper);
    margin: 0 auto;
Enter fullscreen mode Exit fullscreen mode

This is just some placeholder text which will let us explore a few Astro features as we build out the Astro scroll to anchor feature.

Anatomy of an Astro File

Like Markdown files, Astro files also have a frontmatter section. This is where you can import packages as well as run any JavaScript you need to for the output. You can also use TypeScript in the frontmatter.

The next part of the file is essentially a template. You can include JavaScript scripts in script tags, but can't actually run JavaScript code within this section (this is different to the JSX you might use in React, for example). The Astro markup is a superset of HTML meaning making it easy to pick up if you are coming from a HTML/JavaScript only background.

Finally at the bottom we have some styling. You can include it like this within a script tag in your Astro file. As an alternative, for a typical project, you can create a global CSS stylesheet and import that in your Astro frontmatter. In this case you can still include any styles for the current page in this style tag. If you do want to use global stylesheet, just save it somewhere within the src folder of your project and import it as mentioned.

Final Preparation

Before proceeding, download some self-hosted fonts which we will use on the site. Save raleway-latin-400-normal.woff2 and raleway-latin-700-normal.woff2 together with raleway-latin-900-normal.woff2 to a new, public/fonts directory within the project.

🧩 Heading Component

The markup and styling for the Astro scroll to anchor feature will go in a new Astro component file. You can create Astro components as well as pages. The components are akin to those you would have in a React or SvelteKit app. Create a src/components folder and within that directory make a new Heading.astro file, adding this content:

import { Icon } from 'astro-icon';

const { 'aria-label': ariaLabel, id, text } = Astro.props;

const href = `#${id}`;

<span {id} class="container">
  <a aria-label={ariaLabel} {href}
    ><span class="anchor-link"><Icon name="heroicons-solid:link" /> </span></a

<style lang="css">
  .anchor-link {
    visibility: hidden;

  a {
    color: hsl(var(--colour-text-hue) var(--colour-text-saturation) var(--colour-text-luminance));
    text-decoration: none;

  [astro-icon] {
    display: inline;
    width: var(--font-size-5);
    vertical-align: middle;

  .container:focus .anchor-link,
  .container:hover .anchor-link {
    visibility: visible;
Enter fullscreen mode Exit fullscreen mode

You see a few Astro features in here. Firstly, like our home page we have three sections: frontmatter, markup and styles. In the first line we import the astro-icon package by Nate Moore, an Astro maintainer. This makes use of Anthony Fu's fantastic iconês library (used with the iconify package). If you haven’t yet heard of it, it is definitely worth exploring. Go to the iconês site where you can find icons from just about every library that exists. You can pick the icons you want for your app and under the hood, astro-icon efficiently imports just the ones you need.

We use the icon in line 12, you just select the icon you want on the iconês site and it gives you a name to add which you add as an attribute to the <Icon> component instances. Before that though, we need to install it the package:

pnpm add -D astro-icon
Enter fullscreen mode Exit fullscreen mode

and add a few lines of config to astro.config.mjs in the project root folder:

import { defineConfig } from 'astro/config';

export default defineConfig({
  // Comment out "renderers: []" to enable Astro's default component support.
  renderers: [],
  vite: {
    ssr: {
      external: ['svgo'],
Enter fullscreen mode Exit fullscreen mode

Astro props

In line 4 (of the Header.astro file) you see how to access props in an Astro component, we will include these in the markup for the home page in the next step. The two props will be the text of the title together with an id, used to create the scrolling link. This needs to be unique for each link we create. We actually use the text prop in the markup, in line 10.

Moving in line 11 we use an Astro shortcut (this will look familiar if you know Svelte). We can use this shortcut whenever the name of a variable matches the name of the attribute we want to assign it to:

  <a aria-label={ariaLabel} {href}>
Enter fullscreen mode Exit fullscreen mode

is equivalent to:

<a aria-label={ariaLabel} href={href}>
Enter fullscreen mode Exit fullscreen mode

Finally in lines 26–30, you see we can style the icon by targetting [astro-icon]. Notice the global CSS variables we defined on the home page are available in our component.

🔌 Using the new Component

Using the new component is a breeze. Let's update src/pages/index.astro first, importing our new Heading component:

import Heading from '../components/Heading.astro';
Enter fullscreen mode Exit fullscreen mode

and then using it in the headings:

<div class="wrapper">
    <Heading text="Astro Scroll to Anchor" id="astro-scroll-to-anchor" />
  <h2><Heading id="lorem-ipsum" text="Lorem ipsum" /></h2>
Enter fullscreen mode Exit fullscreen mode
<h2><Heading id="amet-porttitor" text="Amet porttitor" /></h2>
Enter fullscreen mode Exit fullscreen mode
<h2><Heading id="blandit-turpis" text="Blandit turpis" /></h2>
Enter fullscreen mode Exit fullscreen mode
<h2><Heading id="arcu-dui" text="Arcu dui" /></h2>
Enter fullscreen mode Exit fullscreen mode
<h2><Heading id="tellus-in-hac" text="Tellus in hac" /></h2>
Enter fullscreen mode Exit fullscreen mode

Alternative Implementation

We are passing the text in as a prop. This is so that you have easier access to the title text in the component, so for example, you could add some code to remove widows. This is the pet peeve of typographers where you have a single short word alone on a line. The alternative is to rewrite the component so it accepts the title text sandwiched between the Heading component:

<h2><Heading id="tellus-in-hac">Tellus in hac</h2>
Enter fullscreen mode Exit fullscreen mode

Then in the heading component, in the markup you would need to replace {text} with <slot/>. We won't go into details, here, just want to let you know another way exists.

If you save and hover over one of the headings, you icon should show up.

🛹 Adding Smooth Scrolling

The final missing piece is to add a touch of CSS to get smooth scrolling. It might seem counter-intuitive but we will switch off the feature for users who prefer reduced motion. This is just because on a long page, scrolling can be quite quick, and might trigger nausea in site visitors with vestibular disorders.

  :global(html) {

    /* ...TRUNCATED */

    color: hsl(var(--colour-text-hue) var(--colour-text-saturation) var(--colour-text-luminance));

        scroll-behavior: smooth;
  @media (prefers-reduced-motion: reduce) {
    :global(html) {
      scroll-behavior: auto;
Enter fullscreen mode Exit fullscreen mode

That’s it now! Let’s test it out next.

🙌🏽 Astro Scroll to Anchor: Wrapping Up

In this post we have had an introduction to Astro and seen:

  • how to pass props into an Astro component and access them from within the component,
  • a convenient and efficient way to add SVG icons to your Astro app,
  • how to make smooth scrolling more accessible.

The full code for the app is available in the Astro demo repo on Rodney Lab GitHub.

I hope you found this article useful and am keen to hear how you plan to use the Astro code in your own projects.

🙏🏽 Astro Scroll to Anchor: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as SvelteKit. Also subscribe to the newsletter to keep up-to-date with our latest projects.

Top comments (0)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐️🎀 JavaScript Visualized: Promises & Async/Await

async await