Register Now: Use Visual Dev + AI to Ship 10x Faster on July 24

Announcing Visual Copilot - Figma to production in half the time logo
Contact Sales
Contact Sales










Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required



Get StartedLogin

‹ Back to blog

Web Development

React Intersection Observer - A Practical Guide

February 1, 2024

Written By Vishwas Gopinath

In this blog post, we'll explore how to use the Intersection Observer in a React app. We'll recreate the appealing secondary nav animations found on Linear's landing page — the reveal and highlight animations that occur on scroll. This is a fantastic way to enhance user experience and add a polished look to your website.

Setting up a Vite React app with Tailwind CSS

First, we create a Vite React app using the command npx create vite app, selecting React as the library. Next, integrate Tailwind CSS by following the simple six-step process outlined in the official Tailwind CSS documentation.

Once setup is complete, launch the development server with npm run dev and navigate to localhost:5173 to see your app in action.

While this guide uses Vite, you can also opt for Next.js or Remix as alternatives.

The default Vite React app provides basic markup, but we're aiming for a design closer to Linear's landing page. Manually coding this from scratch is time-consuming, but AI can help us achieve 80% of the goal without as much effort.

Starting with this mockup in Figma, I used the Figma plugin to convert the design into React + Tailwind CSS code using Visual Copilot.

I've also added markup for the secondary navbar and content sections, which can be found in the LandingPage.tsx file.

The primary and secondary navbars are not aligned, but this doesn’t impact our learning about the Intersection Observer.

Before implementing the animation, let's understand the Intersection Observer API and its importance for our task.

The Intersection Observer is a JavaScript API that enables actions based on the visibility of elements within the viewport. It's highly efficient for detecting when an element becomes visible or hidden, making it ideal for scroll-based animations.

Here's a basic code example:

const observer = new IntersectionObserver(callback);
const targetElement = document.querySelector("selector");

The callback function is triggered when the target element enters or exits the visible part of the screen.

To simplify the implementation of the Intersection Observer in a React app, we'll use the react-intersection-observer package. Install it in your project with the command:

npm i react-intersection-observer

This package offers a straightforward and React-friendly approach to using the Intersection Observer API.

Let's revisit the animation we're aiming to replicate:

To implement this, we need to focus on two aspects:

  1. Detecting when the wrapper of the four sections comes into view.
  2. Transitioning the navbar from hidden to visible below the primary navbar.

In the LandingPage component, start by using the useInView hook from react-intersection-observer.

{/* At the top */}
import { useInView } from "react-intersection-observer";

{/* Within the component */}
const { ref, inView } = useInView({
  threshold: 0.2,

The hook accepts a threshold option, indicating the percentage of visibility before triggering. It returns a ref and a state inView indicating whether the element is in view.

Assign the ref to the DOM element you want to monitor, which in our case is the section wrapper element.

<div id="section-wrapper" ref={ref}>
  {/* 4 sections */}

Use the inView property to conditionally apply classes to the secondary navbar, controlling visibility and transition effects:

  className={`z-20 bg-white/5 fixed flex  px-60 text-white list-none left-0 right-0 top-12 transition-all duration-[320ms] ${
      ? "opacity-100 translate-y-0 backdrop-blur-[12px]"
      : "opacity-0 translate-y-[-100%] backdrop-blur-none"
  {/* 4 nav links */}

When you scroll down to the section wrapper element in the browser, the secondary navbar will reveal itself.

The next step involves expanding and highlighting the secondary nav link based on the section in view:

To achieve this, we focus on two aspects:

  1. Detecting when each individual section comes into view.
  2. Expanding and highlighting the link corresponding to the section in view.

Instead of useInView, we use the InView component from react-intersection-observer for detecting the section in view.

This approach allows us to specify the component once within the map method, rather than invoking the hook four times (once for each section).

Update the section wrapper element as follows:

//Import at the top
import { useInView, InView } from "react-intersection-observer";

// Section wrapper
<div id="section-wrapper" ref={ref}>
  { => (
    <InView onChange={setInView} threshold={0.8} key={section}>
      {({ ref }) => {
        return (
            className="flex justify-center items-center py-[300px] text-white text-5xl"

For the InView component, we specify three props: onChange (a callback function for when the in-view state changes), threshold (a number between 0 and 1 indicating the percentage that should be visible before triggering), and key (for list rendering).

To track the current section in view, maintain a state updated by the setInView function assigned to the onChange prop. This state updates to the id of the section in view.

// Import useState
import React, { useState } from "react";

// State to track current active section
const [visibleSection, setVisibleSection] = useState(sections[0]);

// callback called when a section is in view
const setInView = (inView, entry) => {
  if (inView) {

When a section is in view, we expand the corresponding nav link to accommodate two items and change the background color. For managing the width change, we maintain separate open and closed state widths. This approach allows us to dynamically adjust the width of each nav link, enhancing the visual feedback as the user scrolls through the sections.

const menuWidths = {
  Issues: {
    open: "124px",
    closed: "65px",
  Cycles: {
    open: "128px",
    closed: "65px",
  Roadmaps: {
    open: "178px",
    closed: "94px",
  Workflows: {
    open: "176px",
    closed: "92px",

Update the state of each secondary nav link based on the visibleSection state, and adjust the background color accordingly.

{ => (
      className={`transition-all duration-300 flex rounded-full border border-white/5 bg-white/5 overflow-hidden  px-3 py-0.5 backdrop-blur-none`}
          visibleSection === section
            ? menuWidths[section].open
            : menuWidths[section].closed,
        className={`-ml-2 mr-2 px-2 ${
          visibleSection === section
            ? `bg-indigo-500/70 border-indigo-50 rounded-full`
            : ``

And there you have it — our scroll based animation using Intersection Observer with React and Tailwind CSS. Here’s the final code.

Introducing Visual Copilot: convert Figma designs to code using your existing components in a single click.

Try Visual Copilot


Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

Like our content?

Join Our Newsletter

Continue Reading
The AI-Driven Evolution of CMSs: From Text Boxes to Generative UI
WRITTEN BYSteve Sewell
July 18, 2024
The Truth About AI's Impact on Software Development Jobs
WRITTEN BYSteve Sewell
July 17, 2024
Structured Data: What It Is and Why You Need It
July 15, 2024