Smooth Scrolling to an Element using scrollIntoView in React with a Fixed Header

In modern web development, creating scrolling effects has become a common practice to enhance user experience. Smooth scrolling to a specific element is often accompanied by a fixed header that remains visible while scrolling through content. This combination creates a seamless user experience, allowing users to navigate through sections effortlessly.

But there are often issues with fixed or sticky headers while scrolling to an element. The header can block the element from view. This can be frustrating for users, as they may have to scroll past the header to see the element they’re interested in.

In this blog post, we will explore how to implement smooth scrolling using scrollIntoView in a React application while incorporating a fixed header.

Before diving into the example, let’s understand about native scrollIntoView method in JavaScript.

ScrollIntoView

The scrollIntoView method is used to scroll an element into the visible area of the browser window.

Syntax

scrollIntoView(alignToTop, scrollIntoViewOptions)

alignToTop(optional)
  
  true: The top of the element will be aligned to the top of the 
        visible area of the scrollable ancestor. The default is true.
  false: The bottom of the element will be aligned to the 
        bottom of the visible area of the scrollable ancestor. 

scrollIntoViewOptions(optional)
  An Object with the following properties: 
  
  behavior: Determines scroll animation
  - smooth: Smooth scrolling
  - instant: Instant scrolling in a single jump
  - auto: Scroll behavior is determined by the computed value of scroll-behavior

  block: Defines vertical alignment (start, center, nearest)

  inline: Defines horizontal alignment. (start, center, nearest)

Return value: None(undefined)

During one of our client projects, we encountered a specific need to implement scrolling to a particular element. The scenario involved a list of upcoming meetings, some of which may have already ended. Our objective was to ensure that the user always sees the next upcoming meeting on the list, without modifying the existing logic.

To fulfill this requirement, we incorporated a feature that enables scrolling directly to the subsequent upcoming meeting.

In the following sections, we will delve into the implementation details of this functionality.

We used Material UI as a component library for our project.

In constant.js, we defined the data, and in App.js we loop over the sub-headers to get the meeting list. Note, the sub-headers are fixed.

//App.js

export default function App() {
  const theme = useTheme();
  return (
    <>
      <Typography variant="h5" align="center">
        Meetings
      </Typography>
      <List>
        {subHeaders.length > 0 &&
          subHeaders.map((header, headerIndex) => (
            <List key={`section-${headerIndex}`}>
              <ListSubheader sx={{ backgroundColor: theme.palette.grey[200] }}>
                {header}
              </ListSubheader>
              <MeetingList header={header} key={`list-${headerIndex}`} />
            </List>
          ))}
      </List>
    </>
  );
}

We make use of a useRef to store a reference to the element that we want to scroll to i.e. the next upcoming meeting.

//MeetingList.js

const MeetingList = ({ header }) => {
  const ref = useRef(null);
  return meetings[header].map((meeting, index) => {
    return (
      <React.Fragment key={`list-item-${index}`}>
        <MeetingListItem
          meeting={meeting}
          index={index}
          ref={meeting.id === upcomingMeeting.id ? ref : null}
        />
      </React.Fragment>
    );
  });
};

We wanted to scroll to the next upcoming meeting as soon as the list is loaded. So we called the scrollIntoView method on the ref object in useEffect.

//MeetingListItem.js

const MeetingListItem = forwardRef(function MeetingListItem({ meeting, index }, ref) {
  const theme = useTheme();

  useEffect(() => {
    if (ref?.current) {
      ref.current.scrollIntoView({ behavior: "instant", block: "start" });
    }
  }, [ref]);

  return (
    <ListItem
      dense
      key={index}
      ref={ref}
    >
      <ListItemButton
        disabled={meeting.ended}
        selected={isEqual(upcomingMeeting, meeting)}
      >
        <ListItemText
          primary={<Typography variant="body1">{meeting.name}</Typography>}
        />
      </ListItemButton>
    </ListItem>
  );
});

The fixed header issue while scrolling to the element

Let’s see the scrolling in action.

The header overlaps with the scrolled element, which is not visually appealing.

Solution using scroll-margin CSS property

To fix this, we can use the CSS property scroll-margin.

In React, we write CSS properties in camelCase, so scroll-margin becomes scrollMargin.

MUI details:

MUI provides a prop called sx to specify styling.

We calculate the scrollMargin based on the height of the sub-header. In this case, the height is 48px, which is written in MUI as theme.spacing(6), where 1 spacing unit equals 8px.

return (
    <ListItem
      dense
      key={index}
      //This is needed for fixing the overlapping issue on the scroll.
      //theme.spacing(6) => 48px(8*6) spacing is the height of the sub-header
      sx={{ scrollMargin: theme.spacing(6) }}
      ref={ref}
    >
      <ListItemButton
        disabled={meeting.ended}
        selected={isEqual(upcomingMeeting, meeting)}
      >
        <ListItemText
          primary={<Typography variant="body1">{meeting.name}</Typography>}
        />
      </ListItemButton>
    </ListItem>
  );

Our issue is now resolved, thanks to the changes we have made.

Conclusion:

  • The scrollIntoView method is a powerful tool that can be used to improve the user experience of our React applications.
  • By using scrollIntoView, we can make it easier for users to find the information they are looking for, and highlight important content.
  • The scroll-margin CSS property can be used to prevent the header from overlapping with the scrolled element.

Need help on your Ruby on Rails or React project?

Join Our Newsletter