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
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@^22.x is frozen at v2.1.5 on the
v2branch 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-jsOr 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.stickies → readonly 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.
