Ponyfill

This package ponyfills new features in the Element.scrollIntoView API. Including features in the non-standard Element.scrollIntoViewIfNeeded API, that the CSS working group is proposing to add to scrollIntoView as another option: scrollMode: "if-needed".

Minimal

The core package scroll-into-view-if-needed is unapologetically small and dependency-free. That is why behavior: "smooth" is only ponyfilled if you use the add-on package smooth-scroll-into-view-if-needed. You don't need the add-on if native smooth scrolling is enough.

Customizable

A couple of options not in the spec by the CSS working group helps customize scrolling to your needs. Avoid scrolling unwanted parent elements by using the boundary: Element option. Pass a function to behavior to customize exactly how elements are scrolled when smooth isn't enough.

Scrolling if needed

When deciding if scrolling is needed the visibility of the target element is checked. If it's less than 100% it will be scrolled.

By default the browser controls the scrolling when behavior: 'smooth' (unless you opt in to the ponyfill). Note there's browser differences with native smooth scrolling, like Chrome vs FireFox in this CodePen.

import scrollIntoView from 'smooth-scroll-into-view-if-needed';

scrollIntoView(node, {
  behavior: 'smooth',
  scrollMode: 'if-needed',
});
Scroll to 
A
B
C
D
E
F
G

Scroll alignment

The position options for both block and inline are supported. Mix and match to your heart's content.

Usually block aligns vertically, while inline aligns horizontally. It depends on the writing-mode.

import scrollIntoView from 'scroll-into-view-if-needed';

scrollIntoView(node, {
  behavior: 'smooth',
  block: 'center',
  inline: 'center',
});
Scroll to 
1
2
3
4
5
6
7
8
9

Limit propagation

Boundaries are good, that's what people keep saying. If you want some elements to scroll into view, but not all of the parents then boundary is the answer.

Keep in mind this is a non-standard feature not in any spec.

import scrollIntoView from 'scroll-into-view-if-needed';

scrollIntoView(node, {
  behavior: 'smooth',
  block: 'end',
  boundary: document.getElementById('example-boundary'),
});
Scroll to 
😁
🤯
😅
🤔
🤩
🤨
😲

Custom transition: Motion

If you want a different easing, duration or another creative direction you can pass a function to behavior.

Just like boundary this is not in the spec. The function is given the scroll positions, and you decide how to get there. In this example a spring transition from Motion animates the scroll position.

import scrollIntoView from 'scroll-into-view-if-needed';
import { animate } from 'motion';

scrollIntoView(node, {
  behavior: (actions) =>
    actions.forEach(({ el, top, left }) => {
      animate(el.scrollTop, top, {
        type: 'spring',
        bounce: 0.3,
        onUpdate: (value) => (el.scrollTop = value),
      });
      animate(el.scrollLeft, left, {
        type: 'spring',
        bounce: 0.3,
        onUpdate: (value) => (el.scrollLeft = value),
      });
    }),
  inline: 'center',
});
Scroll to 
🌎
🌍
🌏

Custom transition: el.animate

You don't need an animation library for custom transitions, the browser has one built in.

The Element.animate() method from the Web Animations API can't animate the scroll position directly. Instead the element is scrolled instantly, and its content is animated from where it was to where it ends up. The animation can run off the main thread and adds nothing to your bundle.

import scrollIntoView from 'scroll-into-view-if-needed';
// No animation library needed, Element.animate() is built into the browser

scrollIntoView(node, {
  behavior: (actions) =>
    actions.forEach(({ el, top, left }) => {
      const content = el.firstElementChild;
      const dx = left - el.scrollLeft;
      const dy = top - el.scrollTop;
      // Jump to the new scroll position, then animate the
      // content from where it was to where it is now (FLIP)
      el.scroll({ top, left, behavior: 'instant' });
      content.animate(
        [
          { transform: `translate(${dx}px, ${dy}px)` },
          { transform: 'none' },
        ],
        {
          duration: 600,
          easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
        }
      );
    }),
  inline: 'center',
});
Scroll to 
🌎
🌍
🌏