Active Link Animation with Tailwind CSS and Framer Motion

Back to Tutorials & Guides

Preview of the sliding active link effect we are going to build

When designing a one-page website, it’s often helpful to provide users with a visual indicator of their current location on the page. One elegant way to achieve this is by implementing an animated active link in your navigation menu.

This tutorial will guide you through creating a sliding active link animation using Next.js, Tailwind CSS, and Framer Motion. We’ll develop a navigation menu where the active link is highlighted with a sliding background.

Let’s get started!

Setting up the project

We’ll use Next.js with the App Router for this project. Here’s how we’ll structure our main components:

  • A page component to hold everything together
  • A navigation provider to manage the active link state
  • A navigation menu component
  • A section component for each part of the page

Let’s break down each part.

The page component

In the page.tsx file, we’ll define our page sections:

const sections = [ { title: "Home", slug: "home" }, { title: "Customers", slug: "customers" }, { title: "Partners", slug: "partners" }, { title: "Team", slug: "team" }
];

Then, we’ll set up our main page component:

export default function SlidingActiveLinkPage() { return ( <main className="relative flex min-h-screen flex-col overflow-hidden bg-slate-50"> <NavProvider> <NavigationMenu links={sections} /> <div className="w-full max-w-5xl mx-auto px-4 md:px-6"> {sections.map((section) => ( <Section key={section.slug} section={section} /> ))} </div> </NavProvider> </main> );
}

This component serves as the main structure for our one-page website. It includes the NavigationMenu and Section client components, both wrapped in a NavProvider, that allows us to manage the state of the active section across different components. By using a context provider, we can avoid prop drilling and make the active link state accessible to both the navigation menu and the individual sections.

The navigation provider

The NavProvider is a simple context provider that manages the active link state:

"use client"; import { createContext, Dispatch, SetStateAction, useContext, useState } from "react"; type ContextProps = { activeLink: string, setActiveLink: Dispatch>,
} const NavContext = createContext({ activeLink: "", setActiveLink: (): string => "",
}) export default function NavProvider({ children
}: { children: React.ReactNode
}) { const [activeLink, setActiveLink] = useState("") return ( {children} )
} export const useNavProvider = () => useContext(NavContext)

This allows any child component to access and update the active link state, this way:

const { activeLink, setActiveLink } = useNavProvider();

The section component

Each section of our page is represented by a Section component:

"use client"; import { useRef, useEffect } from "react";
import { useNavProvider } from "./nav-provider";
import { useInView } from "framer-motion"; export default function PageSection({ section,
}: { section: { title: string; slug: string };
}) { const ref = useRef(null); const { setActiveLink } = useNavProvider(); const isInView = useInView(ref, { margin: "-50% 0px -50% 0px", }); useEffect(() => { if (isInView) { setActiveLink(section.slug); } }, [isInView]); return ( <section id={section.slug} ref={ref} className="h-screen flex justify-center items-center" > <h2 className="text-4xl font-bold text-slate-300">{section.title}</h2> </section> );
}

The key part here is the useInView hook from Framer Motion. It detects when the section is in the middle of the viewport and updates the active link accordingly.

The navigation menu component

The NavigationMenu component does several important things:

  • It uses the NavProvider to get and set the active link.
  • It creates a motion.div that slides behind the active link with a spring animation.
  • It updates the position and size of this div whenever the active link changes.
  • It checks the URL hash on load to set the initial active link.

Here’s the code:

"use client"; import { useRef, useState, useEffect } from "react";
import { useNavProvider } from "./nav-provider";
import Link from "next/link";
import { motion } from "framer-motion"; export default function NavigationMenu({ links,
}: { links: { title: string; slug: string }[];
}) { const { activeLink, setActiveLink } = useNavProvider(); const activeLinkRef = useRef<HTMLAnchorElement>(null); const [animationProps, setAnimationProps] = useState({ left: 0, width: 0, }); // check if the url has a hash and if so, set the active link useEffect(() => { const url = window.location.hash; if (url) { const link = url.replace("#", ""); setActiveLink(link); } }, []); // update the position and width of the active link underline useEffect(() => { const updateActiveLink = () => { if (activeLinkRef.current) { const { width } = activeLinkRef.current.getBoundingClientRect(); const left = activeLinkRef.current.offsetLeft; setAnimationProps({ left, width, }); } }; updateActiveLink(); window.addEventListener('resize', updateActiveLink); return () => { window.removeEventListener('resize', updateActiveLink); }; }, [activeLink]); return ( <header className="fixed top-2 md:top-6 w-full z-30"> <div className="max-w-5xl mx-auto px-4 sm:px-6"> <div className="flex h-14 w-full items-center justify-between gap-3 rounded-full border border-gray-100 bg-white px-3 shadow-lg shadow-black/[0.04]"> <div className="flex flex-1 items-center"> <a className="ml-0.5 inline-flex text-indigo-400 hover:text-indigo-500" href="#0" > <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" > <path className="fill-current" fillRule="evenodd" d="m14.862 0 2.763.738-2.073 7.709L21.67 2.35l2.022 2.015-5.656 5.637 8.475-2.263.74 2.753-7.726 2.063L28 14.82l-.74 2.753-7.672-2.05c.095-.412.146-.842.146-1.284 0-3.149-2.561-5.7-5.72-5.7a5.702 5.702 0 0 0-5.572 6.994L0 13.276l.74-2.753 7.726 2.063-6.204-6.183 2.023-2.016 5.656 5.637L7.67 1.58l2.762-.737 2.102 7.817L14.862 0Zm3.294 18.167a5.683 5.683 0 0 0 1.423-2.612l6.157 6.136-2.022 2.015-5.558-5.539Zm-.053.059a5.72 5.72 0 0 1-2.556 1.506l2.022 7.522 2.763-.738-2.23-8.29Zm-4.092 1.712c.493 0 .972-.062 1.428-.179L13.223 28l-2.762-.738 2.024-7.529c.486.134.998.205 1.526.205Zm-1.623-.232a5.721 5.721 0 0 1-2.512-1.528L4.305 23.73l2.022 2.016 6.06-6.04Zm-3.941-4.158a5.682 5.682 0 0 0 1.387 2.58L1.49 20.356l-.74-2.753 7.697-2.055Z" clipRule="evenodd" /> </svg> </a> </div> <nav className="relative flex justify-center"> <motion.div className="absolute left-0 inset-y-0 bg-indigo-100 rounded-full" aria-hidden="true" animate={{ ...animationProps, }} transition={{ type: "spring", duration: 0.5 }} ></motion.div> <ul className="relative flex flex-wrap items-center gap-3 text-sm font-medium md:gap-8"> {links.map((link) => ( <li key={link.slug}> <Link href={`#${link.slug}`} ref={activeLink === link.slug ? activeLinkRef : null} className={`inline-flex rounded-full px-3 py-1.5 text-slate-500 hover:text-indigo-500 [&.active]:text-indigo-600 ${activeLink === link.slug ? "active" : ""}`} > {link.title} </Link> </li> ))} </ul> </nav> <div className="flex flex-1 items-center justify-end"> <a className="inline-flex justify-center whitespace-nowrap rounded-full bg-indigo-500 px-3 py-1.5 text-sm font-medium text-white shadow transition-colors hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300" href="#0" > Sign up </a> </div> </div> </div> </header> );
}

The result

With these components in place, we’ve created a nice animated navigation menu that highlights the current section as users scroll through the page.

This technique can be adapted for various uses, such as creating scrollspy components for documentation sidebars or any other scenario where you need to visually represent the user’s current position in a long-form content page.

ホーム - Wiki
Copyright © 2011-2024 iteam. Current version is 2.139.0. UTC+08:00, 2024-12-27 17:29
浙ICP备14020137号-1 $お客様$