Scroll indicator component in React

Jan 24, 2021

react
components

Scrolling is probably one of the most used actions handled by website users. Several times I came across a situation in which I did not notice that there is still some content to read and discovered this just by accident. In this short tutorial, I want to show how to create a simple scroll indicator. This element might help to highlight that it is still content to scroll down if the user didn’t reach the end of the article or just a container with the text.

I will show how to implement such element using React (hooks) with Styled Components.

This is how the final component looks like in action:

Scroll indicator

Scroll Indicator should be reusable, so it can be injected in any desired place in the code and should work inside any container with content that might be scrollable.

Here’s the code for the component:

scroll-indicator.tsx

import React from "react"
import { useEffect, useState } from "react"
import { Indicator } from "./scroll-indicator-styles"
import { Options } from "./scroll-indicator.model"

interface Props {
  container: HTMLElement
  options?: Options
}

export const ScrollIndicator: React.FC<Props> = props => {
  const { container } = props
  const [isScrollNeeded, setScrollValue] = useState<boolean>(true)
  const [isScrollable, setScrollable] = useState<boolean>(false)

  const handleScroll = (): void => {
    const scrollDiv = container
    const result =
      scrollDiv.scrollTop < scrollDiv.scrollHeight - scrollDiv.clientHeight ||
      scrollDiv.scrollTop === 0

    setScrollValue(result)
  }

  const checkIfScrollable = (el: HTMLElement) => {
    return el && el.scrollHeight > el.offsetHeight
  }

  useEffect(() => {
    setScrollable(checkIfScrollable(container))
    container.addEventListener("scroll", handleScroll)
    return () => container.removeEventListener("scroll", handleScroll)
  }, [container, handleScroll])

  return isScrollable && isScrollNeeded && <Indicator options={props.options} />
}

The component takes the container prop which is just HTMLElement. The second prop, options is not required. In the app where I use the indicator, I found only one option that might be passed here. But it’s easily extendable so probably in further development I can pass more options to it. The core part of this component is inside the useEffect hook. First, the container dimensions are calculated to check if the scrollbar is already visible. To check it, there’s a need to compare the scrollHeight and offsetHeight properties values.

Then, the scroll event is added to the container. Every time the scroll is fired inside the container, the handleScroll function is comparing the actual values of scrollHeight, clientHeight, and scrollTop properties. If both state values are true then the indicator will show up inside the container.

Here’s how the indicator styles look like:

scroll-indicator-styles.ts

import styled, { keyframes } from "styled-components"
import { Options } from "./scroll-indicator.model"

const getPosition = (pos: string) => {
  switch (pos) {
    case "right":
      return "95%"
    case "left":
      return "5%"
    default:
      return "50%"
  }
}

const indicatorKeyframes = keyframes`
  0% { bottom: 1.5rem; opacity: 1; }
  50% { bottom: 0.2rem; opacity: 0.2; }
  100% { bottom: 1.5rem; opacity: 1; }
`

export const Indicator = <{ options?: Options }>`
  position: sticky;
  left: ${props => props.options ? getPosition(props.options.position) : getPosition()};
  top: 50%;
  width: 0;
  height: 0;
  transform: translate(-50%, -50%);
  -webkit-animation: ${indicatorKeyframes} 2s infinite ease-in-out;
  animation: ${indicatorKeyframes} 2s infinite ease-in-out;
  z-index: 10;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 10px solid ${props => props.color || colors.lightBrown};
`

I use a model with only one property (so far) here, which might be considered overkill in that case. But, as it was mentioned earlier, probably more options will show up in the further development. In the default position, the indicator is centered at the bottom of its container.

If there’s a need to set the other position, just pass the appropriate value: left or right. This is handled by the getPosition function. The colors here are imported from the separate file where I keep all colors declaration. That helps a lot to keep the styling settings in order.

The last thing is the implementation of the ScrollIndicator component. Just place the component inside the container:

text-container.tsx

import React, { useRef } from "react"
import { ScrollIndicator } from "./scroll-indicator/scroll-indicator"

const TextContainer: React.FC = props => {
  const containerRef = useRef(null)

  return (
      <div ref={containerRef}>
        {...very long text here}
        {containerRef.current && <ScrollIndicator container={containerRef.current} />}
      </div>
  )
}

That’s it. The only thing that is needed here is to check if the parent container is already rendered.

Conclusion

The scroll indicator helps to draw attention that there’s more content to view. The component itself is very easy to implement in every place in your code.