├── .gitignore ├── dist ├── index.d.ts ├── index.js.map └── index.js ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * Returns a ref, and a stateful value bound to the ref 4 | */ 5 | export declare function useSticky(): readonly [import("react").RefObject, boolean]; 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:react/recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:react-hooks/recommended', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 13, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['react', '@typescript-eslint'], 21 | rules: {}, 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "checkJs": false, 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "jsx": "react-jsx", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "declaration": true, 14 | "strictNullChecks": true, 15 | "resolveJsonModule": true, 16 | "sourceMap": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "*": ["*", "src/*"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "dist"], 23 | "include": ["./src", "./test", "./*"] 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tailwind Labs 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. 22 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,iCAAoD;AAEpD;;GAEG;AACH,SAAgB,SAAS;IACvB,MAAM,SAAS,GAAG,IAAA,cAAM,EAAI,IAAI,CAAC,CAAC;IAClC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,IAAA,gBAAQ,EAAC,KAAK,CAAC,CAAC;IAE5C,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,iDAAiD;QACjD,mIAAmI;QACnI,SAAS,OAAO;YACd,IAAI,CAAC,SAAS,CAAC,OAAO;gBAAE,OAAO;YAC/B,MAAM,aAAa,GAAG,SAAS,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC;YACpE,MAAM,YAAY,GAAG,QAAQ,CAAC,gBAAgB,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;YACvE,MAAM,YAAY,GAAG,aAAa,IAAI,YAAY,CAAC;YAEnD,IAAI,YAAY,IAAI,CAAC,MAAM;gBAAE,SAAS,CAAC,IAAI,CAAC,CAAC;iBACxC,IAAI,CAAC,YAAY,IAAI,MAAM;gBAAE,SAAS,CAAC,KAAK,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,EAAE,CAAC;QAEV,cAAc;QACd,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC3C,MAAM,CAAC,gBAAgB,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;QAEtD,OAAO,GAAG,EAAE;YACV,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAChD,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC9C,MAAM,CAAC,mBAAmB,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;QAC3D,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,OAAO,CAAC,SAAS,EAAE,MAAM,CAAU,CAAC;AACtC,CAAC;AA/BD,8BA+BC"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-sticky", 3 | "version": "0.1.4", 4 | "description": "Observe when DOM element enters or leaves sticky state", 5 | "main": "dist/index.js", 6 | "peerDependencies": { 7 | "react": "^16.8.0" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "build": "tsc" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/robinJonsson/react-use-sticky.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "hooks", 20 | "sticky", 21 | "css-position-sticky", 22 | "position" 23 | ], 24 | "author": "Robin Jonsson", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/robinJonsson/react-use-sticky/issues" 28 | }, 29 | "homepage": "https://github.com/robinJonsson/react-use-sticky#readme", 30 | "devDependencies": { 31 | "@types/react": "^17.0.37", 32 | "@types/react-dom": "^17.0.11", 33 | "@typescript-eslint/eslint-plugin": "^5.4.0", 34 | "@typescript-eslint/parser": "^5.4.0", 35 | "eslint": "^8.3.0", 36 | "eslint-plugin-react": "^7.27.1", 37 | "eslint-plugin-react-hooks": "^4.3.0", 38 | "typescript": "^4.5.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Sticky Hook 2 | 3 | A react hook for observing/watching `position: sticky` state on refs 4 | 5 | ## Installation 6 | 7 | `npm i react-use-sticky` 8 | 9 | ## Usage 10 | 11 | `useSticky` returns a pair of values, the ref to observe/watch and the current sticky state of the ref. 12 | 13 | ```jsx 14 | import { useSticky } from 'react-use-sticky'; 15 | 16 | function HeaderBar() { 17 | const [headerBarRef, sticky] = useSticky(); 18 | const style = { 19 | position: 'sticky', 20 | top: 0, 21 | background: sticky ? 'green' : 'red', 22 | }; 23 | 24 | return ( 25 | 28 | ); 29 | } 30 | 31 | export default HeaderBar; 32 | ``` 33 | 34 | Typescript with generic 35 | 36 | ```tsx 37 | import { useSticky } from 'react-use-sticky'; 38 | 39 | function HeaderBar() { 40 | const [headerBarRef, sticky] = useSticky(); 41 | const style = { 42 | position: 'sticky', 43 | top: 0, 44 | background: sticky ? 'green' : 'red', 45 | } as const; 46 | 47 | return ( 48 |
49 | HeaderBar 50 |
51 | ); 52 | } 53 | 54 | export default HeaderBar; 55 | ``` 56 | 57 | ## Build 58 | 59 | ```bash 60 | npm run build 61 | ``` 62 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | /** 4 | * Returns a ref, and a stateful value bound to the ref 5 | */ 6 | export function useSticky() { 7 | const stickyRef = useRef(null); 8 | const [sticky, setSticky] = useState(false); 9 | 10 | useEffect(() => { 11 | // Observe when ref enters or leaves sticky state 12 | // rAF https://stackoverflow.com/questions/41740082/scroll-events-requestanimationframe-vs-requestidlecallback-vs-passive-event-lis 13 | function observe() { 14 | if (!stickyRef.current) return; 15 | const refPageOffset = stickyRef.current.getBoundingClientRect().top; 16 | const stickyOffset = parseInt(getComputedStyle(stickyRef.current).top); 17 | const stickyActive = refPageOffset <= stickyOffset; 18 | 19 | if (stickyActive && !sticky) setSticky(true); 20 | else if (!stickyActive && sticky) setSticky(false); 21 | } 22 | observe(); 23 | 24 | // Bind events 25 | document.addEventListener('scroll', observe); 26 | window.addEventListener('resize', observe); 27 | window.addEventListener('orientationchange', observe); 28 | 29 | return () => { 30 | document.removeEventListener('scroll', observe); 31 | window.removeEventListener('resize', observe); 32 | window.removeEventListener('orientationchange', observe); 33 | }; 34 | }, [sticky]); 35 | 36 | return [stickyRef, sticky] as const; 37 | } 38 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.useSticky = void 0; 4 | const react_1 = require("react"); 5 | /** 6 | * Returns a ref, and a stateful value bound to the ref 7 | */ 8 | function useSticky() { 9 | const stickyRef = (0, react_1.useRef)(null); 10 | const [sticky, setSticky] = (0, react_1.useState)(false); 11 | (0, react_1.useEffect)(() => { 12 | // Observe when ref enters or leaves sticky state 13 | // rAF https://stackoverflow.com/questions/41740082/scroll-events-requestanimationframe-vs-requestidlecallback-vs-passive-event-lis 14 | function observe() { 15 | if (!stickyRef.current) 16 | return; 17 | const refPageOffset = stickyRef.current.getBoundingClientRect().top; 18 | const stickyOffset = parseInt(getComputedStyle(stickyRef.current).top); 19 | const stickyActive = refPageOffset <= stickyOffset; 20 | if (stickyActive && !sticky) 21 | setSticky(true); 22 | else if (!stickyActive && sticky) 23 | setSticky(false); 24 | } 25 | observe(); 26 | // Bind events 27 | document.addEventListener('scroll', observe); 28 | window.addEventListener('resize', observe); 29 | window.addEventListener('orientationchange', observe); 30 | return () => { 31 | document.removeEventListener('scroll', observe); 32 | window.removeEventListener('resize', observe); 33 | window.removeEventListener('orientationchange', observe); 34 | }; 35 | }, [sticky]); 36 | return [stickyRef, sticky]; 37 | } 38 | exports.useSticky = useSticky; 39 | //# sourceMappingURL=index.js.map --------------------------------------------------------------------------------