npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

stuck-js

v3.0.0

Published

A sticky library that stacks multiple sticky elements and keeps them aligned on horizontal scroll, with no dependencies

Downloads

846

Readme

Stuck.js

CI npm version downloads

A sticky library that stacks multiple sticky elements on top of each other and keeps them aligned when the page scrolls horizontally. No dependencies — jQuery not required.

Demo: https://ryonkmr.github.io/stuck-js/

[!IMPORTANT] v3 builds on the browser's own position: sticky, so it does not run on IE11. Every other browser released since 2017 supports it. If you still need IE11, stay on the 2.x line — it computes positions in JavaScript and has no such requirement:

$ npm i -S stuck-js@^2

2.x is frozen at v2.1.5 on the v2 branch and is no longer developed. That is a safe place to sit: the package ships no runtime dependencies, so there is no supply chain underneath it to rot, and nothing to patch when someone else's package has a CVE. It will keep working for as long as the browsers you target do.

See Migrating from v2 for what changed in v3.

Quickstart

Setup

Install it from npm:

$ npm i -S stuck-js

Or grab a build from the GitHub releases.

Usage

<style>
  header {
    height: 100px;
    z-index: 100;
  }
  .ad {
    width: 300px;
    height: 250px;
  }
</style>
<body>
  <header style="height: 100px; z-index: 100;">
    <h1>This is my first website</h1>
    <!-- header contents -->
  </header>
  <div>
    <main>
      <!-- main contents -->
    </main>
    <div id="js-sidebar">
      <aside class="js-sticky-ad ad ad--01"><!-- ad contents --></aside>
      <aside class="js-sticky-ad ad ad--02"><!-- ad contents --></aside>
    </div>
  </div>
  <script src="https://unpkg.com/stuck-js"></script>
  <script>
  const Stuck = StuckJs.Stuck;
  const instances = new Stuck([
    { selector: '#js-header', marginTop: 0 },
    { selector: '.js-sticky-ad', wrapper: '#js-sidebar' },
  ], { marginTop: 10 });
  </script>
</body>

Or with a bundler:

import { Stuck } from 'stuck-js'

const instances = new Stuck([
  { selector: '#js-header', marginTop: 0 },
  { selector: '.js-sticky-ad', wrapper: '#js-sidebar' },
], { marginTop: 10 })

API

The package exports Stuck (also the default export) and Sticky. Stuck is what you normally use: it resolves selectors, creates one Sticky per element and keeps them stacked.

Positioning itself is left to the browser. Each element gets position: sticky, and the library only decides what top it should stick at — the sum of the heights of the stickies above it. There is no scroll handler.

Options

Every setting accepts these, and Stuck takes the same shape as per-instance defaults:

| Option | Type | Default | Description | | --- | --- | --- | --- | | marginTop | number | 0 | Gap left above the element once it sticks, measured from the bottom of the sticky above it, or from the top of the window when nothing is stacked above. | | wrapper | string \| HTMLElement | the element's parentElement, else document.body | Node that bounds the stacking. The sticky stops once it reaches the wrapper's bottom edge. Because this is position: sticky's own containing block, the wrapper has to be an ancestor of the element. | | observe | boolean | true | Watch the element with a ResizeObserver so the stack re-stacks when its height changes. Set to false if the element never resizes. |

A setting also needs a target, given as either a selector or elements:

type StickySetting = StickyOptions & (
  | { selector: string }
  | { element: HTMLElement | HTMLElement[] | NodeList }
)

new Stuck(settings?, defaultOptions?, sharedStacking?)

| Argument | Type | Default | Description | | --- | --- | --- | --- | | settings | StickySetting \| StickySetting[] | [] | What to stick. Elements already registered by another instance are skipped. | | defaultOptions | StickyOptions | { observe: true } | Applied to every setting; a setting's own options win. | | sharedStacking | boolean | true | Whether these stickies join the stack shared with other Stuck instances. With false they stack only among themselves. |

Throws if a setting has neither selector nor element.

stuck.create(settings, sharedStacking?)Sticky[]

Registers more elements on an existing instance and re-stacks everything. Returns the newly created stickies (empty if every element was already registered).

stuck.stickiesreadonly Sticky[]

The stickies this instance owns, ordered by their position on the page.

stuck.destroy()

Destroys every sticky it owns, restores the original DOM and re-stacks the remaining stickies. The instance should not be reused afterwards — create a new one instead.

new Sticky(element, options?, activate?, onUpdate?)

The single-element primitive Stuck builds on. Use it directly when you already have the element and do not need stacking across instances.

| Argument | Type | Default | Description | | --- | --- | --- | --- | | element | HTMLElement | — | Required. | | options | StickyOptions | { observe: true } | Same options as above. | | activate | boolean | true | Join the shared stack immediately. | | onUpdate | () => void | no-op | Called after a resize triggers a re-stack. |

| Member | Type | Description | | --- | --- | --- | | element | HTMLElement | The element being stuck. | | options | StickyOptions | Resolved options, with marginTop always present. | | offsetTop | number | The top it sticks at, including the stickies stacked above it. | | update() | void | Re-runs the stack calculation. Called for you when a height changes. | | destroy() | void | Restores the DOM and stops observing. Safe to call twice. |

DOM and styling

The element gets a data-stuck attribute — "true" while stuck, "" otherwise — so you can style both states:

.my-sticky[data-stuck='true'] { box-shadow: 0 2px 8px rgba(0, 0, 0, .2); }

To know whether an element is currently stuck, a zero-height div is inserted immediately before it and watched with an IntersectionObserver — the element itself cannot tell you, since a sticky element pinned at top: 0 and one that simply starts there look identical.

That sentinel counts as a sibling, so structural selectors shift by one:

/* does not do what you want: the sentinel is now the first child */
.item:nth-child(n + 2) { margin-top: 30px; }

/* address the elements directly instead */
.item { margin-top: 30px; }
.item:first-of-type { margin-top: 0; }

:first-of-type is safe as long as your sticky elements are not divs. When they are, target them by class or id.

Migrating from v2

| v2 | v3 | | --- | --- | | Wrapped every sticky in a placeholder div | Inserts a zero-height sentinel div before it | | Placeholder was exported | Removed — the browser reserves the space now | | sticky.marginTop was the computed offset | sticky.offsetTop | | sticky.rect, sticky.floor, sticky.isStickToBottom | Removed — the browser owns positioning | | wrapper could be any element | Must be an ancestor of the sticky element | | Ran a scroll handler on every frame | No scroll handler |

new Stuck(...), stuck.create(), stuck.stickies, stuck.destroy(), the three options and the data-stuck attribute all behave as before.