Overriding Gatsby-Link for Mixed Anchor Navigation

Updated: February 4th, 2020

Posted: February 20th, 2019

Topics: Gatsby, React

Update: I ended up the building Gatsby smooth scroll anchor link functionality from this post into a Gatsby plugin. Check it out on Github.

When I started to build this site, I knew that I wanted to house a lot of the information on the home page for better search engine optimization. I also knew that I would need some additional real estate for a blog and whatever else I might dream up. One of my favorite UI patterns for longform content is blocked sections with anchor smooth scrolling. This pattern typically lends itself to single-page sites, but we're gonna cheat the system here today.

Ok, so since we're using Gatsby, we've got to use it's gatsby-link package, which plays nice with all of the routing under the hood. Cool.

Let's think about how this nav is going to work from a logistical standpoint:

  • My main 4-5 nav items relate to blocked sections at route /
  • One or two nav items will lead users to another route, let's say /blog
  • While I'm on a non-root path, I need my 4-5 main nav items to know that I need to head back to / and then magically find the #block it refers to

The non-anchor link can just use the typical functionality of gatsby-link - so we just need to figure out how the 4-5 main links are going to situationally process what to do.

I'm thinking an onClick method on my <Link /> component will probably do the trick here. Let's take a look at what it might look like:

export default function Header() {
  
  const handleLinkClick = (e, target) => {
    
    // NODE-SAFE CODE
    // Gatsby uses Node to generate our pages. 
    // Node doesn't know what a window is. 
    // Be sure to wrap any of your browser interactions
    // in some sort of node-safe if statement like this:
    
    if (typeof window !== 'undefined') {
      
      // First, are we on the home page?
      // If so, let's scroll to the desired block,
      // which was passed in as an onClick method on our <Link />.
      // If an event was also passed, we'll preventDefault()
      
      if (window.location.pathname === '/') {
        if (e) e.preventDefault()
        scrollToElement(target, {
          offset: -95, // Offset a fixed header if you please
          duration: 1000,
        })
      }
    }
  }

    return (
      <HeaderWrapper>
        <Container>
            <Link
              onClick={e => handleLinkClick(e, '#about')}
              to={'/#about'}
            >
              About
            </Link>
            <Link
              onClick={e => handleLinkClick(e, '#experience')}
              to={'/#experience'}
            >
              Experience
            </Link>
            <Link
              onClick={e => handleLinkClick(e, '#work')}
              to={'/#work'}
            >
              Work
            </Link>
            <Link
              onClick={e => handleLinkClick(e, '#clients')}
              to={'/#clients'}
            >
              Clients
            </Link>
            <Link
              onClick={e => handleLinkClick(e, '#testimonials')}
              to={'/#testimonials'}
            >
              Testimonials
            </Link>
            <Link className="divider" to={'/contact'}>
              Contact
            </Link>

            <Link to={'/blog'}>Blog</Link>
        </Container>
      </HeaderWrapper>
    )
}

Okay, so this should work if we're on the home page. But what if we're not? I still want some super smooth scroll action if I've got to hop from one route to another.

After some research, you'll find the Gatsby Browser API to be the one for you. Here we can listen for each route change, and then do something if some criterion is met.

In our gatsby-browser.js file, let's look for /#block, which you'll notice our <Link /> components are pointed to in the snippet above.

const scrollToElement = require('scroll-to-element')

exports.onRouteUpdate = ({ location }) => {
  checkHash(location)
}

const checkHash = location => {
  let { hash } = location
  if (hash) {
    scrollToElement(hash, {
      offset: -95,
      duration: 1000,
    })
  }
}

I prefer using a pre-packaged scrolling solution like scroll-to-element because it's polished & polyfilled. You could easily implement your own solution here to that end.

In any case, all we're doing here is checking to see if any hash value has been appended to the route we've just hit. If so, you know that we've returned to the home page from a non-root route. That'll get it done!

UPDATE:

I've began using a couple of helpers in my personal projects which normalize this to some extent & solve for Gatsby smooth scroll anchor links across multiple root paths. This consists of an array of menu item objects & a couple helper functions:

// consts.js
export const menuData = [
  {
    title: 'What',
    link: '/#what',
  },
  {
    title: 'How',
    link: '/#how',
  },
  {
    title: 'About',
    link: '/about'
  },
	{
    title: 'Team',
    link: '/about#team'
  },
];

// helpers.js
import React from 'react';
import scrollToElement from 'scroll-to-element';
import { map } from 'lodash';
import { Link } from 'gatsby';

export function scroller(target, offset) {
  scrollToElement(target, {
    offset,
  });
}

export function handleMenuLinkClick(l, e) {
  if (typeof window !== 'undefined' && l.link.includes('#')) {
    const [anchorPath, anchor] = l.link.split('#');
    if (window.location.pathname === anchorPath) {
      e.preventDefault();
      scroller(`#${anchor}`, -80);
    }
  }
}

export function renderLinks(linkData) {
  return map(linkData, l => {
    return (
      <Link
        key={l.link}
        title={l.title}
        to={l.link}
        onClick={e => handleMenuLinkClick(l, e)}
      >
        {l.title}
      </Link>
    );
  });
}

// MenuComponent.js
import React from 'react';
import { renderMenuLinks } from '../helpers'; 
import { menuData } from '../consts'; 

export default function Menu() {
	return (
		<Wrapper>
			{renderMenuLinks(menuData)}
		</Wrapper>
	)
}