Skip to main content

Custom Smooth Scrolling

In addition to the default 'smooth' and 'auto' scroll behavior values, the component accepts a custom function that accepts the current scroll location, the target scroll location and returns a custom payload that describes a smooth scroll.

Live Editor
/**
 * Bounce easing function - https://easings.net/#easeOutBounce. This is just an example, you can use any easing function.
 */
function easeOutBounce(x: number): number {
  const n1 = 7.5625;
  const d1 = 2.75;

  if (x < 1 / d1) {
    return n1 * x * x;
  } else if (x < 2 / d1) {
    return n1 * (x -= 1.5 / d1) * x + 0.75;
  } else if (x < 2.5 / d1) {
    return n1 * (x -= 2.25 / d1) * x + 0.9375;
  } else {
    return n1 * (x -= 2.625 / d1) * x + 0.984375;
  }
}

/**
 * If the location is too far, you can return a different smooth scroll behavior
 */
function customSmoothScroll(currentTop: number, targetTop: number) {
  return {
    // increase the animation frame count to smoothen and slow down the scroll.
    animationFrameCount: 50,
    easing: easeOutBounce,
  };
}

function App() {
  const virtuoso = React.useRef<VirtuosoMessageListMethods<Message>>(null);

  return (
    <div
      className="wide-example full-code"
      style={{
        height: 500,
        display: "flex",
        flexDirection: "column",
        fontSize: "70%",
      }}
    >
      <VirtuosoMessageListLicense licenseKey="">
        <VirtuosoMessageList<Message, null>
          initialData={Array.from({ length: 100 }, (_, index) =>
            randomMessage(index % 2 === 0 ? "me" : "other"),
          )}
          ref={virtuoso}
          style={{ flex: 1 }}
          computeItemKey={({ data }) => data.key}
          initialLocation={{ index: "LAST", align: "end" }}
          ItemContent={ItemContent}
        />
      </VirtuosoMessageListLicense>

      <button
        style={{ marginTop: "1rem", fontSize: "1.1rem", padding: "1rem" }}
        onClick={(e) => {
          e.target.disabled = true;
          const myMessage = randomMessage("me");
          virtuoso.current?.data.append(
            [myMessage],
            ({ scrollInProgress, atBottom }) => {
              return {
                index: "LAST",
                align: "start",
                behavior:
                  atBottom || scrollInProgress ? customSmoothScroll : "auto",
              };
            },
          );

          setTimeout(() => {
            const botMessage = randomMessage("other");
            virtuoso.current?.data.append([botMessage]);

            let counter = 0;
            const interval = setInterval(() => {
              if (counter++ > 20) {
                clearInterval(interval);
                e.target.disabled = false;
              }
              virtuoso.current?.data.map((message) => {
                return message.key === botMessage.key
                  ? { ...message, text: message.text + " " + randPhrase() }
                  : message;
              }, "smooth");
            }, 150);
          }, 1000);
        }}
      >
        Ask the bot a question!
      </button>
    </div>
  );
}

interface Message {
  key: string;
  text: string;
  user: "me" | "other";
}

let idCounter = 0;

function randomMessage(user: Message["user"]): Message {
  return {
    user,
    key: `${idCounter++}`,
    text: randTextRange({ min: user === "me" ? 20 : 100, max: 200 }),
  };
}

const ItemContent: VirtuosoMessageListProps<Message, null>["ItemContent"] = ({
  data,
}) => {
  const ownMessage = data.user === "me";
  return (
    <div style={{ paddingBottom: "2rem", display: "flex" }}>
      <div
        style={{
          maxWidth: "60%",
          marginLeft: data.user === "me" ? "auto" : undefined,

          background: ownMessage
            ? "var(--ifm-color-primary-lightest)"
            : "#F0F0F3",
          color: ownMessage ? "white" : "black",
          borderRadius: "1rem",
          padding: "1rem",
        }}
      >
        {data.text}
      </div>
    </div>
  );
};

render(<App />);
Result
Loading...