DEV Community

Marius Ibsen
Marius Ibsen

Posted on • Updated on

React Scroll Hook with Shadows

Have you ever thought of spicing up your scrollable areas with nice-looking shadows? Yes, all browsers have the scroll bar, but we can make the scrollable content shine even better and, at the same time, improve usability. I think shadows add a smooth and excellent feel to our content and help by showing the users that "there is more content down here" or "there is more content up here".

If you’ve had the same thought (I’ll guess so since you’re already reading this) but have not figured out a solution yet, you should read on.

The Scroll Hook

Recently, I made this hook that adds inner shadows when scrolling an element that have overflowing content. We’ll break down the code throughout the post, see how it works, and see how we use it in our code.

import { useState } from 'react';

export function useScrollWithShadow() {
  const [scrollTop, setScrollTop] = useState(0);
  const [scrollHeight, setScrollHeight] = useState(0);
  const [clientHeight, setClientHeight] = useState(0);

  const onScrollHandler = (event) => {
    setScrollTop(event.target.scrollTop);
    setScrollHeight(event.target.scrollHeight);
    setClientHeight(event.target.clientHeight);
  };

  function getBoxShadow() {
    const isBottom = clientHeight === scrollHeight - scrollTop;
    const isTop = scrollTop === 0;
    const isBetween = scrollTop > 0 && clientHeight < scrollHeight - scrollTop;

    let boxShadow = 'none';
    const top = 'inset 0 8px 5px -5px rgb(200 200 200 / 1)';
    const bottom = 'inset 0 -8px 5px -5px rgb(200 200 200 / 1)';

    if (isTop) {
      boxShadow = bottom;
    } else if (isBetween) {
      boxShadow = `${top}, ${bottom}`;
    } else if (isBottom) {
      boxShadow = top;
    }
    return boxShadow;
  }

  return { boxShadow: getBoxShadow(), onScrollHandler };
}
Enter fullscreen mode Exit fullscreen mode

Handling scroll events

Firstly, let’s go through the scroll-logic part of the useScrollWithShadow-hook.

import { useState } from "react";

export function useScrollWithShadow() {
  const [scrollTop, setScrollTop] = useState(0);
  const [scrollHeight, setScrollHeight] = useState(0);
  const [clientHeight, setClientHeight] = useState(0);

  const onScrollHandler = (event) => {
    setScrollTop(event.target.scrollTop);
    setScrollHeight(event.target.scrollHeight);
    setClientHeight(event.target.clientHeight);
  };

  return { onScrollHandler };
}
Enter fullscreen mode Exit fullscreen mode

onScroll is a React property that we can get on any HTML element for listening on scroll events, such as a div. It takes a function and returns an event of the target element. In native JavaScript, this would be somewhat equivalent to element.addEventListener(‘scroll’, () => /* do something*/)

The onScrollHandler-method (also exported from the hook) we can pass as the value to the onScroll-property of an element. When scrolling the element, the onScrollHandler-method triggers, then we pick the properties scrollTop, scrollHeight and clientHeight from the event target and set them as values for their respective states.

Illustration of scrollHeight and clientHeight on an element with overflowing children elements

  • clientHeight (visible content) is the inner height of an element, including padding. We can calculate it by CSS height + CSS padding. If a horizontal scrollbar is present, the inner height includes the height of the scrollbar.
  • scrollHeight (visible + hidden content) is the height of an element’s visible content, including hidden content not visible on the screen for an overflowing element.
  • scrollTop is the number of pixels scrolled vertically on the content of an element. Measuring the value takes the distance from the top of an element to the topmost of visible content.

Calculate shadow position

We have to do some calculations to determine if and when we should show shadows on the top, bottom, or both. We will have shadows on the top and bottom of the element when we’re not at the top of the scroll or the bottom.

const isBottom = clientHeight === scrollHeight - scrollTop;
const isTop = scrollTop === 0;
const isBetween = 
     scrollTop > 0 && clientHeight < scrollHeight - scrollTop;
Enter fullscreen mode Exit fullscreen mode
  • isBottom: true when the inner height of the visible element equals the total height of the visible and hidden content minus (-) pixels scrolled from the top of the element.
  • isTop: true when we’ve not scrolled yet, or scrollTop equals 0 pixels.
  • isBetween: true when we’ve scrolled more than 0 pixels from the top, and the inner height of the visible element is less (<) than the visible plus (+) hidden content height minus pixels scrolled from the top.

Below is an example of an element that has 400 pixels inner height, and we have scrolled 150 pixels from the top; we should then get that isBetween is true:

// Some element inner height: 400px
// Totalt height of the visible and hidden content: 700px
// Pixels scrolled from top of the element: 150px

const isBottom = 400 === 700 - 150;           // false
const isTop = 150 === 0;                      // false
const isBetween = 150 > 0 && 400 < 700 - 150; // true
Enter fullscreen mode Exit fullscreen mode

Determine shadow type based on the calculated position

Our getBoxShadow()-function handles the logic for returning the correct CSS box-shadow value as a string. This value we want to set to the target element as we scroll. Here we will use the variables of the calculation to determine when we should use the different shadows:

function getBoxShadow() {
    const isBottom = clientHeight === scrollHeight - scrollTop;
    const isTop = scrollTop === 0;
    const isBetween = 
     scrollTop > 0 && clientHeight < scrollHeight - scrollTop;    let boxShadow = "none";
    const top = "inset 0 8px 5px -5px rgb(200 200 200 / 1)";
    const bottom = "inset 0 -8px 5px -5px rgb(200 200 200 / 1)";

    if (isTop) {
      boxShadow = bottom;
    } else if (isBetween) {
      boxShadow = `${top}, ${bottom}`;
    } else if (isBottom) {
      boxShadow = top;
    }
    return boxShadow;
}
Enter fullscreen mode Exit fullscreen mode

We want to set the bottom shadow if we are at the top of the element. If we are between (as we calculated in the previous code example), we want to show both top and bottom shadows, or else if we are at the bottom of the element, we want to show the top shadow.

Using the Scroll Hook

To use the hook, we need to create a component that can use it. In the code block below, we have a component called App. We import the useScrollWithShadow-hook, destruct the onScrollHandler()-method and the boxShadowvariable. We pass onScrollHandler() as the value of the div’ onScroll property and set boxShadow as an inline style.

import { useScrollWithShadow } from "./hooks/useScrollWithShadow";

export function App() {
  const { boxShadow, onScrollHandler } = useScrollWithShadow();

  return (
    <div onScroll={onScrollHandler} style={{ boxShadow }}>
      /* ... Scroll area here ... */
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Without some CSS, this would not work very well. See the final result for the styled solution with content and source code on Codesandbox.

Final result

Here is the final result. Click “toggle shadows” to switch between with and without shadows (source code on Codesandbox):

Thank you for reading! Code for shizzle

(This article was originally posted on Medium )

Oldest comments (0)