react-sticky-kit 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Saiya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # React Sticky Kit
2
+
3
+ <p align="left">
4
+ <a href="https://www.npmjs.com/package/react-sticky-kit" target="_blank">
5
+ <img src="https://img.shields.io/npm/v/react-sticky-kit.svg?style=flat-square" alt="NPM Version" />
6
+ </a>
7
+ <a href="https://github.com/oe/react-sticky-kit" target="_blank">
8
+ <img src="https://img.shields.io/github/stars/oe/react-sticky-kit?style=flat-square" alt="GitHub Stars" />
9
+ </a>
10
+ <a href="https://github.com/oe/react-sticky-kit/actions" target="_blank">
11
+ <img src="https://github.com/oe/react-sticky-kit/actions/workflows/ci.yml/badge.svg" alt="Build Status" />
12
+ </a>
13
+ <a href="https://github.com/oe/react-sticky-kit/blob/main/LICENSE" target="_blank">
14
+ <img src="https://img.shields.io/github/license/oe/react-sticky-kit?style=flat-square" alt="License" />
15
+ </a>
16
+ </p>
17
+
18
+ A lightweight, flexible React sticky container and item component library. Easily create sticky headers, sections, and advanced sticky layouts with support for multiple modes and edge cases.
19
+
20
+ ## Features
21
+
22
+ - 📦 Simple API: `<StickyContainer>` and `<StickyItem>`
23
+ - 🧩 Supports `replace`, `stack`, and `none` sticky modes
24
+ - 🏷️ Customizable offset, z-index (baseZIndex), and sticky logic
25
+ - 🧪 Handles edge cases: empty sections, dynamic heights, zero-height headers, long headers, etc.
26
+ - ⚡️ Written in TypeScript, fully typed
27
+ - 🧪 Includes demo pages for real-world scenarios
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install react-sticky-kit
33
+ # or
34
+ yarn add react-sticky-kit
35
+ # or
36
+ pnpm add react-sticky-kit
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```tsx
42
+ import { StickyContainer, StickyItem } from 'react-sticky-kit';
43
+ import 'react-sticky-kit/dist/style.css'; // Import styles for sticky components
44
+
45
+ export default function Example() {
46
+ return (
47
+ <StickyContainer offsetTop={48} defaultMode="stack" baseZIndex={300}>
48
+ <StickyItem>
49
+ <div>Sticky Header</div>
50
+ </StickyItem>
51
+ <div>Content...</div>
52
+ <StickyItem mode="replace">
53
+ <div>Another Sticky Header (replace mode)</div>
54
+ </StickyItem>
55
+ <div>More Content...</div>
56
+ </StickyContainer>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ## Props
62
+
63
+ ### `<StickyContainer />`
64
+ | Prop | Type | Default | Description |
65
+ |-----------------------------|--------------------------------------|-------------|---------------------------------------------------------------------------------------------|
66
+ | `offsetTop` | `number` | `0` | Offset from the top of the viewport |
67
+ | `defaultMode` | `'replace' \| 'stack' \| 'none'` | `'replace'`| Default sticky mode for all items |
68
+ | `baseZIndex` | `number` | `200` | Base z-index for sticky items. Should be greater than the number of sticky items. |
69
+ | | | | In `replace` mode, z-index = baseZIndex - index; in `stack` mode, z-index = baseZIndex + index. |
70
+ | `onStickyItemsHeightChange` | `(height: number) => void` | | Callback when total sticky height changes |
71
+
72
+ ### `<StickyItem />`
73
+ | Prop | Type | Default | Description |
74
+ |---------|--------------------------------------|---------|---------------------------------------------|
75
+ | `mode` | `'replace' \| 'stack' \| 'none'` | | Sticky mode for this item (overrides group) |
76
+
77
+ ## Sticky Modes
78
+ - **replace**: Only one sticky item is visible at a time, replacing the previous.
79
+ - **stack**: Sticky items stack on top of each other.
80
+ - **none**: Sticky is disabled for this item.
81
+
82
+ ## Demo
83
+
84
+ Run the demo locally:
85
+
86
+ ```bash
87
+ pnpm install
88
+ pnpm dev
89
+ ```
90
+
91
+ Open [http://localhost:5173](http://localhost:5173) and switch between demo pages to explore all features and edge cases.
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,50 @@
1
+ import { default as default_2 } from 'react';
2
+ import { JSX } from 'react/jsx-runtime';
3
+
4
+ export declare interface IStickyContainerProps extends default_2.HTMLAttributes<HTMLDivElement> {
5
+ children: default_2.ReactNode;
6
+ /**
7
+ * The offset from the top of the viewport for sticky elements. Default is 0.
8
+ */
9
+ offsetTop?: number;
10
+ /**
11
+ * base z-index for sticky items. Default is 200. minimum z-index is 20.
12
+ * * - When using the `replace` mode, the z-index of a `StickyItem` is calculated as `baseZIndex` minus its index within the container.
13
+ * * - When using the `stack` mode, the z-index of a `StickyItem` is calculated as `baseZIndex` plus its index.
14
+ * * should be greater than number of sticky items in the container.
15
+ * * for performance reasons, when baseZIndex is changed, the component will not re-render.
16
+ * * use it when you need nest StickyContainer or need to change z-index of sticky items.
17
+ */
18
+ baseZIndex?: number;
19
+ /**
20
+ * Default sticky mode for the group. 'none' disables sticky behavior.
21
+ */
22
+ defaultMode?: 'replace' | 'stack' | 'none';
23
+ /**
24
+ * Callback triggered when the total height of all currently sticky items changes.
25
+ * @param height The total height of all sticky items inside the container.
26
+ */
27
+ onStickyItemsHeightChange?: (height: number) => void;
28
+ }
29
+
30
+ export declare interface IStickyItemProps extends default_2.HTMLAttributes<HTMLDivElement> {
31
+ children: default_2.ReactNode;
32
+ /**
33
+ * Sticky mode for this item. Defaults to the group's mode if not specified.
34
+ */
35
+ mode?: IStickyMode;
36
+ }
37
+
38
+ /**
39
+ * Sticky mode:
40
+ * - 'replace': This item replaces the previous sticky item; its height is set to the previous sticky item's height.
41
+ * - 'stack': This item stacks on top of previous sticky items; its height is set to 0.
42
+ * - 'none': This item is not sticky.
43
+ */
44
+ export declare type IStickyMode = 'replace' | 'stack' | 'none';
45
+
46
+ export declare function StickyContainer({ children, offsetTop, baseZIndex, onStickyItemsHeightChange, defaultMode, ...rest }: IStickyContainerProps): JSX.Element;
47
+
48
+ export declare function StickyItem({ mode, children, className, ...rest }: IStickyItemProps): JSX.Element;
49
+
50
+ export { }
@@ -0,0 +1,139 @@
1
+ import { jsx as R } from "react/jsx-runtime";
2
+ import { createContext as b, useContext as Z, useRef as I, useState as N, useEffect as B, useCallback as w, useMemo as T } from "react";
3
+ const x = 20, $ = b(null);
4
+ function _() {
5
+ return Z($);
6
+ }
7
+ function P({ mode: s, children: o, className: m, ...h }) {
8
+ const e = _(), S = I(null), k = I(null), [r, n] = N(!1), t = I({
9
+ fixedOffsetTop: (e == null ? void 0 : e.fixedOffsetTop) || 0,
10
+ isSticky: r,
11
+ baseZIndex: (e == null ? void 0 : e.baseZIndex) || x
12
+ });
13
+ return t.current.baseZIndex = (e == null ? void 0 : e.baseZIndex) || x, t.current.fixedOffsetTop = (e == null ? void 0 : e.fixedOffsetTop) || 0, t.current.isSticky = r, B(() => {
14
+ const u = k.current, f = S.current, a = s || (e == null ? void 0 : e.mode);
15
+ if (!e || !f || !u || a === "none" || !a || !e.register) {
16
+ n(!1);
17
+ return;
18
+ }
19
+ const d = {
20
+ el: f,
21
+ update: (l, i, p, C, H) => {
22
+ if (!l)
23
+ return t.current.isSticky && n(!1), 0;
24
+ if (i <= p) {
25
+ const y = u.getBoundingClientRect().height;
26
+ let v = p;
27
+ if (a === "replace" && typeof C < "u") {
28
+ const g = C - (p + y);
29
+ if (g < 0 && (v = p + g, g + y < 0))
30
+ return t.current.isSticky && n(!1), 0;
31
+ }
32
+ return t.current.isSticky || n(!0), u.style.top = `${v}px`, u.style.width = `${f.offsetWidth}px`, u.style.zIndex = `${t.current.baseZIndex + (a === "replace" ? -H : H)}`, f.style.height = `${y}px`, a === "replace" ? 0 : y;
33
+ } else
34
+ n(!1);
35
+ return 0;
36
+ }
37
+ };
38
+ return e.register(d);
39
+ }, [e == null ? void 0 : e.mode, e == null ? void 0 : e.register, s]), B(() => {
40
+ const u = S.current, f = k.current, a = e == null ? void 0 : e.updateStickyItemsHeight;
41
+ if (!r || !u || !f || !a)
42
+ return;
43
+ const c = a(f.getBoundingClientRect().height);
44
+ return () => {
45
+ O(u, ["height"]), O(f, ["top", "z-index", "width"]), c();
46
+ };
47
+ }, [r, e == null ? void 0 : e.updateStickyItemsHeight]), /* @__PURE__ */ R("div", { className: "oe-sticky-item " + (m || ""), ...h, ref: S, children: /* @__PURE__ */ R("div", { className: "oe-sticky-content " + (r ? "is-sticky" : ""), ref: k, children: o }) });
48
+ }
49
+ function O(s, o) {
50
+ o.forEach((m) => {
51
+ s.style.removeProperty(m);
52
+ });
53
+ }
54
+ const L = 200;
55
+ function U({
56
+ children: s,
57
+ offsetTop: o = 0,
58
+ baseZIndex: m,
59
+ onStickyItemsHeightChange: h,
60
+ defaultMode: e = "replace",
61
+ ...S
62
+ }) {
63
+ const k = I(null), r = I([]), n = I({
64
+ fixedOffsetTop: o,
65
+ defaultMode: e,
66
+ stickyItemsHeight: 0,
67
+ onStickyItemsHeightChange: h
68
+ });
69
+ n.current.onStickyItemsHeightChange = h;
70
+ const t = w(() => {
71
+ const c = k.current;
72
+ if (!c)
73
+ return;
74
+ const d = c.getBoundingClientRect(), l = n.current, i = l.fixedOffsetTop, p = l.stickyItemsHeight, C = !(d.top > i || d.bottom < i);
75
+ let H = i, y = d.bottom - (i + p);
76
+ y > 0 && (y = 0);
77
+ const v = r.current.map((g) => g.el.getBoundingClientRect().top);
78
+ r.current.forEach((g, E) => {
79
+ H += g.update(C, v[E], H + y, v[E + 1], E);
80
+ });
81
+ }, []), u = w((c) => {
82
+ const d = n.current.stickyItemsHeight + c;
83
+ return n.current.stickyItemsHeight = d, setTimeout(() => {
84
+ var l, i;
85
+ (i = (l = n.current).onStickyItemsHeightChange) == null || i.call(l, d);
86
+ }, 0), () => {
87
+ const l = n.current.stickyItemsHeight - c;
88
+ n.current.stickyItemsHeight = l, setTimeout(() => {
89
+ var i, p;
90
+ (p = (i = n.current).onStickyItemsHeightChange) == null || p.call(i, l);
91
+ }, 0);
92
+ };
93
+ }, []);
94
+ B(() => {
95
+ n.current.fixedOffsetTop = o, n.current.defaultMode = e, t();
96
+ }, [o, e, t]);
97
+ const f = w((c) => (r.current.push(c), r.current = A(r.current), t(), () => {
98
+ r.current = r.current.filter((d) => d !== c), t();
99
+ }), [t]);
100
+ B(() => (window.addEventListener("scroll", t, { passive: !0 }), window.addEventListener("resize", t, { passive: !0 }), () => {
101
+ window.removeEventListener("scroll", t), window.removeEventListener("resize", t);
102
+ }), [t]);
103
+ const a = T(() => z(m), [m]);
104
+ return /* @__PURE__ */ R(
105
+ $.Provider,
106
+ {
107
+ value: {
108
+ register: f,
109
+ baseZIndex: a,
110
+ updateStickyItemsHeight: u,
111
+ fixedOffsetTop: o,
112
+ mode: e
113
+ },
114
+ children: /* @__PURE__ */ R(
115
+ "div",
116
+ {
117
+ ...S,
118
+ ref: k,
119
+ className: "oe-sticky-container",
120
+ "data-mode": e,
121
+ children: s
122
+ }
123
+ )
124
+ }
125
+ );
126
+ }
127
+ function z(s) {
128
+ return typeof s > "u" ? L : Math.max(Number(s) || 0, x);
129
+ }
130
+ function A(s) {
131
+ return s.sort((o, m) => {
132
+ const h = o.el.getBoundingClientRect(), e = m.el.getBoundingClientRect();
133
+ return h.top - e.top;
134
+ });
135
+ }
136
+ export {
137
+ U as StickyContainer,
138
+ P as StickyItem
139
+ };
@@ -0,0 +1 @@
1
+ (function(p,m){typeof exports=="object"&&typeof module<"u"?m(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],m):(p=typeof globalThis<"u"?globalThis:p||self,m(p.ReactSticky={},p.jsxRuntime,p.React))})(this,function(p,m,n){"use strict";const _=n.createContext(null);function N(){return n.useContext(_)}const A="";function O({mode:c,children:o,className:g,...S}){const e=N(),R=n.useRef(null),E=n.useRef(null),[s,i]=n.useState(!1),t=n.useRef({fixedOffsetTop:(e==null?void 0:e.fixedOffsetTop)||0,isSticky:s,baseZIndex:(e==null?void 0:e.baseZIndex)||20});return t.current.baseZIndex=(e==null?void 0:e.baseZIndex)||20,t.current.fixedOffsetTop=(e==null?void 0:e.fixedOffsetTop)||0,t.current.isSticky=s,n.useEffect(()=>{const f=E.current,d=R.current,l=c||(e==null?void 0:e.mode);if(!e||!d||!f||l==="none"||!l||!e.register){i(!1);return}const a={el:d,update:(y,r,h,v,H)=>{if(!y)return t.current.isSticky&&i(!1),0;if(r<=h){const k=f.getBoundingClientRect().height;let C=h;if(l==="replace"&&typeof v<"u"){const I=v-(h+k);if(I<0&&(C=h+I,I+k<0))return t.current.isSticky&&i(!1),0}return t.current.isSticky||i(!0),f.style.top=`${C}px`,f.style.width=`${d.offsetWidth}px`,f.style.zIndex=`${t.current.baseZIndex+(l==="replace"?-H:H)}`,d.style.height=`${k}px`,l==="replace"?0:k}else i(!1);return 0}};return e.register(a)},[e==null?void 0:e.mode,e==null?void 0:e.register,c]),n.useEffect(()=>{const f=R.current,d=E.current,l=e==null?void 0:e.updateStickyItemsHeight;if(!s||!f||!d||!l)return;const u=l(d.getBoundingClientRect().height);return()=>{x(f,["height"]),x(d,["top","z-index","width"]),u()}},[s,e==null?void 0:e.updateStickyItemsHeight]),m.jsx("div",{className:"oe-sticky-item "+(g||""),...S,ref:R,children:m.jsx("div",{className:"oe-sticky-content "+(s?"is-sticky":""),ref:E,children:o})})}function x(c,o){o.forEach(g=>{c.style.removeProperty(g)})}const b=200;function w({children:c,offsetTop:o=0,baseZIndex:g,onStickyItemsHeightChange:S,defaultMode:e="replace",...R}){const E=n.useRef(null),s=n.useRef([]),i=n.useRef({fixedOffsetTop:o,defaultMode:e,stickyItemsHeight:0,onStickyItemsHeightChange:S});i.current.onStickyItemsHeightChange=S;const t=n.useCallback(()=>{const u=E.current;if(!u)return;const a=u.getBoundingClientRect(),y=i.current,r=y.fixedOffsetTop,h=y.stickyItemsHeight,v=!(a.top>r||a.bottom<r);let H=r,k=a.bottom-(r+h);k>0&&(k=0);const C=s.current.map(I=>I.el.getBoundingClientRect().top);s.current.forEach((I,B)=>{H+=I.update(v,C[B],H+k,C[B+1],B)})},[]),f=n.useCallback(u=>{const a=i.current.stickyItemsHeight+u;return i.current.stickyItemsHeight=a,setTimeout(()=>{var y,r;(r=(y=i.current).onStickyItemsHeightChange)==null||r.call(y,a)},0),()=>{const y=i.current.stickyItemsHeight-u;i.current.stickyItemsHeight=y,setTimeout(()=>{var r,h;(h=(r=i.current).onStickyItemsHeightChange)==null||h.call(r,y)},0)}},[]);n.useEffect(()=>{i.current.fixedOffsetTop=o,i.current.defaultMode=e,t()},[o,e,t]);const d=n.useCallback(u=>(s.current.push(u),s.current=T(s.current),t(),()=>{s.current=s.current.filter(a=>a!==u),t()}),[t]);n.useEffect(()=>(window.addEventListener("scroll",t,{passive:!0}),window.addEventListener("resize",t,{passive:!0}),()=>{window.removeEventListener("scroll",t),window.removeEventListener("resize",t)}),[t]);const l=n.useMemo(()=>Z(g),[g]);return m.jsx(_.Provider,{value:{register:d,baseZIndex:l,updateStickyItemsHeight:f,fixedOffsetTop:o,mode:e},children:m.jsx("div",{...R,ref:E,className:"oe-sticky-container","data-mode":e,children:c})})}function Z(c){return typeof c>"u"?b:Math.max(Number(c)||0,20)}function T(c){return c.sort((o,g)=>{const S=o.el.getBoundingClientRect(),e=g.el.getBoundingClientRect();return S.top-e.top})}p.StickyContainer=w,p.StickyItem=O,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})});
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .oe-sticky-container,.oe-sticky-item{position:relative}.oe-sticky-content{will-change:top,position}.oe-sticky-content.is-sticky{position:fixed}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "react-sticky-kit",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "A lightweight, flexible React sticky container and item component library supporting multiple sticky modes, advanced layouts, and edge cases.",
6
+ "keywords": [
7
+ "react",
8
+ "sticky",
9
+ "react-sticky",
10
+ "sticky-container",
11
+ "sticky-item",
12
+ "z-index"
13
+ ],
14
+ "license": "MIT",
15
+ "scripts": {
16
+ "dev": "vite",
17
+ "build": "vite build",
18
+ "preview": "vite preview",
19
+ "type-check": "tsc --noEmit",
20
+ "lint": "eslint src demo --ext .ts,.tsx,.js,.jsx",
21
+ "pub": "tsx ./scripts/publish.ts"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "main": "dist/react-sticky.umd.js",
27
+ "module": "dist/react-sticky.es.js",
28
+ "types": "dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/react-sticky.es.js",
32
+ "require": "./dist/react-sticky.umd.js",
33
+ "types": "./dist/index.d.ts"
34
+ }
35
+ },
36
+ "dependencies": {
37
+ "react": "^19.1.0",
38
+ "react-dom": "^19.1.0"
39
+ },
40
+ "peerDependencies": {
41
+ "react": ">=17.0.0",
42
+ "react-dom": ">=17.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@eslint/js": "^9.25.0",
46
+ "@types/node": "^22.15.18",
47
+ "@types/react": "^19.1.2",
48
+ "@types/react-dom": "^19.1.2",
49
+ "@typescript-eslint/eslint-plugin": "^8.32.1",
50
+ "@typescript-eslint/parser": "^8.32.1",
51
+ "@vitejs/plugin-react": "^4.0.0",
52
+ "eslint": "^9.26.0",
53
+ "eslint-config-prettier": "^10.1.5",
54
+ "eslint-plugin-jsx-a11y": "^6.8.0",
55
+ "eslint-plugin-react": "^7.33.2",
56
+ "eslint-plugin-react-hooks": "^5.2.0",
57
+ "globals": "^16.0.0",
58
+ "sass": "^1.88.0",
59
+ "tsx": "^4.19.4",
60
+ "typescript": "^5.0.0",
61
+ "typescript-eslint": "^8.30.1",
62
+ "vite": "^4.0.0",
63
+ "vite-plugin-dts": "^4.5.4"
64
+ }
65
+ }