1

So I have the following sticky div React component which I called RelativeSticky:

import React, { useRef, useMemo, useLayoutEffect } from "react";
import { compose } from "js-utl";
import { classNames } from "react-js-utl/utils";
import { useWindowRef } from "react-js-utl/hooks";
import "./relative-sticky.css";

const getScrollY = element =>
  element === window ? element.scrollY : element.scrollTop;

const RelativeSticky = compose(React.memo)(function RelativeSticky({
  className = void 0,
  children,
  scrollElementRef = void 0,
  disable = false,
  topThresold = 0
} = {}) {
  const windowRef = useWindowRef();
  scrollElementRef =
    useMemo(() => scrollElementRef, [scrollElementRef]) || windowRef;
  const ref = useRef(null);
  const holderRef = useRef(null);
  const topCallback = useMemo(
    () => () => {
      if (!ref.current || !scrollElementRef.current || !holderRef.current) {
        return;
      }
      const { y } = ref.current.getBoundingClientRect();
      const scrollTop = getScrollY(scrollElementRef.current);
      const absTop = scrollTop + y;
      const topThresholdOffset =
        typeof topThresold === "function" ? topThresold() : topThresold;
      const top = Math.max(0, scrollTop - absTop + topThresholdOffset);
      holderRef.current.style.transform = `translateY(${disable ? 0 : top}px)`;
    },
    [scrollElementRef, topThresold, disable]
  );
  useLayoutEffect(() => {
    const el = scrollElementRef.current;
    el && el.addEventListener("scroll", topCallback);
    topCallback();
    return () => {
      el.removeEventListener("scroll", topCallback);
    };
  }, [scrollElementRef, topCallback]);

  return (
    <div className={classNames("relative-sticky", className)} ref={ref}>
      <div className="relative-sticky-holder" ref={holderRef}>
        {children}
      </div>
    </div>
  );
});
RelativeSticky.displayName = "RelativeSticky";
export default RelativeSticky;

Which I use like this:

import ReactDOM from "react-dom";
import React from "react";
import RelativeSticky from "./RelativeSticky";
import "./styles.css";

function Example() {
  return (
    <React.Fragment>
      <header>HEADER</header>
      <div className="padding" />
      <div className="container">
        {/* For the sticky div I cannot use "position: fixed;"
            because ".container" has "transform: translate(0, 0);"
            and I cannot change the style of ".container"
            because it is added by an external library I am using.
            Indeed, you can see that the ".fixed" element is not fixed even though
            it has "position: fixed;"...

            The only option would be to use a relative-sticky div,
            but when scrolling the translation of the top position of this
            sticky div is janky and jumps and not smooth
            (especially when scrolling fast)...
            */}
        <RelativeSticky topThresold={50 + 10}>
          <div className="sticky-element">STICKY</div>
        </RelativeSticky>
        <div className="fixed">FIXED</div>
      </div>
    </React.Fragment>
  );
}

ReactDOM.render(<Example />, document.getElementById("root"));

You can check this Codesandbox to see it in action: https://codesandbox.io/s/react-example-vc0x2

The problem is that the "stickiness" is not smooth but janky. The changing of the style.transform = translateY() property in the scroll event listener causes the div to make small jumps when scrolling.

Is there a way to correct this behaviour and keep the translation smooth even when scrolling fast?

Thanks for the attention.

NOTE: You may wonder why didn't I use position: fixed;. The thing is that position: fixed doesn't work when the container of the fixed div has transform: translate(0, 0), as explained here -> Positions fixed doesn't work when using -webkit-transform and I cannot avoid this transform: translate(0, 0) setting on container because it is added by a library I am using, so I guess the only option would be to use relative positioning...

tonix
  • 6,671
  • 13
  • 75
  • 136

0 Answers0