├── .npmrc ├── src ├── utils │ ├── generate-id.ts │ ├── clear-selection.ts │ ├── generate-style.ts │ ├── events.ts │ ├── is.ts │ └── get-scroll-width.ts ├── index.ts ├── types │ ├── props.ts │ ├── styles.ts │ └── state.ts ├── modules │ ├── scroll-to.ts │ └── mouse.without.window.ts ├── styles │ └── index.ts └── scroll.tsx ├── example ├── src │ ├── styles │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ ├── glyphicons-halflings-regular.woff2 │ │ └── glyphicons-halflings-regular.svg │ └── index.jsx ├── build.js ├── package.json └── package-lock.json ├── .editorconfig ├── eslint.config.js ├── .gitignore ├── .npmignore ├── tsconfig.json ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | save = true 2 | save-exact = true -------------------------------------------------------------------------------- /src/utils/generate-id.ts: -------------------------------------------------------------------------------- 1 | export const generateId = (): string => `_${Math.random().toString(36).substr(2, 9)}`; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { CustomScroll, getDefaultScrollWidth } from "./scroll"; 2 | 3 | export default CustomScroll; 4 | 5 | export { getDefaultScrollWidth }; 6 | -------------------------------------------------------------------------------- /example/src/styles/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexSergey/react-customscroll/HEAD/example/src/styles/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /example/src/styles/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexSergey/react-customscroll/HEAD/example/src/styles/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /example/src/styles/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexSergey/react-customscroll/HEAD/example/src/styles/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /example/src/styles/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexSergey/react-customscroll/HEAD/example/src/styles/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 2 3 | indent_style = space 4 | max_line_length = 120 5 | tab_width = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const {makeConfig} = require('@rockpack/codestyle'); 2 | 3 | const config = makeConfig(); 4 | 5 | config.push({ 6 | rules: { 7 | '@typescript-eslint/no-unsafe-function-type': 'off', 8 | }, 9 | }); 10 | 11 | module.exports = config; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_STORE 4 | static 5 | 6 | .module-cache 7 | *.log 8 | *.map 9 | 10 | *.orig 11 | .DS_Store 12 | Thumbs.db 13 | logs 14 | reports 15 | documentation 16 | webpack.customConfig.js 17 | 18 | dist 19 | lib 20 | coverage 21 | test-reports 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_STORE 4 | static 5 | 6 | .module-cache 7 | *.log 8 | *.map 9 | 10 | *.orig 11 | .DS_Store 12 | Thumbs.db 13 | logs 14 | reports 15 | documentation 16 | webpack.customConfig.js 17 | example 18 | examples 19 | src 20 | coverage 21 | test-reports 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rockpack/tsconfig", 3 | "compilerOptions": { 4 | "strict": false, 5 | "exactOptionalPropertyTypes": false, 6 | "strictNullChecks": false 7 | }, 8 | "exclude": [ 9 | "./node_modules/**/*" 10 | ], 11 | "include": [ 12 | "./src/**/*" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/clear-selection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Clear selection range on content in the scroll block 3 | * */ 4 | import { isClient } from "./is"; 5 | 6 | export const clearSelection = (): void => { 7 | if (isClient()) { 8 | (window.getSelection() as { removeAllRanges: () => void }).removeAllRanges(); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/generate-style.ts: -------------------------------------------------------------------------------- 1 | export const generateStyle = (css: string, id: string): void => { 2 | const head = document.head || document.getElementsByTagName("head")[0]; 3 | const style = document.createElement("style"); 4 | 5 | style.setAttribute("id", id); 6 | 7 | style.appendChild(document.createTextNode(css)); 8 | 9 | head.appendChild(style); 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/events.ts: -------------------------------------------------------------------------------- 1 | export const on = (el: Document | HTMLElement | Window, events: string[], cb: Function): void => { 2 | events.forEach((event) => el.addEventListener(event, cb as EventListener)); 3 | }; 4 | 5 | export const off = (el: Document | HTMLElement | Window, events: string[], cb: Function): void => { 6 | events.forEach((event) => el.removeEventListener(event, cb as EventListener)); 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const isFunction = (fn: unknown): fn is Function => typeof fn === "function"; 2 | 3 | const isDefined = (value: unknown): boolean => typeof value !== "undefined"; 4 | 5 | const isClient = (): boolean => typeof window !== "undefined"; 6 | 7 | const isObject = (value: unknown): boolean => typeof value === "object"; 8 | 9 | export { isClient, isDefined, isFunction, isObject }; 10 | -------------------------------------------------------------------------------- /src/types/props.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface Props { 4 | children: (offset: number) => ReactNode; 5 | rtl?: boolean; 6 | scrollAreaColor?: string; 7 | scrollBarColor?: string; 8 | scrollBarRadius?: string; 9 | scrollSync?: (offset: number) => unknown; 10 | scrollTo?: number; 11 | scrollWidth?: string; 12 | virtualized?: { 13 | height: number; 14 | scrollHeight: number; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/types/styles.ts: -------------------------------------------------------------------------------- 1 | export interface Styles { 2 | ctmScroll: Record; 3 | ctmScrollActive: Record; 4 | ctmScrollFrame: Record; 5 | ctmScrollHolder: Record; 6 | noselect: Record; 7 | scrollArea: Record; 8 | scrollAreaFrame: Record; 9 | scrollBar: Record; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/state.ts: -------------------------------------------------------------------------------- 1 | import { Styles } from "./styles"; 2 | 3 | export interface State { 4 | animate: boolean; 5 | classes: { 6 | area: string; 7 | "area-holder": string; 8 | base: string; 9 | frame: string; 10 | holder: string; 11 | "scroll-bar": string; 12 | }; 13 | scrollAreaShow: boolean; 14 | scrollTop: number; 15 | selection: boolean; 16 | styles: Styles; 17 | virtualState: null | { height: number; top: number }; 18 | width: string; 19 | } 20 | -------------------------------------------------------------------------------- /example/build.js: -------------------------------------------------------------------------------- 1 | const { frontendCompiler } = require('@rockpack/compiler'); 2 | const path = require('path'); 3 | 4 | frontendCompiler({}, finalConfig => { 5 | finalConfig.output.publicPath = '/'; 6 | 7 | Object.assign(finalConfig.resolve, { 8 | alias: { 9 | 'react-dom': path.resolve(__dirname, '../node_modules/react-dom'), 10 | 'prop-types': path.resolve(__dirname, '../node_modules/prop-types'), 11 | react: path.resolve(__dirname, '../node_modules/react') 12 | } 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/modules/scroll-to.ts: -------------------------------------------------------------------------------- 1 | const _scrollTo = (element: HTMLDivElement, to: number, duration: number): void => { 2 | if (duration <= 0) { 3 | return; 4 | } 5 | const difference = to - element.scrollTop; 6 | const perTick = (difference / duration) * 10; 7 | 8 | setTimeout(() => { 9 | element.scrollTop += perTick; 10 | if (element.scrollTop === to) { 11 | return; 12 | } 13 | _scrollTo(element, to, duration - 10); 14 | }, 10); 15 | }; 16 | 17 | export const scrollTo = (domEl: HTMLDivElement, offsetY: number, animate?: boolean): void => { 18 | if (animate) { 19 | _scrollTo(domEl, offsetY, 500); 20 | } else { 21 | domEl.scrollTop = offsetY; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-customscroll-example", 3 | "version": "1.0.0", 4 | "description": "This is little component for custom scroll in React", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development node build", 8 | "build": "cross-env NODE_ENV=production node build" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/AlexSergey/react-customscroll" 13 | }, 14 | "author": "Aleksandrov Sergey (https://github.com/AlexSergey/react-customscroll)", 15 | "license": "MIT", 16 | "dependencies": { 17 | "react": "19.1.0", 18 | "react-dom": "19.1.0", 19 | "react-measure": "2.5.2", 20 | "react-container-dimensions": "1.4.1", 21 | "react-virtualized": "9.22.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/get-scroll-width.ts: -------------------------------------------------------------------------------- 1 | import { isClient } from "./is"; 2 | 3 | export const getScrollWidth = (): number => { 4 | if (!isClient()) { 5 | return 0; 6 | } 7 | const inner = document.createElement("p"); 8 | inner.style.width = "100%"; 9 | inner.style.height = "200px"; 10 | 11 | const outer = document.createElement("div"); 12 | outer.style.position = "absolute"; 13 | outer.style.top = "0px"; 14 | outer.style.left = "0px"; 15 | outer.style.visibility = "hidden"; 16 | outer.style.width = "200px"; 17 | outer.style.height = "150px"; 18 | outer.style.overflow = "hidden"; 19 | outer.appendChild(inner); 20 | 21 | document.body.appendChild(outer); 22 | const w1 = inner.offsetWidth; 23 | outer.style.overflow = "scroll"; 24 | let w2 = inner.offsetWidth; 25 | 26 | if (w1 === w2) { 27 | w2 = outer.clientWidth; 28 | } 29 | 30 | document.body.removeChild(outer); 31 | 32 | return w1 - w2; 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React CustomScroll 2 | 3 | **React-customscroll** is a tiny React component for scroll bar customization, without dependencies but with a lot of features. 4 | 5 | [Demo](http://natrube.net/custom-scroll/index.html) 6 | 7 | ## Features: 8 | - React 19 support 9 | - TypeScript support 10 | - Tiny size (11kb) 11 | - Without dependencies 12 | - Easy customization, simple api 13 | - Native OS scroll behavior 14 | - Cross browser 15 | - Animate scrollTo feature 16 | - RTL support 17 | - Server Side Rendering support 18 | - Scroll snap support 19 | 20 | ## Usage 21 | 22 | ### Step 1: 23 | 24 | ``` 25 | npm install react-customscroll -save 26 | ``` 27 | 28 | ### Step 2: 29 | 30 | **React-customscroll** works like native browser scroll. 31 | 32 | You should paste the component inside the block with scrollable data. 33 | 34 | For instance: 35 | 36 | ```jsx 37 | import CustomScroll from 'react-customscroll'; 38 | ``` 39 | 40 | ```jsx 41 |
42 | 43 | ...long_data_here... 44 | 45 |
46 | ``` 47 | 48 | If block with a native browser scroll works well it will work with **React-customscroll** 49 | 50 | --- 51 | 52 | [Examples](https://github.com/AlexSergey/react-customscroll/tree/master/example) 53 | 54 | ## License 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /src/modules/mouse.without.window.ts: -------------------------------------------------------------------------------- 1 | import { isClient } from "../utils/is"; 2 | 3 | const fire = (): void => { 4 | const event = document.createEvent("Event"); 5 | event.initEvent("mouseWithoutWindow", true, true); 6 | document.dispatchEvent(event); 7 | }; 8 | 9 | const MouseWithoutWindow = ((): { getInstance: () => void } => { 10 | let instance; 11 | 12 | const createInstance = (): boolean => { 13 | document.addEventListener("mouseup", (e) => { 14 | const w = window; 15 | const d = document; 16 | const el = d.documentElement; 17 | const body = d.getElementsByTagName("body")[0]; 18 | const width = w.innerWidth || el.clientWidth || body.clientWidth; 19 | const height = w.innerHeight || el.clientHeight || body.clientHeight; 20 | 21 | if (e.clientX >= width || e.clientX < 0 || e.clientY >= height || e.clientY < 0) { 22 | fire(); 23 | } 24 | }); 25 | 26 | window.addEventListener("blur", () => { 27 | fire(); 28 | }); 29 | 30 | return true; 31 | }; 32 | 33 | return { 34 | getInstance: (): void => { 35 | if (instance) { 36 | return instance; 37 | } 38 | instance = createInstance(); 39 | }, 40 | }; 41 | })(); 42 | 43 | export const mouseWithoutWindow = (): void => { 44 | if (isClient()) { 45 | MouseWithoutWindow.getInstance(); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-customscroll", 3 | "version": "5.3.0", 4 | "description": "This is a tiny React component for scroll bar customization, without dependencies but with a lot of features.", 5 | "main": "dist/index.js", 6 | "types": "dist/types/index.d.ts", 7 | "scripts": { 8 | "start": "cross-env NODE_ENV=development node build", 9 | "build": "cross-env NODE_ENV=production node build", 10 | "typing": "cross-env NODE_ENV=production tsc -p . --noEmit", 11 | "lint": "cross-env NODE_ENV=production eslint \"src/**\"", 12 | "format": "cross-env NODE_ENV=production eslint \"src/**\" --fix", 13 | "production": "npm run typing && npm run lint && npm run build && npm publish" 14 | }, 15 | "husky": { 16 | "hooks": { 17 | "pre-push": "npm run lint && npm run typing" 18 | } 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/AlexSergey/react-customscroll" 23 | }, 24 | "author": "Aleksandrov Sergey (https://github.com/AlexSergey/react-customscroll)", 25 | "license": "MIT", 26 | "homepage": "https://github.com/AlexSergey/react-customscroll", 27 | "bugs": { 28 | "url": "https://github.com/AlexSergey/react-customscroll/issues" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "react-component", 33 | "scroll", 34 | "scrollbar", 35 | "customscroll", 36 | "ui" 37 | ], 38 | "devDependencies": { 39 | "@rockpack/codestyle": "6.0.3", 40 | "@rockpack/compiler": "6.0.3", 41 | "@rockpack/tsconfig": "6.0.3", 42 | "@types/react": "19.1.6", 43 | "@types/react-dom": "19.1.5", 44 | "husky": "9.1.7", 45 | "react": "19.1.0", 46 | "react-dom": "19.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { Styles } from "../types/styles"; 2 | 3 | interface Props { 4 | isZero: boolean; 5 | originalScrollWidth: number; 6 | scrollAreaColor: string; 7 | scrollBarColor: string; 8 | scrollBarRadius: string; 9 | scrollWidth: string; 10 | virtualized: boolean; 11 | } 12 | 13 | export const stylesFactory = (props: Props, rtl?: boolean): Styles => ({ 14 | ctmScroll: { 15 | height: "100%", 16 | overflow: "hidden", 17 | position: "relative", 18 | }, 19 | ctmScrollActive: Object.assign( 20 | {}, 21 | rtl 22 | ? { 23 | paddingLeft: props.scrollWidth, 24 | } 25 | : { 26 | paddingRight: props.scrollWidth, 27 | }, 28 | ), 29 | ctmScrollFrame: Object.assign( 30 | {}, 31 | props.virtualized ? { height: "100%", width: "100%" } : {}, 32 | props.isZero 33 | ? { 34 | width: `calc(100% - ${props.originalScrollWidth}px)`, 35 | } 36 | : {}, 37 | ), 38 | ctmScrollHolder: { 39 | height: "100%", 40 | overflowY: "scroll", 41 | }, 42 | noselect: { 43 | MozUserSelect: "none", 44 | msUserSelect: "none", 45 | userSelect: "none", 46 | WebkitTouchCallout: "none", 47 | WebkitUserSelect: "none", 48 | }, 49 | scrollArea: Object.assign( 50 | { 51 | background: props.scrollAreaColor, 52 | height: "100%", 53 | padding: "1px", 54 | position: "absolute", 55 | width: props.scrollWidth, 56 | zIndex: "10", 57 | }, 58 | rtl 59 | ? { 60 | left: "0", 61 | top: "0", 62 | } 63 | : { 64 | right: "0", 65 | top: "0", 66 | }, 67 | ), 68 | scrollAreaFrame: { 69 | height: "100%", 70 | position: "relative", 71 | }, 72 | scrollBar: Object.assign({ 73 | background: props.scrollBarColor, 74 | borderRadius: props.scrollBarRadius, 75 | cursor: "pointer", 76 | position: "absolute", 77 | top: "0", 78 | width: "100%", 79 | }), 80 | }); 81 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-customscroll-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "react-customscroll-example", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "react": "19.1.0", 13 | "react-container-dimensions": "1.4.1", 14 | "react-dom": "19.1.0", 15 | "react-measure": "2.5.2", 16 | "react-virtualized": "9.22.6" 17 | } 18 | }, 19 | "node_modules/@babel/runtime": { 20 | "version": "7.27.6", 21 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", 22 | "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", 23 | "license": "MIT", 24 | "engines": { 25 | "node": ">=6.9.0" 26 | } 27 | }, 28 | "node_modules/batch-processor": { 29 | "version": "1.0.0", 30 | "resolved": "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz", 31 | "integrity": "sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==", 32 | "license": "MIT" 33 | }, 34 | "node_modules/clsx": { 35 | "version": "1.1.1", 36 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", 37 | "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", 38 | "license": "MIT", 39 | "engines": { 40 | "node": ">=6" 41 | } 42 | }, 43 | "node_modules/dom-helpers": { 44 | "version": "5.2.0", 45 | "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", 46 | "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", 47 | "license": "MIT", 48 | "dependencies": { 49 | "@babel/runtime": "^7.8.7", 50 | "csstype": "^3.0.2" 51 | } 52 | }, 53 | "node_modules/dom-helpers/node_modules/csstype": { 54 | "version": "3.0.5", 55 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", 56 | "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==", 57 | "license": "MIT" 58 | }, 59 | "node_modules/element-resize-detector": { 60 | "version": "1.1.15", 61 | "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.1.15.tgz", 62 | "integrity": "sha512-16/5avDegXlUxytGgaumhjyQoM6hpp5j3+L79sYq5hlXfTNRy5WMMuTVWkZU3egp/CokCmTmvf18P3KeB57Iog==", 63 | "license": "MIT", 64 | "dependencies": { 65 | "batch-processor": "^1.0.0" 66 | } 67 | }, 68 | "node_modules/get-node-dimensions": { 69 | "version": "1.2.1", 70 | "resolved": "https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz", 71 | "integrity": "sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==", 72 | "license": "MIT" 73 | }, 74 | "node_modules/invariant": { 75 | "version": "2.2.4", 76 | "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", 77 | "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", 78 | "license": "MIT", 79 | "dependencies": { 80 | "loose-envify": "^1.0.0" 81 | } 82 | }, 83 | "node_modules/js-tokens": { 84 | "version": "4.0.0", 85 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 86 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 87 | "license": "MIT" 88 | }, 89 | "node_modules/loose-envify": { 90 | "version": "1.4.0", 91 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 92 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 93 | "license": "MIT", 94 | "dependencies": { 95 | "js-tokens": "^3.0.0 || ^4.0.0" 96 | }, 97 | "bin": { 98 | "loose-envify": "cli.js" 99 | } 100 | }, 101 | "node_modules/object-assign": { 102 | "version": "4.1.1", 103 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 104 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 105 | "license": "MIT", 106 | "engines": { 107 | "node": ">=0.10.0" 108 | } 109 | }, 110 | "node_modules/prop-types": { 111 | "version": "15.7.2", 112 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", 113 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", 114 | "license": "MIT", 115 | "dependencies": { 116 | "loose-envify": "^1.4.0", 117 | "object-assign": "^4.1.1", 118 | "react-is": "^16.8.1" 119 | } 120 | }, 121 | "node_modules/react": { 122 | "version": "19.1.0", 123 | "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", 124 | "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", 125 | "license": "MIT", 126 | "engines": { 127 | "node": ">=0.10.0" 128 | } 129 | }, 130 | "node_modules/react-container-dimensions": { 131 | "version": "1.4.1", 132 | "resolved": "https://registry.npmjs.org/react-container-dimensions/-/react-container-dimensions-1.4.1.tgz", 133 | "integrity": "sha512-f5WXYpsL0vAuT9hD6bSTqRYOJXFbU9JzjSZfscNT77PbdCUReLv2/aSvYBgkXyALtOBVdS/nwO5RVZgBxxWDnw==", 134 | "license": "MIT", 135 | "dependencies": { 136 | "element-resize-detector": "^1.1.10", 137 | "invariant": "^2.2.2", 138 | "prop-types": "^15.5.8" 139 | }, 140 | "peerDependencies": { 141 | "react": "^0.14.0 || ^15.0.0 || 16.x", 142 | "react-dom": "^0.14.0 || ^15.0.0 || 16.x" 143 | } 144 | }, 145 | "node_modules/react-dom": { 146 | "version": "19.1.0", 147 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", 148 | "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", 149 | "license": "MIT", 150 | "dependencies": { 151 | "scheduler": "^0.26.0" 152 | }, 153 | "peerDependencies": { 154 | "react": "^19.1.0" 155 | } 156 | }, 157 | "node_modules/react-is": { 158 | "version": "16.10.1", 159 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.1.tgz", 160 | "integrity": "sha512-BXUMf9sIOPXXZWqr7+c5SeOKJykyVr2u0UDzEf4LNGc6taGkQe1A9DFD07umCIXz45RLr9oAAwZbAJ0Pkknfaw==", 161 | "license": "MIT" 162 | }, 163 | "node_modules/react-lifecycles-compat": { 164 | "version": "3.0.4", 165 | "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", 166 | "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", 167 | "license": "MIT" 168 | }, 169 | "node_modules/react-measure": { 170 | "version": "2.5.2", 171 | "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-2.5.2.tgz", 172 | "integrity": "sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==", 173 | "license": "MIT", 174 | "dependencies": { 175 | "@babel/runtime": "^7.2.0", 176 | "get-node-dimensions": "^1.2.1", 177 | "prop-types": "^15.6.2", 178 | "resize-observer-polyfill": "^1.5.0" 179 | }, 180 | "peerDependencies": { 181 | "react": ">0.13.0", 182 | "react-dom": ">0.13.0" 183 | } 184 | }, 185 | "node_modules/react-virtualized": { 186 | "version": "9.22.6", 187 | "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz", 188 | "integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==", 189 | "license": "MIT", 190 | "dependencies": { 191 | "@babel/runtime": "^7.7.2", 192 | "clsx": "^1.0.4", 193 | "dom-helpers": "^5.1.3", 194 | "loose-envify": "^1.4.0", 195 | "prop-types": "^15.7.2", 196 | "react-lifecycles-compat": "^3.0.4" 197 | }, 198 | "peerDependencies": { 199 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 200 | "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 201 | } 202 | }, 203 | "node_modules/resize-observer-polyfill": { 204 | "version": "1.5.1", 205 | "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", 206 | "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", 207 | "license": "MIT" 208 | }, 209 | "node_modules/scheduler": { 210 | "version": "0.26.0", 211 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", 212 | "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", 213 | "license": "MIT" 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/scroll.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef, CSSProperties, ReactNode } from "react"; 2 | 3 | import { mouseWithoutWindow } from "./modules/mouse.without.window"; 4 | import { scrollTo } from "./modules/scroll-to"; 5 | import { stylesFactory } from "./styles"; 6 | import { Props } from "./types/props"; 7 | import { State } from "./types/state"; 8 | import { clearSelection } from "./utils/clear-selection"; 9 | import { off, on } from "./utils/events"; 10 | import { generateId } from "./utils/generate-id"; 11 | import { generateStyle } from "./utils/generate-style"; 12 | import { getScrollWidth } from "./utils/get-scroll-width"; 13 | import { isClient, isDefined, isFunction, isObject } from "./utils/is"; 14 | 15 | mouseWithoutWindow(); 16 | 17 | /** 18 | * This is min height for Scroll Bar. 19 | * If children content will be very big 20 | * Scroll Bar stay 20 pixels 21 | * */ 22 | const minHeightScrollBar = 20; 23 | const defaultScrollWidth = 17; 24 | const REINIT_MS = 250; 25 | const SCROLL_WIDTH = getScrollWidth(); 26 | // If this is Safari / iPhone / iPad or other browser / device with scrollWidth === 0 27 | const isZero = SCROLL_WIDTH === 0; 28 | 29 | const getDefaultScrollWidth = (): number => (typeof SCROLL_WIDTH === "number" ? SCROLL_WIDTH : 0); 30 | 31 | class CustomScroll extends Component { 32 | private customScroll: HTMLDivElement; 33 | 34 | private customScrollFrameRef: { 35 | current: HTMLDivElement; 36 | }; 37 | 38 | private customScrollHolder: HTMLDivElement; 39 | 40 | private customScrollHolderRef: { 41 | current: HTMLDivElement; 42 | }; 43 | 44 | private customScrollRef: { 45 | current: HTMLDivElement; 46 | }; 47 | 48 | private endScroll: () => void; 49 | 50 | private interval; 51 | 52 | private readonly isVirtualized: boolean; 53 | 54 | private nextHolderHeight = 0; 55 | 56 | private nextWrapperHeight = 0; 57 | 58 | private scrollBarRef: { 59 | current: HTMLDivElement; 60 | }; 61 | 62 | private scrollBlock: HTMLDivElement; 63 | 64 | private scrollID: string = generateId(); 65 | 66 | private scrollRun: (e) => void; 67 | 68 | constructor(props) { 69 | super(props); 70 | 71 | [ 72 | "scroll-area", 73 | "scroll-area-holder", 74 | "scrollBar", 75 | "customScroll", 76 | "customScrollHolder", 77 | "customScrollFrame", 78 | ].forEach((r) => { 79 | this[`${r}Ref`] = createRef(); 80 | }); 81 | let scrollWidth = getDefaultScrollWidth(); 82 | 83 | this.isVirtualized = isObject(props.virtualized); 84 | 85 | if (isZero) { 86 | scrollWidth = defaultScrollWidth; 87 | } 88 | 89 | const className = isDefined(props.className) ? props.className : "react-customscroll"; 90 | 91 | this.state = { 92 | animate: props.animate || true, 93 | classes: { 94 | area: `${className}-scrollbar-area`, 95 | "area-holder": `${className}-scrollbar-holder`, 96 | base: className, 97 | frame: `${className}-frame`, 98 | holder: `${className}-holder`, 99 | "scroll-bar": `${className}-scrollbar`, 100 | }, 101 | scrollAreaShow: false, 102 | scrollTop: 0, 103 | selection: true, 104 | styles: { 105 | ctmScroll: {}, 106 | ctmScrollActive: {}, 107 | ctmScrollFrame: {}, 108 | ctmScrollHolder: {}, 109 | noselect: {}, 110 | scrollArea: {}, 111 | scrollAreaFrame: {}, 112 | scrollBar: {}, 113 | }, 114 | virtualState: this.isVirtualized ? this.getScrollBarStyles(props.scrollTo || 0) : null, 115 | width: `calc(100% + ${scrollWidth}px)`, 116 | }; 117 | 118 | if (isClient() && !document.getElementById(this.scrollID)) { 119 | generateStyle( 120 | `#${this.scrollID}::-webkit-scrollbar { opacity: 0 } 121 | #${this.scrollID}::-webkit-scrollbar-track-piece { background-color: transparent }`, 122 | this.scrollID, 123 | ); 124 | } 125 | } 126 | 127 | applyStyles(): void { 128 | const scrollWidth = getDefaultScrollWidth(); 129 | 130 | this.setState((state) => 131 | Object.assign(state, { 132 | styles: stylesFactory( 133 | { 134 | isZero, 135 | originalScrollWidth: scrollWidth, 136 | scrollAreaColor: isDefined(this.props.scrollAreaColor) ? this.props.scrollAreaColor : "#494949", 137 | scrollBarColor: isDefined(this.props.scrollBarColor) ? this.props.scrollBarColor : "#aeaeae", 138 | scrollBarRadius: isDefined(this.props.scrollBarRadius) ? this.props.scrollBarRadius : "6px", 139 | scrollWidth: isDefined(this.props.scrollWidth) ? this.props.scrollWidth : "6px", 140 | virtualized: this.isVirtualized, 141 | }, 142 | !!this.props.rtl, 143 | ), 144 | }), 145 | ); 146 | } 147 | 148 | blockSelection(state): void { 149 | if (!state) { 150 | clearSelection(); 151 | } 152 | this.setState({ selection: !!state }); 153 | } 154 | 155 | override componentDidMount(): void { 156 | /** 157 | * If mouse cursor gone outside window 158 | * Will trigger event 'mouseWithoutWindow' 159 | * And all listeners will remove 160 | * Content in scroll block will be selectable 161 | * */ 162 | on(document, ["mouseWithoutWindow"], this.reset); 163 | on(window, ["resize"], this.restScrollAfterResize); 164 | 165 | this.scrollBlock = this.customScrollHolderRef.current; 166 | this.customScroll = this.customScrollRef.current; 167 | this.customScrollHolder = this.customScrollFrameRef.current; 168 | 169 | this.applyStyles(); 170 | 171 | /** 172 | * Reinitialize scroll bar every 250 ms 173 | * */ 174 | this.interval = setInterval(this.reinit, REINIT_MS); 175 | } 176 | 177 | override componentDidUpdate(prevProps): void { 178 | let offsetY = this.props.scrollTo; 179 | 180 | if (isDefined(offsetY) && !isNaN(offsetY)) { 181 | if (prevProps.scrollTo !== offsetY) { 182 | if (this.isVirtualized) { 183 | offsetY = offsetY || 0; 184 | 185 | setTimeout(() => { 186 | this.setState({ 187 | virtualState: this.getScrollBarStyles(offsetY), 188 | }); 189 | }); 190 | } else { 191 | scrollTo(this.scrollBlock, offsetY, this.state.animate); 192 | } 193 | } 194 | } 195 | } 196 | 197 | override componentWillUnmount(): void { 198 | if (isClient()) { 199 | const el = document.getElementById(this.scrollID); 200 | if (el) { 201 | el.parentNode.removeChild(el); 202 | } 203 | } 204 | clearInterval(this.interval); 205 | this.removeListeners(); 206 | } 207 | 208 | getParams(): { height: number; holderHeight: number; percentDiff: number; wrapperHeight: number } { 209 | let wrapperHeight = 0; 210 | let holderHeight = 0; 211 | let percentDiff = 0; 212 | let height = 0; 213 | 214 | if (!isClient()) { 215 | return { 216 | height, 217 | holderHeight, 218 | percentDiff, 219 | wrapperHeight, 220 | }; 221 | } 222 | 223 | const scrollArea = this["scroll-areaRef"].current; 224 | const paddings = 225 | window && scrollArea 226 | ? parseFloat(window.getComputedStyle(scrollArea, null).getPropertyValue("padding-top")) + 227 | parseFloat(window.getComputedStyle(scrollArea, null).getPropertyValue("padding-bottom")) 228 | : 0; 229 | 230 | if (this.isVirtualized) { 231 | wrapperHeight = this.props.virtualized.height || 0; 232 | holderHeight = this.props.virtualized.scrollHeight || 0; 233 | } else { 234 | wrapperHeight = this.customScroll && this.customScroll.offsetHeight; 235 | holderHeight = this.customScroll && this.customScrollHolder.offsetHeight; 236 | } 237 | if (holderHeight === 0) { 238 | height = 0; 239 | percentDiff = 0; 240 | } else { 241 | percentDiff = (wrapperHeight - paddings) / holderHeight; 242 | height = wrapperHeight * percentDiff; 243 | } 244 | 245 | return { 246 | height, 247 | holderHeight, 248 | percentDiff, 249 | wrapperHeight: Math.ceil(wrapperHeight), 250 | }; 251 | } 252 | 253 | getScrollBarStyles(offsetY = 0): { height: number; top: number } { 254 | const { height, holderHeight, percentDiff } = this.getParams(); 255 | 256 | if (holderHeight === 0 && percentDiff === 0 && height === 0) { 257 | return { 258 | height: 0, 259 | top: 0, 260 | }; 261 | } 262 | 263 | const scrollTop = this.isVirtualized ? offsetY : this.state.scrollTop || this.scrollBlock.scrollTop; 264 | 265 | const newPercentDiff = 266 | height < minHeightScrollBar ? percentDiff - (minHeightScrollBar - height) / holderHeight : percentDiff; 267 | 268 | const scrollBarHeight = height < minHeightScrollBar ? minHeightScrollBar : height; 269 | 270 | return { 271 | height: scrollBarHeight, 272 | top: scrollTop * newPercentDiff, 273 | }; 274 | } 275 | 276 | jump = (e): void => { 277 | const y = e.touches ? e.touches[0].pageY : e.pageY; 278 | let scrollBar = this.scrollBarRef.current as { offsetHeight: number; offsetTop: number }; 279 | let scrollPosition = this.scrollBlock.scrollTop; 280 | const { wrapperHeight } = this.getParams(); 281 | const topOffset = this.scrollBlock.getBoundingClientRect().top; 282 | 283 | if (this.isVirtualized) { 284 | scrollPosition = this.props.scrollTo || 0; 285 | scrollBar = { 286 | offsetHeight: this.state.virtualState.height, 287 | offsetTop: this.state.virtualState.top, 288 | }; 289 | } 290 | if (y < topOffset + scrollBar.offsetTop || y > topOffset + scrollBar.offsetTop + scrollBar.offsetHeight) { 291 | const offset = topOffset + scrollBar.offsetTop <= y ? 1 : -1; 292 | const scrollY = scrollPosition + wrapperHeight * offset; 293 | if (this.isVirtualized) { 294 | if (isFunction(this.props.scrollSync)) { 295 | this.props.scrollSync(scrollY); 296 | } 297 | } else { 298 | scrollTo(this.scrollBlock, scrollY); 299 | } 300 | } 301 | }; 302 | 303 | onClick = (evt): void => { 304 | evt.stopPropagation(); 305 | evt.preventDefault(); 306 | /** 307 | * If we clicked right mouse button we must skip this event 308 | * */ 309 | let isRightMB; 310 | if ("which" in evt) { 311 | isRightMB = evt.which === 3; 312 | } else if ("button" in evt) { 313 | isRightMB = evt.button === 2; 314 | } 315 | if (isRightMB) { 316 | setTimeout(this.reset); 317 | 318 | return; 319 | } 320 | 321 | const elem = this.scrollBlock; 322 | const startPoint = evt.touches ? evt.touches[0].pageY : evt.pageY; 323 | 324 | const scrollTopOffset = this.isVirtualized ? this.props.scrollTo || 0 : elem.scrollTop; 325 | this.blockSelection(false); 326 | 327 | this.scrollRun = (e): void => { 328 | e.stopPropagation(); 329 | e.preventDefault(); 330 | const { holderHeight, wrapperHeight } = this.getParams(); 331 | const diff = holderHeight / wrapperHeight; 332 | const pageY = e.touches ? e.touches[0].pageY : e.pageY; 333 | if (this.isVirtualized) { 334 | let scrollTop = (pageY - startPoint) * diff + scrollTopOffset; 335 | scrollTop = holderHeight - wrapperHeight <= scrollTop ? holderHeight - wrapperHeight : scrollTop; 336 | if (isFunction(this.props.scrollSync)) { 337 | this.props.scrollSync(scrollTop); 338 | } 339 | } else { 340 | scrollTo(elem, (pageY - startPoint) * diff + scrollTopOffset); 341 | } 342 | }; 343 | 344 | this.endScroll = (): void => { 345 | this.reset(); 346 | }; 347 | 348 | on(document, ["mouseup", "touchend"], this.endScroll); 349 | on(document, ["mousemove", "touchmove"], this.scrollRun); 350 | }; 351 | 352 | reinit = (): void => { 353 | const { holderHeight, wrapperHeight } = this.getParams(); 354 | 355 | if (wrapperHeight !== this.nextWrapperHeight || holderHeight !== this.nextHolderHeight) { 356 | if (this.isVirtualized) { 357 | const scrollPosition = this.props.scrollTo || 0; 358 | const virtualState = this.getScrollBarStyles(scrollPosition); 359 | this.setState({ 360 | scrollAreaShow: holderHeight > wrapperHeight, 361 | virtualState, 362 | }); 363 | } else { 364 | this.setState({ 365 | scrollAreaShow: holderHeight > wrapperHeight, 366 | }); 367 | } 368 | } 369 | 370 | this.nextWrapperHeight = wrapperHeight; 371 | this.nextHolderHeight = holderHeight; 372 | }; 373 | 374 | removeListeners(): void { 375 | off(document, ["mouseWithoutWindow"], this.reset); 376 | off(window, ["resize"], this.restScrollAfterResize); 377 | off(document, ["mouseup", "touchend"], this.endScroll); 378 | off(document, ["mousemove", "touchmove"], this.scrollRun); 379 | } 380 | 381 | override render(): ReactNode { 382 | const ctmScroll = !this.state.selection 383 | ? Object.assign({}, this.state.styles.ctmScroll, this.state.styles.noselect) 384 | : this.state.styles.ctmScroll; 385 | const ctmScrollFrame = this.state.scrollAreaShow 386 | ? Object.assign({}, this.state.styles.ctmScrollFrame, this.state.styles.ctmScrollActive) 387 | : this.state.styles.ctmScrollFrame; 388 | 389 | return ( 390 |
396 |
410 |
422 | {isFunction(this.props.children) 423 | ? this.props.children(this.scrollBlock && this.scrollBlock.scrollTop ? this.scrollBlock.scrollTop : 0) 424 | : this.props.children} 425 |
426 | {this.state.scrollAreaShow ? ( 427 |
433 |
438 |
449 |
450 |
451 | ) : null} 452 |
453 |
454 | ); 455 | } 456 | 457 | reset = (): void => { 458 | this.removeListeners(); 459 | this.blockSelection(true); 460 | }; 461 | 462 | restScrollAfterResize = (): void => { 463 | this.nextWrapperHeight = 0; 464 | this.nextHolderHeight = 0; 465 | }; 466 | 467 | scroll = (): void => { 468 | this.setState({ 469 | scrollTop: this.scrollBlock.scrollTop, 470 | }); 471 | }; 472 | 473 | setY(value: number): void { 474 | scrollTo(this.scrollBlock, value, this.state.animate); 475 | } 476 | } 477 | 478 | export { CustomScroll, getDefaultScrollWidth }; 479 | -------------------------------------------------------------------------------- /example/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef, StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import CustomScroll from '../../src'; 4 | import './styles/example.css'; 5 | 6 | class Layout extends Component { 7 | constructor(prop) { 8 | super(prop); 9 | this.sticky = createRef(); 10 | 11 | this.anchors = {}; 12 | 13 | this.state = { 14 | data: [ 15 | 'Click add please! ' 16 | ], 17 | mount: true, 18 | scrollTop: 0 19 | }; 20 | } 21 | 22 | setScrollTo = (e) => { 23 | e.preventDefault(); 24 | const anchorOffset = this.anchors[e.currentTarget.dataset.anc].offsetTop; 25 | 26 | this.scrollWithAnchor.setY(anchorOffset); 27 | } 28 | 29 | add = () => { 30 | this.setState(state => ( 31 | Object.assign(state, { 32 | data: [...state.data, 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad, animi aspernatur cum debitis deleniti deserunt distinctio eius esse expedita facilis harum illo iure iusto molestiae, perferendis quam, quidem quos tenetur.'] 33 | }) 34 | )); 35 | } 36 | 37 | render() { 38 | return ( 39 | 40 |
41 |
42 |

React CustomScroll

43 |

44 | This is a tiny React component for scroll bar customization, without dependencies but with a lot of 45 | features. 46 |

47 |

Features

48 |
    49 |
  • - React 19 support
  • 50 |
  • - TypeScript support
  • 51 |
  • - Extremely small size (11kb)
  • 52 |
  • - Without dependencies
  • 53 |
  • - Easy customization, simple api
  • 54 |
  • - Native OS scroll behavior
  • 55 |
  • - Cross browser
  • 56 |
  • - Animate scrollTo feature
  • 57 |
  • - RTL support
  • 58 |
  • - Server Side Rendering support
  • 59 |
  • - Scroll snap support
  • 60 |
61 |

License MIT

62 |

63 | Github 64 |

65 |
66 |
67 |
68 |

Scroll by anchor:

69 | 75 |
76 | this.scrollWithAnchor = c}> 77 |

this.anchors.anc1 = c}>Anchor 1

78 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 79 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 80 | eos necessitatibus saepe voluptatem?

81 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 82 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 83 | eos necessitatibus saepe voluptatem?

84 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 85 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 86 | eos necessitatibus saepe voluptatem?

87 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 88 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 89 | eos necessitatibus saepe voluptatem?

90 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 91 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 92 | eos necessitatibus saepe voluptatem?

93 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 94 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 95 | eos necessitatibus saepe voluptatem?

96 |

this.anchors.anc2 = c}>Anchor 2

97 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 98 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 99 | eos necessitatibus saepe voluptatem?

100 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 101 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 102 | eos necessitatibus saepe voluptatem?

103 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 104 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 105 | eos necessitatibus saepe voluptatem?

106 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 107 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 108 | eos necessitatibus saepe voluptatem?

109 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 110 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 111 | eos necessitatibus saepe voluptatem?

112 |

this.anchors.anc3 = c}>Anchor 3

113 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 114 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 115 | eos necessitatibus saepe voluptatem?

116 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 117 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 118 | eos necessitatibus saepe voluptatem?

119 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 120 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 121 | eos necessitatibus saepe voluptatem?

122 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 123 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 124 | eos necessitatibus saepe voluptatem?

125 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 126 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 127 | eos necessitatibus saepe voluptatem?

128 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 129 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 130 | eos necessitatibus saepe voluptatem?

131 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 132 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 133 | eos necessitatibus saepe voluptatem?

134 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 135 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 136 | eos necessitatibus saepe voluptatem?

137 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 138 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 139 | eos necessitatibus saepe voluptatem?

140 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 141 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 142 | eos necessitatibus saepe voluptatem?

143 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 144 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 145 | eos necessitatibus saepe voluptatem?

146 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 147 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 148 | eos necessitatibus saepe voluptatem?

149 |

this.anchors.anc4 = c}>Anchor 4

150 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 151 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 152 | eos necessitatibus saepe voluptatem?

153 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 154 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 155 | eos necessitatibus saepe voluptatem?

156 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 157 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 158 | eos necessitatibus saepe voluptatem?

159 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 160 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 161 | eos necessitatibus saepe voluptatem?

162 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 163 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 164 | eos necessitatibus saepe voluptatem?

165 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 166 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 167 | eos necessitatibus saepe voluptatem?

168 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 169 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 170 | eos necessitatibus saepe voluptatem?

171 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 172 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 173 | eos necessitatibus saepe voluptatem?

174 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 175 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 176 | eos necessitatibus saepe voluptatem?

177 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 178 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 179 | eos necessitatibus saepe voluptatem?

180 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 181 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 182 | eos necessitatibus saepe voluptatem?

183 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 184 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 185 | eos necessitatibus saepe voluptatem?

186 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 187 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 188 | eos necessitatibus saepe voluptatem?

189 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 190 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 191 | eos necessitatibus saepe voluptatem?

192 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 193 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 194 | eos necessitatibus saepe voluptatem?

195 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 196 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 197 | eos necessitatibus saepe voluptatem?

198 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 199 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 200 | eos necessitatibus saepe voluptatem?

201 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 202 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 203 | eos necessitatibus saepe voluptatem?

204 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 205 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 206 | eos necessitatibus saepe voluptatem?

207 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 208 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 209 | eos necessitatibus saepe voluptatem?

210 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 211 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 212 | eos necessitatibus saepe voluptatem?

213 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 214 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 215 | eos necessitatibus saepe voluptatem?

216 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 217 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 218 | eos necessitatibus saepe voluptatem?

219 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 220 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 221 | eos necessitatibus saepe voluptatem?

222 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 223 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 224 | eos necessitatibus saepe voluptatem?

225 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 226 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 227 | eos necessitatibus saepe voluptatem?

228 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 229 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 230 | eos necessitatibus saepe voluptatem?

231 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 232 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 233 | eos necessitatibus saepe voluptatem?

234 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 235 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 236 | eos necessitatibus saepe voluptatem?

237 |

The end

238 |
239 |
240 |
241 |
242 |

Auto reinit:

243 |
244 | 245 | {this.state.data.map((p, k) =>

{p}

)} 246 |
247 |
248 | 249 |
250 |
251 |
252 |

Textarea scroll:

253 |
254 | 255 |
256 | I look like a textarea 257 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 258 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 259 | eos necessitatibus saepe voluptatem? 260 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 261 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 262 | eos necessitatibus saepe voluptatem? 263 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 264 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 265 | eos necessitatibus saepe voluptatem? 266 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 267 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 268 | eos necessitatibus saepe voluptatem? 269 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 270 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 271 | eos necessitatibus saepe voluptatem? 272 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 273 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 274 | eos necessitatibus saepe voluptatem? 275 |
276 |
277 |
278 |
279 |
280 |

Customization scroll

281 |
282 | 283 |

284 | I look like a textarea 285 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 286 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 287 | necessitatibus saepe voluptatem? 288 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 289 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 290 | necessitatibus saepe voluptatem? 291 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 292 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 293 | necessitatibus saepe voluptatem? 294 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 295 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 296 | necessitatibus saepe voluptatem? 297 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 298 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 299 | necessitatibus saepe voluptatem? 300 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 301 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 302 | necessitatibus saepe voluptatem? 303 |

304 |
305 |
306 |
307 |

Sticky block

308 | 309 | {scroll => { 310 | let offset = scroll; 311 | if (this.sticky.current) { 312 | if (scroll >= this.sticky.current.offsetHeight - 60) { 313 | offset = this.sticky.current.offsetHeight - 60; 314 | } 315 | } 316 | return ( 317 | <> 318 |
327 | I am sticky 328 |
329 |

330 | I look like a textarea 331 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 332 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 333 | eos necessitatibus saepe voluptatem? 334 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 335 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 336 | eos necessitatibus saepe voluptatem? 337 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 338 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 339 | eos necessitatibus saepe voluptatem? 340 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 341 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 342 | eos necessitatibus saepe voluptatem? 343 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 344 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 345 | eos necessitatibus saepe voluptatem? 346 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 347 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae 348 | eos necessitatibus saepe voluptatem? 349 |

350 | 351 | ); 352 | }} 353 |
354 |
355 |
356 |

Unmount scroll

357 | 360 |
361 | {this.state.mount && 362 | 363 |

364 | I look like a textarea 365 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 366 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 367 | necessitatibus saepe voluptatem? 368 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 369 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 370 | necessitatibus saepe voluptatem? 371 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 372 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 373 | necessitatibus saepe voluptatem? 374 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 375 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 376 | necessitatibus saepe voluptatem? 377 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 378 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 379 | necessitatibus saepe voluptatem? 380 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid dignissimos distinctio, eaque eos 381 | libero maxime, modi natus necessitatibus nesciunt quis reiciendis rem repellat rerum. Aliquam beatae eos 382 | necessitatibus saepe voluptatem? 383 |

384 |
385 | } 386 |
387 |
388 |

RTL example:

389 |
390 |
391 | 392 |

?سوف أعطي مثالا على ذلك. لا تمانع

393 |

?سوف أعطي مثالا على ذلك. لا تمانع

394 |

?سوف أعطي مثالا على ذلك. لا تمانع

395 |

?سوف أعطي مثالا على ذلك. لا تمانع

396 |

?سوف أعطي مثالا على ذلك. لا تمانع

397 |

?سوف أعطي مثالا على ذلك. لا تمانع

398 |

?سوف أعطي مثالا على ذلك. لا تمانع

399 |

?سوف أعطي مثالا على ذلك. لا تمانع

400 |

?سوف أعطي مثالا على ذلك. لا تمانع

401 |

?سوف أعطي مثالا على ذلك. لا تمانع

402 |

?سوف أعطي مثالا على ذلك. لا تمانع

403 |

?سوف أعطي مثالا على ذلك. لا تمانع

404 |

?سوف أعطي مثالا على ذلك. لا تمانع

405 |

?سوف أعطي مثالا على ذلك. لا تمانع

406 |

?سوف أعطي مثالا على ذلك. لا تمانع

407 |
408 |
409 |
410 |
411 |
412 |

Scroll Snap

413 |
414 | 415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 | 424 |
425 |
426 |
427 | 428 | ) 429 | } 430 | } 431 | 432 | const root = createRoot(document.getElementById('root')); 433 | 434 | root.render( 435 | 436 | 437 | 438 | ); 439 | -------------------------------------------------------------------------------- /example/src/styles/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | --------------------------------------------------------------------------------