├── packages ├── scene │ ├── src │ │ ├── index.ts │ │ └── components │ │ │ └── StickyScene.tsx │ ├── tsconfig.json │ ├── webpack.config.dist.js │ ├── __tests__ │ │ └── scene.test.js │ ├── README.md │ ├── tsconfig.build.json │ └── package.json ├── trigger │ ├── src │ │ ├── index.ts │ │ └── hooks │ │ │ └── useIntersectingTrigger.ts │ ├── tsconfig.json │ ├── webpack.config.dist.js │ ├── __tests__ │ │ └── magic.test.js │ ├── README.md │ ├── tsconfig.build.json │ └── package.json ├── core │ ├── tsconfig.json │ ├── src │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── ScrollPosition.ts │ │ │ └── IntersectionObserverConfig.ts │ │ ├── index.ts │ │ ├── utils │ │ │ ├── getScrollPosition.ts │ │ │ ├── getIntersectionObserver.ts │ │ │ └── getStickyPosition.ts │ │ ├── context │ │ │ └── PageContext.ts │ │ ├── hooks │ │ │ ├── section │ │ │ │ ├── useSection.ts │ │ │ │ └── useScrolledRatio.ts │ │ │ ├── common │ │ │ │ ├── useSubscription.ts │ │ │ │ ├── useIntersectionObservable.ts │ │ │ │ ├── usePageScroll.ts │ │ │ │ └── useSectionPosition.ts │ │ │ └── page │ │ │ │ └── useActiveSectionTracker.ts │ │ └── components │ │ │ ├── Section.tsx │ │ │ └── PageProvider.tsx │ ├── webpack.config.dist.js │ ├── __tests__ │ │ └── core.test.js │ ├── README.md │ ├── tsconfig.build.json │ └── package.json └── plot │ ├── tsconfig.json │ ├── webpack.config.dist.js │ ├── __tests__ │ └── plot.test.js │ ├── README.md │ ├── src │ ├── index.ts │ ├── components │ │ ├── Plot.tsx │ │ └── StickyPlot.tsx │ └── hooks │ │ ├── usePlot.tsx │ │ └── useActiveSectionInfo.ts │ ├── tsconfig.build.json │ └── package.json ├── _docs ├── public │ ├── favicon.png │ ├── logo-long.png │ └── scrolled_ratio.png ├── tsconfig.json └── src │ ├── config │ ├── media-queries.js │ ├── theme.js │ └── shared-styles.js │ ├── components │ ├── DemoStickyPlot.jsx │ ├── DemoStickyScene.jsx │ ├── DemoWrapper.jsx │ ├── DemoSection.jsx │ ├── DescriptionBox.jsx │ ├── CenterBox.jsx │ ├── AnimatedFadeIn.jsx │ └── AnimatedTrail.jsx │ ├── index.mdx │ ├── PinningSections │ ├── StickyScene.mdx │ └── StickyPlot.mdx │ ├── RevealingAnimations │ └── Trigger.mdx │ └── ScrollTracking │ ├── Section.mdx │ └── Plot.mdx ├── configs ├── tsconfig.build.json ├── .eslintrc.js ├── tsconfig.json └── webpack.config.base.js ├── .npmrc ├── .gitignore ├── prettier.config.js ├── .vscode ├── settings.json └── main.yml ├── docs ├── src │ ├── config │ │ ├── media-queries.js │ │ ├── theme.js │ │ └── shared-styles.js │ ├── components │ │ ├── DemoStickyPlot.jsx │ │ ├── DemoStickyScene.jsx │ │ ├── DemoWrapper.jsx │ │ ├── DemoSection.jsx │ │ ├── DescriptionBox.jsx │ │ ├── CenterBox.jsx │ │ ├── AnimatedFadeIn.jsx │ │ └── AnimatedTrail.jsx │ ├── PinningSections │ │ ├── StickyScene.mdx │ │ └── StickyPlot.mdx │ ├── RevealingAnimations │ │ └── Trigger.mdx │ └── ScrollTracking │ │ ├── Section.mdx │ │ └── Plot.mdx ├── theme │ └── gatsby-theme-docz │ │ ├── components │ │ └── Logo │ │ │ └── index.js │ │ └── wrapper.jsx ├── doczrc.js ├── package.json └── index.mdx ├── lerna.json ├── .editorconfig ├── .github └── workflows │ ├── canary.yml │ └── main.yml ├── package.json ├── doczrc.js └── README.md /packages/scene/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/StickyScene'; 2 | -------------------------------------------------------------------------------- /packages/trigger/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks/useIntersectingTrigger'; 2 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/plot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/scene/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/trigger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /_docs/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsunpei/react-scrolly/HEAD/_docs/public/favicon.png -------------------------------------------------------------------------------- /configs/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@garfieldduck/typescript-config/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /_docs/public/logo-long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsunpei/react-scrolly/HEAD/_docs/public/logo-long.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @garfieldduck:registry=https://npm.pkg.github.com/ 2 | @react-scrolly:registry=https://registry.npmjs.org 3 | -------------------------------------------------------------------------------- /_docs/public/scrolled_ratio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsunpei/react-scrolly/HEAD/_docs/public/scrolled_ratio.png -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IntersectionObserverConfig'; 2 | export * from './ScrollPosition'; 3 | -------------------------------------------------------------------------------- /packages/core/webpack.config.dist.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../configs/webpack.config.base.js'); 2 | 3 | module.exports = baseConfig; 4 | -------------------------------------------------------------------------------- /packages/plot/webpack.config.dist.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../configs/webpack.config.base.js'); 2 | 3 | module.exports = baseConfig; 4 | -------------------------------------------------------------------------------- /packages/scene/webpack.config.dist.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../configs/webpack.config.base.js'); 2 | 3 | module.exports = baseConfig; 4 | -------------------------------------------------------------------------------- /packages/trigger/webpack.config.dist.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../configs/webpack.config.base.js'); 2 | 3 | module.exports = baseConfig; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .DS_Store 3 | node_modules/ 4 | .next/ 5 | out/ 6 | lib/ 7 | dist/ 8 | .docz/ 9 | coverage/ 10 | *-debug.log 11 | *-error.log 12 | -------------------------------------------------------------------------------- /packages/core/__tests__/core.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const core = require('..'); 4 | 5 | describe('core', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/plot/__tests__/plot.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const plot = require('..'); 4 | 5 | describe('plot', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/scene/__tests__/scene.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const scene = require('..'); 4 | 5 | describe('scene', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/trigger/__tests__/magic.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const trigger = require('..'); 4 | 5 | describe('trigger', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/plot/README.md: -------------------------------------------------------------------------------- 1 | # `plot` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const plot = require('plot'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/scene/README.md: -------------------------------------------------------------------------------- 1 | # `scene` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const scene = require('scene'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/plot/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks/useActiveSectionInfo'; 2 | export * from './hooks/usePlot'; 3 | export * from './components/Plot'; 4 | export * from './components/StickyPlot'; 5 | -------------------------------------------------------------------------------- /packages/trigger/README.md: -------------------------------------------------------------------------------- 1 | # `trigger` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const trigger = require('trigger'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable global-require */ 3 | module.exports = { 4 | ...require('@garfieldduck/prettier-config'), 5 | }; 6 | -------------------------------------------------------------------------------- /configs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@garfieldduck', 3 | rules: { 4 | 'react/jsx-props-no-spreading': 'warn', 5 | 'react/require-default-props': 'warn', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "eslint.options": { 6 | "extensions": [".js", ".jsx", ".md", ".mdx", ".ts", ".tsx"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # `core` 2 | 3 | > TODO: description 4 | 5 | ## Scrollytelling made easy with react-scrolly 6 | 7 | ``` 8 | const core = require('core'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /configs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@react-scrolly/*": ["packages/*/src"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist/esm" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/plot/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist/esm" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/scene/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist/esm" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/trigger/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "./dist/esm" 6 | }, 7 | 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /_docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "typeRoots": ["../../node_modules/@types", "node_modules/@types"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /_docs/src/config/media-queries.js: -------------------------------------------------------------------------------- 1 | import { generateMedia } from 'styled-media-query'; 2 | 3 | const mq = generateMedia({ 4 | desktop: '78em', 5 | large: '70em', 6 | tablet: '60.5em', 7 | mobile: '46em', 8 | }); 9 | 10 | export default mq; 11 | -------------------------------------------------------------------------------- /docs/src/config/media-queries.js: -------------------------------------------------------------------------------- 1 | import { generateMedia } from 'styled-media-query'; 2 | 3 | const mq = generateMedia({ 4 | desktop: '78em', 5 | large: '70em', 6 | tablet: '60.5em', 7 | mobile: '46em', 8 | }); 9 | 10 | export default mq; 11 | -------------------------------------------------------------------------------- /_docs/src/components/DemoStickyPlot.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { StickyPlot } from '@react-scrolly/plot' 3 | 4 | import { borderedStyle } from '../config/shared-styles'; 5 | 6 | export const BorderedStickyPlot = styled(StickyPlot)`${borderedStyle}`; 7 | -------------------------------------------------------------------------------- /docs/src/components/DemoStickyPlot.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { StickyPlot } from '@react-scrolly/plot' 3 | 4 | import { borderedStyle } from '../config/shared-styles'; 5 | 6 | export const BorderedStickyPlot = styled(StickyPlot)`${borderedStyle}`; 7 | -------------------------------------------------------------------------------- /_docs/src/components/DemoStickyScene.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { StickyScene } from '@react-scrolly/scene' 3 | 4 | import { borderedStyle } from '../config/shared-styles'; 5 | 6 | export const BorderedStickyScene = styled(StickyScene)`${borderedStyle}`; 7 | -------------------------------------------------------------------------------- /docs/src/components/DemoStickyScene.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { StickyScene } from '@react-scrolly/scene' 3 | 4 | import { borderedStyle } from '../config/shared-styles'; 5 | 6 | export const BorderedStickyScene = styled(StickyScene)`${borderedStyle}`; 7 | -------------------------------------------------------------------------------- /_docs/src/components/DemoWrapper.jsx: -------------------------------------------------------------------------------- 1 | import styledComponents from 'styled-components'; 2 | 3 | export const DemoWrapper = styledComponents.div` 4 | padding: 2rem 1rem; 5 | margin: 1.8rem 0; 6 | border-radius: 5px; 7 | box-shadow: 0 12px 22px 0 rgba(0,0,0,.06), 0 6px 14px 0 rgba(0,0,0,.03); 8 | `; 9 | -------------------------------------------------------------------------------- /docs/src/components/DemoWrapper.jsx: -------------------------------------------------------------------------------- 1 | import styledComponents from 'styled-components'; 2 | 3 | export const DemoWrapper = styledComponents.div` 4 | padding: 2rem 1rem; 5 | margin: 1.8rem 0; 6 | border-radius: 5px; 7 | box-shadow: 0 12px 22px 0 rgba(180,180,180,.3), 0 6px 14px 0 rgba(180,180,180,.3); 8 | `; 9 | -------------------------------------------------------------------------------- /docs/theme/gatsby-theme-docz/components/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Logo = () => ( 4 | react-scrolly 9 | ); 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.0.0", 6 | "npmClient": "yarn", 7 | "command": { 8 | "publish": { 9 | "registry": "https://registry.npmjs.org", 10 | "ignoreChanges": [ 11 | "docs" 12 | ] 13 | } 14 | }, 15 | "useWorkspaces": true 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/main.yml: -------------------------------------------------------------------------------- 1 | name: Update docs 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Build packages & docs 12 | run: yarn clean && yarn build 13 | - name: Deploy 14 | uses: peaceiris/actions-gh-pages@v3 15 | with: 16 | github_token: ${{ secrets.GITHUB_TOKEN }} 17 | publish_dir: ./docs/.docz 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | # Unix-style newlines with a newline ending every file 8 | charset = utf-8 9 | indent_size = 2 10 | indent_style = space 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.json] 16 | indent_size = 2 17 | 18 | [*.js] 19 | trim_trailing_whitespace = true 20 | -------------------------------------------------------------------------------- /docs/theme/gatsby-theme-docz/wrapper.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Helmet } from 'react-helmet-async'; 3 | 4 | // The doc prop contains some metadata about the page being rendered that you can use. 5 | const Wrapper = ({ children, _ }) => ( 6 | <> 7 | 8 | 13 | 14 | {children} 15 | 16 | ); 17 | export default Wrapper; 18 | -------------------------------------------------------------------------------- /packages/core/src/types/ScrollPosition.ts: -------------------------------------------------------------------------------- 1 | export interface ScrollPosition { 2 | /** The pageYOffset of the window obtained in */ 3 | scrollTop: number; 4 | 5 | /** The pageYOffset + height of the window obtained in */ 6 | scrollBottom: number; 7 | 8 | /** The height of the window obtained in */ 9 | windowHeight: number; 10 | 11 | /** 12 | * The difference between the current scrolltop and previous scrolltop obtained in . 13 | * Positive: if the user scroll down the page. 14 | */ 15 | scrollOffset: number; 16 | } 17 | -------------------------------------------------------------------------------- /_docs/src/config/theme.js: -------------------------------------------------------------------------------- 1 | const RED = '#ef3e36'; 2 | 3 | export const defaultColors = { 4 | primary: RED, 5 | background: '#f3f3f3', 6 | text: '#282c34', 7 | gray: '#8a96b1', 8 | blue: '#1b76f1', 9 | white: '#ffffff', 10 | red: RED, 11 | orange: '#ff8e2b', 12 | yellow: '#ffd60a', 13 | green: '#24da74', 14 | purple: '#8755d6', 15 | } 16 | 17 | export const sectionStyle = { 18 | margin: 'auto', 19 | height: '30vh', 20 | color: defaultColors.blue, 21 | border: `3px solid ${defaultColors.gray}`, 22 | padding: '1rem', 23 | marginTop: '-3px', 24 | borderRadius: '3px', 25 | }; 26 | -------------------------------------------------------------------------------- /docs/src/config/theme.js: -------------------------------------------------------------------------------- 1 | const RED = '#ef3e36'; 2 | 3 | export const defaultColors = { 4 | primary: RED, 5 | background: '#f3f3f3', 6 | text: '#282c34', 7 | gray: '#8a96b1', 8 | blue: '#1b76f1', 9 | white: '#ffffff', 10 | red: RED, 11 | orange: '#ff8e2b', 12 | yellow: '#ffd60a', 13 | green: '#24da74', 14 | purple: '#8755d6', 15 | } 16 | 17 | export const sectionStyle = { 18 | margin: 'auto', 19 | height: '30vh', 20 | color: defaultColors.blue, 21 | border: `3px solid ${defaultColors.gray}`, 22 | padding: '1rem', 23 | marginTop: '-3px', 24 | borderRadius: '3px', 25 | }; 26 | -------------------------------------------------------------------------------- /packages/core/src/types/IntersectionObserverConfig.ts: -------------------------------------------------------------------------------- 1 | export interface Margin { 2 | top: number; 3 | right: number; 4 | bottom: number; 5 | left: number; 6 | } 7 | 8 | export interface IntersectionObserverConfig { 9 | /** 10 | * Threshold at which to trigger callback. 11 | * See: https://developers.google.com/web/updates/2016/04/intersectionobserver 12 | */ 13 | threshold?: number[] | 0 | 1; 14 | 15 | /** 16 | * Margins for the root (document’s viewport), 17 | * which allows you to grow or shrink the area used for intersections 18 | */ 19 | rootMargin?: Margin; 20 | } 21 | -------------------------------------------------------------------------------- /docs/doczrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'React-Scrolly', 3 | repository: 'https://github.com/garfieldduck/react-scrolly', 4 | 5 | docgenConfig: { 6 | searchPath: '../packages', 7 | }, 8 | 9 | themesDir: 'theme', 10 | 11 | port: 5000, 12 | 13 | // Ignore README.md 14 | files: '**/*.mdx', 15 | 16 | // order of the menu 17 | menu: [ 18 | 'Introduction', 19 | 'Scroll Tracking', 20 | 'Pinning Sections', 21 | 'Revealing Animations', 22 | ], 23 | 24 | themeConfig: { 25 | colors: { 26 | props: { 27 | highlight: '#ef3e36' 28 | }, 29 | }, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /_docs/src/components/DemoSection.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Section } from '@react-scrolly/core'; 3 | 4 | import { borderedStyle } from '../config/shared-styles'; 5 | 6 | export const DemoSection = styled(Section)` 7 | position: relative; 8 | height: ${({ height = '100vh' }) => height}; 9 | background-image: ${({ gradient = 'linear-gradient(to top, #e6e9f2 0%, #eef1f6 100%)' }) => gradient}; 10 | box-shadow: 0 15px 30px 0 rgba(0,0,0,.11), 0 6px 16px 0 rgba(0,0,0,.08); 11 | border-radius: 2px; 12 | margin: 1rem 0; 13 | `; 14 | 15 | export const BorderedDemoSection = styled(Section)`${borderedStyle}`; 16 | -------------------------------------------------------------------------------- /docs/src/components/DemoSection.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Section } from '@react-scrolly/core'; 3 | 4 | import { borderedStyle } from '../config/shared-styles'; 5 | 6 | export const DemoSection = styled(Section)` 7 | position: relative; 8 | height: ${({ height = '100vh' }) => height}; 9 | background-image: ${({ gradient = 'linear-gradient(to top, #e6e9f2 0%, #eef1f6 100%)' }) => gradient}; 10 | box-shadow: 0 15px 30px 0 rgba(0,0,0,.11), 0 6px 16px 0 rgba(0,0,0,.08); 11 | border-radius: 2px; 12 | margin: 1rem 0; 13 | `; 14 | 15 | export const BorderedDemoSection = styled(Section)`${borderedStyle}`; 16 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/PageProvider'; 2 | export * from './components/Section'; 3 | export * from './context/PageContext'; 4 | export * from './utils/getIntersectionObserver'; 5 | export * from './utils/getStickyPosition'; 6 | export * from './hooks/page/useActiveSectionTracker'; 7 | export * from './hooks/common/useIntersectionObservable'; 8 | export * from './hooks/common/usePageScroll'; 9 | export * from './hooks/common/useSectionPosition'; 10 | export * from './hooks/section/useScrolledRatio'; 11 | export * from './hooks/section/useSection'; 12 | export * from './hooks/common/useSubscription'; 13 | export * from './types'; 14 | -------------------------------------------------------------------------------- /packages/core/src/utils/getScrollPosition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the current window `scrollTop`, `windowHeight` (height of the window), 3 | * and the `scrollBottom` (`scrollTop` + `windowHeight`) 4 | */ 5 | export function getScrollPosition() { 6 | // detect window object to prevent issues in SSR 7 | if (typeof window === 'undefined') { 8 | return { 9 | scrollTop: 0, 10 | scrollBottom: 0, 11 | windowHeight: 10, 12 | }; 13 | } 14 | 15 | const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 16 | const windowHeight = window.innerHeight || document.documentElement.clientHeight; 17 | const scrollBottom = scrollTop + windowHeight; 18 | 19 | return { 20 | scrollTop, 21 | scrollBottom, 22 | windowHeight, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "files": [ 7 | "src/", 8 | "doczrc.js", 9 | "package.json" 10 | ], 11 | "scripts": { 12 | "dev": "docz dev", 13 | "clean": "rimraf -rf .docz", 14 | "build": "docz build", 15 | "serve": "docz serve" 16 | }, 17 | "dependencies": { 18 | "@react-scrolly/core": "^1.0.0", 19 | "@react-scrolly/plot": "^1.0.0", 20 | "@react-scrolly/scene": "^1.0.0", 21 | "@react-scrolly/trigger": "^1.0.0", 22 | "docz": "^2.3.1", 23 | "prop-types": "^15.7.2", 24 | "react": "^16.13.1", 25 | "react-dom": "^16.13.1", 26 | "styled-components": "^5.1.1", 27 | "universal-console": "^0.1.3" 28 | }, 29 | "resolutions": { 30 | "docz/**/gatsby": "2.22.15" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/src/config/shared-styles.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { lighten } from 'polished' 3 | 4 | import mq from '../config/media-queries'; 5 | import { defaultColors } from '../config/theme'; 6 | 7 | export const borderedStyle = css` 8 | position: relative; 9 | margin-top: -3px; 10 | ${({ height }) => height ? `height: ${height}` : null}; 11 | border: ${({ color = defaultColors.gray }) => `2.5px solid ${lighten(0.2)(color)}`}; 12 | border-radius: 2px; 13 | 14 | h5 { 15 | font-size: 1.05rem; 16 | margin-top: -2px; 17 | margin-left: -2px; 18 | color: ${defaultColors.white}; 19 | background-color: ${({ color = defaultColors.text }) => color}; 20 | display: inline-block; 21 | padding: 0.25rem 0.75rem; 22 | border-radius: 2px; 23 | ${mq.greaterThan('mobile')` 24 | font-size: 1.15rem; 25 | `} 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /_docs/src/config/shared-styles.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | import { lighten } from 'polished' 3 | 4 | import mq from '../config/media-queries'; 5 | import { defaultColors } from '../config/theme'; 6 | 7 | export const borderedStyle = css` 8 | position: relative; 9 | margin-top: -3px; 10 | ${({ height }) => height ? `height: ${height}` : null}; 11 | border: ${({ color = defaultColors.gray }) => `2.5px solid ${lighten(0.2)(color)}`}; 12 | border-radius: 2px; 13 | 14 | h5 { 15 | font-size: 1.05rem; 16 | margin-top: -2px; 17 | margin-left: -2px; 18 | color: ${defaultColors.white}; 19 | background-color: ${({ color = defaultColors.text }) => color}; 20 | display: inline-block; 21 | padding: 0.25rem 0.75rem; 22 | border-radius: 2px; 23 | ${mq.greaterThan('mobile')` 24 | font-size: 1.15rem; 25 | `} 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /_docs/src/components/DescriptionBox.jsx: -------------------------------------------------------------------------------- 1 | import styledComponents from 'styled-components'; 2 | import { transparentize } from 'polished' 3 | 4 | import mq from '../config/media-queries'; 5 | import { defaultColors } from '../config/theme'; 6 | 7 | const DEFAULT_COLOR = defaultColors.primary; 8 | 9 | export const DescriptionBox = styledComponents.div` 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | width: 100%; 14 | height: 40%; 15 | box-sizing: border-box; 16 | border: 3px solid ${({ color = DEFAULT_COLOR }) => color}; 17 | border-radius: 2px; 18 | background: ${({ color = DEFAULT_COLOR }) => transparentize(0.8, color)}; 19 | padding: 1.5rem; 20 | 21 | ${mq.greaterThan('mobile')` 22 | width: 55%; 23 | `} 24 | 25 | h4 { 26 | background: ${defaultColors.background}; 27 | display: inline-block; 28 | padding: 0.1rem 0.5rem; 29 | border-radius: 6px; 30 | margin-left: -0.3rem; 31 | color: ${defaultColors.primary}; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /docs/src/components/DescriptionBox.jsx: -------------------------------------------------------------------------------- 1 | import styledComponents from 'styled-components'; 2 | import { transparentize } from 'polished' 3 | 4 | import mq from '../config/media-queries'; 5 | import { defaultColors } from '../config/theme'; 6 | 7 | const DEFAULT_COLOR = defaultColors.primary; 8 | 9 | export const DescriptionBox = styledComponents.div` 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | width: 100%; 14 | height: 40%; 15 | box-sizing: border-box; 16 | border: 3px solid ${({ color = DEFAULT_COLOR }) => color}; 17 | border-radius: 2px; 18 | background: ${({ color = DEFAULT_COLOR }) => transparentize(0.8, color)}; 19 | padding: 1.5rem; 20 | 21 | ${mq.greaterThan('mobile')` 22 | width: 55%; 23 | `} 24 | 25 | h4 { 26 | background: ${defaultColors.background}; 27 | display: inline-block; 28 | padding: 0.1rem 0.5rem; 29 | border-radius: 6px; 30 | margin-left: -0.3rem; 31 | color: ${defaultColors.primary}; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /packages/plot/src/components/Plot.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { 3 | // types 4 | SectionProps, 5 | SectionInfo, 6 | ActiveSectionInfo, 7 | } from '@react-scrolly/core'; 8 | 9 | import { usePlot } from '../hooks/usePlot'; 10 | 11 | export interface PlotInfo extends SectionInfo { 12 | activeSection?: ActiveSectionInfo | null; 13 | } 14 | 15 | export type PlotRenderProps = (plotInfo: PlotInfo) => React.ReactNode; 16 | export interface PlotProps extends Omit { 17 | children: PlotRenderProps; 18 | } 19 | 20 | export const Plot = ({ className, style, children, trackingId, ...restProps }: PlotProps) => { 21 | const plotRef = useRef(null); 22 | const { sectionInfo, activeSection } = usePlot(plotRef, trackingId); 23 | 24 | return ( 25 |
26 | {children({ 27 | ...sectionInfo, 28 | activeSection, 29 | })} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /_docs/src/components/CenterBox.jsx: -------------------------------------------------------------------------------- 1 | import styledComponents from 'styled-components'; 2 | import mq from '../config/media-queries'; 3 | import { defaultColors } from '../config/theme'; 4 | 5 | export const CenterBox = styledComponents.div` 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-50%, -50%); 10 | font-size: 1.1rem; 11 | list-style: none; 12 | 13 | ${mq.greaterThan('mobile')` 14 | font-size: 1.5rem; 15 | `} 16 | 17 | h4 { 18 | background: ${defaultColors.background}; 19 | display: inline-block; 20 | padding: 0.1rem 0.5rem; 21 | border-radius: 6px; 22 | color: ${defaultColors.primary}; 23 | margin-left: -0.5rem; 24 | } 25 | 26 | b { 27 | padding: 0.05rem 0.25rem; 28 | border-radius: 3px; 29 | background: rgba(255, 255, 255, 0.85); 30 | color: ${({ boldColor = defaultColors.blue }) => boldColor}; 31 | } 32 | 33 | li:before { 34 | content: "•"; 35 | padding-right: 1rem; 36 | color: ${defaultColors.gray}; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /docs/src/components/CenterBox.jsx: -------------------------------------------------------------------------------- 1 | import styledComponents from 'styled-components'; 2 | import mq from '../config/media-queries'; 3 | import { defaultColors } from '../config/theme'; 4 | 5 | export const CenterBox = styledComponents.div` 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-50%, -50%); 10 | font-size: 1.1rem; 11 | list-style: none; 12 | 13 | ${mq.greaterThan('mobile')` 14 | font-size: 1.5rem; 15 | `} 16 | 17 | h4 { 18 | background: ${defaultColors.background}; 19 | display: inline-block; 20 | padding: 0.1rem 0.5rem; 21 | border-radius: 6px; 22 | color: ${defaultColors.primary}; 23 | margin-left: -0.5rem; 24 | } 25 | 26 | b { 27 | padding: 0.05rem 0.25rem; 28 | border-radius: 3px; 29 | background: rgba(255, 255, 255, 0.85); 30 | color: ${({ boldColor = defaultColors.blue }) => boldColor}; 31 | } 32 | 33 | li:before { 34 | content: "•"; 35 | padding-right: 1rem; 36 | color: ${defaultColors.gray}; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /packages/plot/src/hooks/usePlot.tsx: -------------------------------------------------------------------------------- 1 | import { useIntersectionObservable, useScrolledRatio } from '@react-scrolly/core'; 2 | 3 | import { useActiveSectionInfo } from './useActiveSectionInfo'; 4 | 5 | export function usePlot( 6 | /** Ref of the plot being tracked */ 7 | plotRef: React.RefObject, 8 | 9 | /** 10 | * By setting an unique Section ID, you can know which section the user is currently viewing. 11 | * If `trackingId` is not null, 12 | * it will trigger the update of the active section infomation managed in ``. 13 | * Please make sure that on the same `scrollTop`, 14 | * there is **NO** more than one tracked section (section with `trackingId`). 15 | */ 16 | trackingId?: string 17 | ) { 18 | const intersectObsr$ = useIntersectionObservable(plotRef, trackingId); 19 | const sectionInfo = useScrolledRatio(plotRef, intersectObsr$, trackingId); 20 | const activeSection = useActiveSectionInfo(intersectObsr$); 21 | 22 | return { 23 | sectionInfo, 24 | ...activeSection, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/utils/getIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionObserverConfig } from '../types/IntersectionObserverConfig'; 2 | 3 | const DEFAULT_THRESHOLD = 0; 4 | const DEFAULT_MARGIN = { 5 | top: 0, 6 | right: 0, 7 | bottom: 0, 8 | left: 0, 9 | }; 10 | 11 | export function getIntersectionObserver( 12 | callback: (entries: IntersectionObserverEntry[]) => void, 13 | { threshold = DEFAULT_THRESHOLD, rootMargin = DEFAULT_MARGIN }: IntersectionObserverConfig = { 14 | threshold: DEFAULT_THRESHOLD, 15 | rootMargin: DEFAULT_MARGIN, 16 | } 17 | ) { 18 | const { top, right, bottom, left } = rootMargin; 19 | 20 | return new IntersectionObserver(callback, { 21 | threshold, 22 | 23 | /** Observe changes in visibility of the section relative to the document's viewport */ 24 | root: null, 25 | 26 | /** 27 | * Watch only the changes in the intersection between the section and the viewport, 28 | * without any added or substracted space 29 | */ 30 | rootMargin: `${top}px ${right}px ${bottom}px ${left}px`, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /_docs/src/components/AnimatedFadeIn.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 4 | 5 | import { defaultColors } from '../config/theme'; 6 | import mq from '../config/media-queries'; 7 | 8 | const Wrapper = styled.div` 9 | transition: all 1s ease-in-out; 10 | opacity: ${ props => props.isIntersecting ? 1 : 0}; 11 | transform: ${ props => props.isIntersecting ? 0 : 'translateY(3.5rem)' }; 12 | color: ${({color = defaultColors.gray}) => color}; 13 | padding: 5%; 14 | font-size: 1.1rem; 15 | 16 | ${mq.greaterThan('mobile')` 17 | font-size: 1.3rem; 18 | `} 19 | `; 20 | 21 | export const AnimatedFadeIn = ({ 22 | children, 23 | trackOnce, 24 | ...restProps 25 | }) => { 26 | const containerRef = useRef(null); 27 | const isIntersecting = useIntersectingTrigger(containerRef, trackOnce); 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /docs/src/components/AnimatedFadeIn.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 4 | 5 | import { defaultColors } from '../config/theme'; 6 | import mq from '../config/media-queries'; 7 | 8 | const Wrapper = styled.div` 9 | transition: all 1s ease-in-out; 10 | opacity: ${ props => props.isIntersecting ? 1 : 0}; 11 | transform: ${ props => props.isIntersecting ? 0 : 'translateY(3.5rem)' }; 12 | color: ${({color = defaultColors.gray}) => color}; 13 | padding: 5%; 14 | font-size: 1.1rem; 15 | 16 | ${mq.greaterThan('mobile')` 17 | font-size: 1.3rem; 18 | `} 19 | `; 20 | 21 | export const AnimatedFadeIn = ({ 22 | children, 23 | trackOnce, 24 | ...restProps 25 | }) => { 26 | const containerRef = useRef(null); 27 | const isIntersecting = useIntersectingTrigger(containerRef, trackOnce); 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/core/src/context/PageContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ScrollPosition } from '../types/ScrollPosition'; 5 | 6 | export type sectionID = string | null; 7 | 8 | export interface ActiveSectionInfo { 9 | /** `trackingId` of the active section (section closest to the bottom of the viewport) */ 10 | id: sectionID; 11 | 12 | /** Ratio of the active section being scrolled */ 13 | ratio: number | null; 14 | } 15 | 16 | export interface ActiveSectionTracker { 17 | addActiveSection: (trackingId: string, sectionTop: number, scrollBottom: number) => void; 18 | removeActiveSection: (trackingId: string, scrollBottom: number) => void; 19 | updateScrollRatio: (trackingId: string, scrolledRatio: number) => void; 20 | activeSectionObs$: Observable; 21 | } 22 | 23 | export interface PageContextInterface extends ActiveSectionTracker { 24 | scrollObs$: Observable; 25 | resizeObs$?: Observable; 26 | } 27 | 28 | export const PageContext = createContext(null); 29 | -------------------------------------------------------------------------------- /packages/scene/src/components/StickyScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { 3 | // hooks 4 | useSection, 5 | // utils 6 | getStickyPosition, 7 | // types 8 | SectionProps, 9 | } from '@react-scrolly/core'; 10 | 11 | export interface StickySceneProps extends SectionProps { 12 | /** 13 | * Render the non-sticky part of the section, 14 | * which is placed on top of the children 15 | */ 16 | renderOverlay: React.ReactNode; 17 | } 18 | 19 | export const StickyScene = ({ 20 | className, 21 | style, 22 | children, 23 | trackingId, 24 | renderOverlay, 25 | ...restProps 26 | }: StickySceneProps) => { 27 | const outerStyle: React.CSSProperties = { 28 | ...style, 29 | position: 'relative', 30 | }; 31 | 32 | const sectionRef = useRef(null); 33 | const sectionInfo = useSection(sectionRef, trackingId); 34 | 35 | const stickyStyle: React.CSSProperties = getStickyPosition(sectionInfo); 36 | 37 | return ( 38 |
39 |
{children(sectionInfo)}
40 |
{renderOverlay}
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /configs/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const dashToCamelCase = (str) => str.split('-').reduce( 4 | (res, segment, idx) => { 5 | if (idx === 0) { 6 | return segment 7 | } 8 | return `${res}${segment[0].toUpperCase()}${segment.substr(1)}`; 9 | }, 10 | '' 11 | ); 12 | 13 | const packageDirname = process.cwd(); 14 | const fullPackageName = process.env.npm_package_name || process.env.PKG_NAME; 15 | const packageName = fullPackageName.replace(/@react-scrolly\//, ''); 16 | 17 | module.exports = { 18 | entry: `./src/index.ts`, 19 | 20 | context: packageDirname, 21 | mode: 'production', 22 | resolve: { 23 | extensions: ['.ts', '.tsx', '.js'], 24 | }, 25 | 26 | output: { 27 | filename: `${packageName}.js`, 28 | path: path.resolve(packageDirname, 'dist'), 29 | library: dashToCamelCase(packageName), 30 | }, 31 | 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.tsx?$/, 36 | loader: 'ts-loader', 37 | options: { 38 | configFile: `${packageDirname}/src/tsconfig.cjs.json`, 39 | }, 40 | }, 41 | ], 42 | }, 43 | 44 | externals: { 45 | react: 'React', 46 | "react-dom": "ReactDOM", 47 | "styled-components": { 48 | commonjs: "styled-components", 49 | commonjs2: "styled-components", 50 | amd: "styled-components", 51 | }, 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-scrolly/core", 3 | "version": "1.0.0", 4 | "description": "A performant scroll progress tracking of sections designed for scrolly-telling.", 5 | "homepage": "https://github.com/garfieldduck/intrasections#readme", 6 | "license": "MIT", 7 | "main": "dist/cjs/index.js", 8 | "module": "dist/esm/index.js", 9 | "types": "dist/esm/index.d.ts", 10 | "sideEffects": false, 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "publishConfig": { 19 | "registry": "https://registry.npmjs.org", 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/garfieldduck/intrasections.git" 25 | }, 26 | "scripts": { 27 | "dev": "yarn compile:esm --watch", 28 | "build": "run-p compile:esm compile:cjs", 29 | "clean": "rimraf -rf ./dist", 30 | "compile:esm": "tsc -p tsconfig.build.json", 31 | "compile:cjs": "tsc -p . -m commonjs --outDir dist/cjs" 32 | }, 33 | "peerDependencies": { 34 | "observable-hooks": "^3.1.2", 35 | "prop-types": "^15.7.2", 36 | "react": "^16.13.1", 37 | "react-dom": "^16.13.1", 38 | "rxjs": "^6.6.2" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/garfieldduck/intrasections/issues" 42 | }, 43 | "gitHead": "7671b4280d28c8a9fe4da2479497024a68d78c1d" 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/hooks/section/useSection.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useIntersectionObservable } from '../common/useIntersectionObservable'; 3 | 4 | import { useScrolledRatio } from './useScrolledRatio'; 5 | import { IntersectionObserverConfig } from '../../types/IntersectionObserverConfig'; 6 | 7 | /** 8 | * Return the `sectionInfo` of obtained from `useScrolledRatio()` 9 | */ 10 | export function useSection( 11 | /** Ref of the section being tracked */ 12 | sectionRef: React.RefObject, 13 | 14 | /** 15 | * By setting an unique Section ID, you can know which section the user is currently viewing. 16 | * If `trackingId` is not null, 17 | * it will trigger the update of the active section infomation managed in ``. 18 | * Please make sure that on the same `scrollTop`, 19 | * there is **NO** more than one tracked section (section with `trackingId`). 20 | */ 21 | trackingId?: string, 22 | 23 | /** 24 | * The array of intersectionRatio thresholds which is used in the options of IntersectionObserver 25 | * @example [0, 0.25, 0.5, 0.75, 1] 26 | */ 27 | threshold?: IntersectionObserverConfig['threshold'] 28 | ) { 29 | const intersectionConfig = useRef({ 30 | threshold, 31 | }); 32 | const intersectObsr$ = useIntersectionObservable( 33 | sectionRef, 34 | trackingId, 35 | intersectionConfig.current 36 | ); 37 | return useScrolledRatio(sectionRef, intersectObsr$, trackingId); 38 | } 39 | -------------------------------------------------------------------------------- /packages/scene/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-scrolly/scene", 3 | "version": "1.0.0", 4 | "description": "Provides various kinds of sections designed for scrolly-telling.", 5 | "homepage": "https://github.com/garfieldduck/intrasections#readme", 6 | "license": "MIT", 7 | "main": "dist/cjs/index.js", 8 | "module": "dist/esm/index.js", 9 | "types": "dist/esm/index.d.ts", 10 | "sideEffects": false, 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "publishConfig": { 19 | "registry": "https://registry.npmjs.org", 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/garfieldduck/intrasections.git" 25 | }, 26 | "scripts": { 27 | "dev": "yarn compile:esm --watch", 28 | "build": "run-p compile:esm compile:cjs", 29 | "clean": "rimraf -rf ./dist", 30 | "compile:esm": "tsc -p tsconfig.build.json", 31 | "compile:cjs": "tsc -p . -m commonjs --outDir dist/cjs" 32 | }, 33 | "peerDependencies": { 34 | "@react-scrolly/core": "^0.2.1-alpha.4", 35 | "prop-types": "^15.7.2", 36 | "react": "^16.13.1", 37 | "react-dom": "^16.13.1" 38 | }, 39 | "devDependencies": { 40 | "@react-scrolly/core": "^1.0.0" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/garfieldduck/intrasections/issues" 44 | }, 45 | "gitHead": "7671b4280d28c8a9fe4da2479497024a68d78c1d" 46 | } 47 | -------------------------------------------------------------------------------- /packages/plot/src/components/StickyPlot.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { 3 | // utils 4 | getStickyPosition, 5 | } from '@react-scrolly/core'; 6 | 7 | import { usePlot } from '../hooks/usePlot'; 8 | import { PlotProps } from './Plot'; 9 | 10 | export interface StickyPlotProps extends PlotProps { 11 | /** 12 | * Render the non-sticky part of the section, 13 | * which is placed on top of the children 14 | */ 15 | renderOverlay: React.ReactNode; 16 | } 17 | 18 | export const Overlay = React.memo(({ children }) => { 19 | return
{children}
; 20 | }); 21 | 22 | export const StickyPlot = ({ 23 | className, 24 | style, 25 | children, 26 | trackingId, 27 | renderOverlay, 28 | ...restProps 29 | }: StickyPlotProps) => { 30 | const outerStyle: React.CSSProperties = { 31 | ...style, 32 | position: 'relative', 33 | }; 34 | 35 | const plotRef = useRef(null); 36 | 37 | const { sectionInfo, activeSection } = usePlot(plotRef, trackingId); 38 | const stickyStyle: React.CSSProperties = getStickyPosition(sectionInfo); 39 | 40 | return ( 41 |
42 | {/* Sticky background */} 43 |
44 | {children({ 45 | ...sectionInfo, 46 | activeSection, 47 | })} 48 |
49 | {/* Overlay on top of the background */} 50 | {renderOverlay} 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/core/src/hooks/common/useSubscription.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * DEPRECATED: use observable-hooks as its syntax is much cleaner when managing the states 3 | * Ref: https://github.com/crimx/observable-hooks 4 | */ 5 | import { useEffect, useRef, useCallback } from 'react'; 6 | import { Subscription } from 'rxjs'; 7 | 8 | export interface SubscriptionControl { 9 | /** Subscription to a stream */ 10 | subscription: Subscription | null; 11 | 12 | /** Unsubscribe to the current subscription */ 13 | unSubscribe: () => void; 14 | 15 | /** Change the subscription */ 16 | setSubscription: (sub: Subscription) => void; 17 | } 18 | 19 | export function useSubscription(subscription: SubscriptionControl['subscription']) { 20 | const subscriptionRef = useRef(subscription); 21 | 22 | /** Function to unscribe the current subscription */ 23 | const unSubscribe = useCallback(() => { 24 | if (subscriptionRef.current) { 25 | subscriptionRef.current.unsubscribe(); 26 | } 27 | }, []); 28 | 29 | /** Function to set the latest subscription */ 30 | const setSubscription = useCallback( 31 | (subs) => { 32 | unSubscribe(); 33 | subscriptionRef.current = subs; 34 | }, 35 | [unSubscribe] 36 | ); 37 | 38 | useEffect(() => { 39 | // unsubscribe the subscription on unmounting 40 | return () => { 41 | unSubscribe(); 42 | }; 43 | }, [unSubscribe]); 44 | 45 | return { 46 | setSubscription, 47 | unSubscribe, 48 | subscription: subscriptionRef.current, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/trigger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-scrolly/trigger", 3 | "version": "1.0.0", 4 | "description": "Components provided with the information that whether they are scrolled in/out of the viewport.", 5 | "author": "Mika Wang ", 6 | "homepage": "https://github.com/garfieldduck/intrasections#readme", 7 | "license": "MIT", 8 | "main": "dist/cjs/index.js", 9 | "module": "dist/esm/index.js", 10 | "types": "dist/esm/index.d.ts", 11 | "sideEffects": false, 12 | "directories": { 13 | "lib": "lib", 14 | "test": "__tests__" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "publishConfig": { 20 | "registry": "https://registry.npmjs.org", 21 | "access": "public" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/garfieldduck/intrasections.git" 26 | }, 27 | "scripts": { 28 | "dev": "yarn compile:esm --watch", 29 | "build": "run-p compile:esm compile:cjs", 30 | "clean": "rimraf -rf ./dist", 31 | "compile:esm": "tsc -p tsconfig.build.json", 32 | "compile:cjs": "tsc -p . -m commonjs --outDir dist/cjs" 33 | }, 34 | "peerDependencies": { 35 | "@react-scrolly/core": "^0.2.1-alpha.4", 36 | "prop-types": "^15.7.2", 37 | "react": "^16.13.1", 38 | "react-dom": "^16.13.1" 39 | }, 40 | "devDependencies": { 41 | "@react-scrolly/core": "^1.0.0" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/garfieldduck/intrasections/issues" 45 | }, 46 | "gitHead": "7671b4280d28c8a9fe4da2479497024a68d78c1d" 47 | } 48 | -------------------------------------------------------------------------------- /packages/plot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-scrolly/plot", 3 | "version": "1.0.0", 4 | "description": "Components which are able to track the scrolling progress of the section closest to the bottom of the viewport.", 5 | "homepage": "https://github.com/garfieldduck/intrasections#readme", 6 | "license": "MIT", 7 | "main": "dist/cjs/index.js", 8 | "module": "dist/esm/index.js", 9 | "types": "dist/esm/index.d.ts", 10 | "sideEffects": false, 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "publishConfig": { 19 | "registry": "https://registry.npmjs.org", 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/garfieldduck/intrasections.git" 25 | }, 26 | "scripts": { 27 | "dev": "yarn compile:esm --watch", 28 | "build": "run-p compile:esm compile:cjs", 29 | "clean": "rimraf -rf ./dist", 30 | "compile:esm": "tsc -p tsconfig.build.json", 31 | "compile:cjs": "tsc -p . -m commonjs --outDir dist/cjs" 32 | }, 33 | "peerDependencies": { 34 | "@react-scrolly/core": "^0.2.1-alpha.4", 35 | "observable-hooks": "^3.1.2", 36 | "prop-types": "^15.7.2", 37 | "react": "^16.13.1", 38 | "react-dom": "^16.13.1", 39 | "rxjs": "^6.6.2" 40 | }, 41 | "devDependencies": { 42 | "@react-scrolly/core": "^1.0.0" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/garfieldduck/intrasections/issues" 46 | }, 47 | "gitHead": "7671b4280d28c8a9fe4da2479497024a68d78c1d" 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/components/Section.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import { SectionInfo } from '../hooks/section/useScrolledRatio'; 4 | import { useSection } from '../hooks/section/useSection'; 5 | import { IntersectionObserverConfig } from '../types/IntersectionObserverConfig'; 6 | 7 | export interface SectionProps { 8 | /** 9 | * By setting an unique Section ID, you can know which section the user is currently viewing. 10 | * If `trackingId` is not null, 11 | * `
` will notify the `` to keep track of the sections in the viewport, 12 | * and determine which is closest to the bottom of the viewport. 13 | * Please make sure that on the same `scrollTop`, 14 | * there is **NO** more than one tracked section (section with `trackingId`). 15 | */ 16 | trackingId?: string; 17 | 18 | /** 19 | * The array of intersectionRatio thresholds which is used in the options of IntersectionObserver 20 | * @example [0, 0.25, 0.5, 0.75, 1] 21 | */ 22 | threshold?: IntersectionObserverConfig['threshold']; 23 | className?: string; 24 | style?: React.CSSProperties; 25 | children: (section: SectionInfo) => React.ReactNode; 26 | } 27 | 28 | export const Section = ({ 29 | className, 30 | style, 31 | children, 32 | trackingId, 33 | threshold, 34 | ...restProps 35 | }: SectionProps) => { 36 | const sectionRef = useRef(null); 37 | const sectionInfo = useSection(sectionRef, trackingId, threshold); 38 | 39 | return ( 40 |
41 | {children(sectionInfo)} 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/core/src/utils/getStickyPosition.ts: -------------------------------------------------------------------------------- 1 | import { SectionInfo } from '../hooks/section/useScrolledRatio'; 2 | 3 | const STICKY_POS: { 4 | absTop: React.CSSProperties; 5 | fixed: React.CSSProperties; 6 | absBottom: React.CSSProperties; 7 | } = { 8 | absTop: { 9 | position: 'absolute', 10 | top: 0, 11 | width: '100%', 12 | }, 13 | fixed: { 14 | position: 'fixed', 15 | top: 0, 16 | }, 17 | absBottom: { 18 | position: 'absolute', 19 | bottom: 0, 20 | width: '100%', 21 | }, 22 | }; 23 | 24 | /** 25 | * Returns the position of the inner div of the StickySection 26 | */ 27 | export function getStickyPosition(section: SectionInfo): React.CSSProperties { 28 | const { scrollInfo, sectionTop, boundingRect } = section; 29 | 30 | // TODO: think of a way to handle the case when sectionPosition cannot be successfully obtained when mounted 31 | if (boundingRect.width < 0) { 32 | return { position: 'relative' }; 33 | } 34 | 35 | const { scrollTop, scrollBottom } = scrollInfo; 36 | const sectionBottom = sectionTop + boundingRect.height; 37 | const stickyHeight = { 38 | height: `${section.scrollInfo.windowHeight || window.innerHeight}px`, 39 | }; 40 | 41 | if (scrollTop < sectionTop) { 42 | // appears on the top of the page 43 | return { ...stickyHeight, ...STICKY_POS.absTop }; 44 | } 45 | 46 | if (scrollTop >= sectionTop && sectionBottom > scrollBottom) { 47 | // sticks to the viewport 48 | return { 49 | ...stickyHeight, 50 | ...STICKY_POS.fixed, 51 | left: boundingRect.left, 52 | width: boundingRect.width, 53 | }; 54 | } 55 | 56 | // appears on the bottom of the page 57 | return { ...stickyHeight, ...STICKY_POS.absBottom }; 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/canary.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' # matches every branch that doesn't contain a '/' 7 | - '*/*' # matches every branch containing a single '/' 8 | - '**' # matches every branch 9 | - '!main' # excludes main 10 | - '!master' # excludes master 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 # Required to retrieve git history 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 12 24 | - name: Get yarn cache directory path 25 | id: yarn-cache-dir-path 26 | run: echo "::set-output name=dir::$(yarn cache dir)" 27 | - uses: actions/cache@v1 28 | id: yarn-cache 29 | with: 30 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 31 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-yarn- 34 | - name: Install packages 35 | run: | 36 | npm config set //npm.pkg.github.com/:_authToken=\${NPM_TOKEN} 37 | yarn install --frozen-lockfile --ignore-engines 38 | env: 39 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | - name: Build packages 41 | run: yarn clean && yarn build 42 | env: 43 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | - name: Publish as NPM packages 45 | run: | 46 | git stash 47 | npm config set //registry.npmjs.org/:_authToken=\${NPM_TOKEN} 48 | yarn release:canary 49 | env: 50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 52 | -------------------------------------------------------------------------------- /_docs/src/components/AnimatedTrail.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useTrail, animated } from 'react-spring'; 4 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 5 | 6 | import { defaultColors } from '../config/theme'; 7 | import mq from '../config/media-queries'; 8 | 9 | const Wrapper = styled.div` 10 | margin-top: 2.5rem; 11 | padding: 3rem 0 2rem 1rem; 12 | 13 | ${mq.greaterThan('mobile')` 14 | margin-top: 3.5rem; 15 | padding: 5rem 0 2rem 3rem; 16 | `} 17 | `; 18 | 19 | const Text = styled.span` 20 | display: block; 21 | color: ${({color = defaultColors.green}) => color}; 22 | font-size: 2.8em; 23 | font-weight: 800; 24 | text-transform: uppercase; 25 | line-height: 1.2; 26 | 27 | ${mq.greaterThan('mobile')` 28 | font-size: 3.5em; 29 | `} 30 | `; 31 | const AnimatedText = animated(Text); 32 | 33 | export const AnimatedTrail = ({ 34 | color, 35 | title, 36 | trackOnce, 37 | }) => { 38 | const items = title.split(' '); 39 | const containerRef = useRef(null); 40 | const isIntersecting = useIntersectingTrigger(containerRef, trackOnce); 41 | const trail = useTrail(items.length, { 42 | opacity: isIntersecting ? 1 : 0, 43 | x: isIntersecting ? 0 : 200, 44 | }); 45 | 46 | return ( 47 | 48 | {trail.map(({ opacity, x }, idx) => { 49 | const text = items[idx]; 50 | 51 | return ( 52 | `translate3d(0,${pos}%,0)`), 58 | }} 59 | > 60 | {text} 61 | 62 | ); 63 | })} 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /docs/src/components/AnimatedTrail.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useTrail, animated } from 'react-spring'; 4 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 5 | 6 | import { defaultColors } from '../config/theme'; 7 | import mq from '../config/media-queries'; 8 | 9 | const Wrapper = styled.div` 10 | margin-top: 2.5rem; 11 | padding: 3rem 0 2rem 1rem; 12 | 13 | ${mq.greaterThan('mobile')` 14 | margin-top: 3.5rem; 15 | padding: 5rem 0 2rem 3rem; 16 | `} 17 | `; 18 | 19 | const Text = styled.span` 20 | display: block; 21 | color: ${({color = defaultColors.green}) => color}; 22 | font-size: 2.8em; 23 | font-weight: 800; 24 | text-transform: uppercase; 25 | line-height: 1.2; 26 | 27 | ${mq.greaterThan('mobile')` 28 | font-size: 3.5em; 29 | `} 30 | `; 31 | const AnimatedText = animated(Text); 32 | 33 | export const AnimatedTrail = ({ 34 | color, 35 | title, 36 | trackOnce, 37 | }) => { 38 | const items = title.split(' '); 39 | const containerRef = useRef(null); 40 | const isIntersecting = useIntersectingTrigger(containerRef, trackOnce); 41 | const trail = useTrail(items.length, { 42 | opacity: isIntersecting ? 1 : 0, 43 | x: isIntersecting ? 0 : 200, 44 | }); 45 | 46 | return ( 47 | 48 | {trail.map(({ opacity, x }, idx) => { 49 | const text = items[idx]; 50 | 51 | return ( 52 | `translate3d(0,${pos}%,0)`), 58 | }} 59 | > 60 | {text} 61 | 62 | ); 63 | })} 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /packages/plot/src/hooks/useActiveSectionInfo.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { map, switchMap, merge, filter } from 'rxjs/operators'; 3 | import { useContext, useMemo } from 'react'; 4 | import { useObservableState } from 'observable-hooks'; 5 | import { 6 | // context 7 | PageContext, 8 | // types 9 | IntersectionInfo, 10 | ActiveSectionInfo, 11 | PageContextInterface, 12 | } from '@react-scrolly/core'; 13 | 14 | const getActiveSectionObsrFunc = ( 15 | isIntersectingObs$: Observable, 16 | activeSectionObs$: Observable 17 | ) => () => { 18 | /** 19 | * Observer to the changes in the scrolledRatio of the active section 20 | * when the section is in the viewport 21 | */ 22 | return ( 23 | // first emit true in order to take the active section info when Page is mounted 24 | of(true) 25 | // merge it with the intersection observer 26 | .pipe(merge(isIntersectingObs$)) 27 | // use `isIntersecting` to determine whether to take the active section info 28 | .pipe( 29 | switchMap((isIntersecting: boolean) => { 30 | return isIntersecting 31 | ? activeSectionObs$.pipe( 32 | map((activeSectionInfo) => { 33 | return activeSectionInfo; 34 | }) 35 | ) 36 | : of(undefined); 37 | }), 38 | filter((info) => typeof info !== 'undefined') 39 | ) 40 | ); 41 | }; 42 | 43 | export function useActiveSectionInfo(intersectObsr$: Observable) { 44 | const context = useContext(PageContext); 45 | const { activeSectionObs$ } = context!; 46 | 47 | const isIntersectingObs = useMemo( 48 | () => intersectObsr$.pipe(map(({ isIntersecting }) => isIntersecting)), 49 | [intersectObsr$] 50 | ); 51 | 52 | const activeSectionFunc = useMemo(() => { 53 | return getActiveSectionObsrFunc(isIntersectingObs, activeSectionObs$); 54 | }, [activeSectionObs$, isIntersectingObs]); 55 | 56 | const [activeSection] = useObservableState( 57 | activeSectionFunc, 58 | null 59 | ); 60 | 61 | return { 62 | activeSection, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/trigger/src/hooks/useIntersectingTrigger.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useCallback, useState } from 'react'; 2 | import { getIntersectionObserver, IntersectionObserverConfig } from '@react-scrolly/core'; 3 | 4 | export function useIntersectingTrigger( 5 | /** Ref which is binded to the container */ 6 | containerRef: React.RefObject, 7 | 8 | /** If true, the container will not be tracked again once it is visible in the viewport */ 9 | trackOnce = false, 10 | 11 | /** Margin and threshold configurations for IntersectionObserver */ 12 | intersectionConfig?: IntersectionObserverConfig 13 | ) { 14 | const [isIntersecting, setIsIntersecting] = useState(false); 15 | const preIntersecting = useRef(false); 16 | 17 | /** 18 | * Stores references to the observer listening to section intersection with the viewport 19 | */ 20 | const intersectionObserverRef = useRef(null); 21 | 22 | const disconnectIntersection = useCallback(() => { 23 | if (intersectionObserverRef.current) { 24 | intersectionObserverRef.current.disconnect(); 25 | } 26 | }, []); 27 | 28 | /** Use browser's IntersectionObserver to record whether the container is inside the viewport */ 29 | const recordIntersection = useCallback( 30 | (entries: IntersectionObserverEntry[]) => { 31 | const [entry] = entries; 32 | const { isIntersecting: curIntersecting } = entry; 33 | setIsIntersecting(curIntersecting); 34 | 35 | if (trackOnce && !preIntersecting.current && curIntersecting) { 36 | disconnectIntersection(); 37 | } 38 | 39 | preIntersecting.current = curIntersecting; 40 | }, 41 | [disconnectIntersection, trackOnce] 42 | ); 43 | 44 | useEffect(() => { 45 | // start observing whether the container is scrolled into the viewport 46 | intersectionObserverRef.current = getIntersectionObserver( 47 | recordIntersection, 48 | intersectionConfig 49 | ); 50 | 51 | intersectionObserverRef.current!.observe(containerRef.current!); 52 | 53 | // unsubscribe to the intersection observer on unmounting 54 | return () => { 55 | disconnectIntersection(); 56 | }; 57 | }, [containerRef, disconnectIntersection, intersectionConfig, recordIntersection]); 58 | 59 | return isIntersecting; 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scrolly", 3 | "version": "0.1.0", 4 | "workspaces": { 5 | "packages": [ 6 | "packages/*", 7 | "docs" 8 | ] 9 | }, 10 | "private": true, 11 | "scripts": { 12 | "bootstrap": "lerna bootstrap", 13 | "postinstall": "npm run bootstrap", 14 | "clean": "lerna run clean", 15 | "start": "yarn dev", 16 | "dev": "run-p dev:packages dev:docs", 17 | "dev:packages": "lerna run dev --parallel --stream", 18 | "dev:docs": "yarn workspace docs dev", 19 | "build": "lerna run build --stream", 20 | "_start": "npm run clean && npm run _dev", 21 | "_dev": "docz dev --port 5000", 22 | "_build:doc": "docz build", 23 | "lint": "eslint --ignore-path .gitignore \"./**/*.{ts,tsx}\"", 24 | "lint:fix": "yarn lint --fix", 25 | "bumpversion": "lerna version --no-push --no-git-tag-version", 26 | "lerna": "lerna", 27 | "release": "lerna publish from-package --yes --no-push --no-git-tag-version", 28 | "release:beta": "lerna publish --no-push --no-git-tag-version --preid=beta --npm-tag=prerelease", 29 | "release:canary": "lerna publish -y --canary --preid ci --npm-tag=ci --force-publish", 30 | "deploy": "DOCZ_BASE=/react-scrolly/ npm run build:doc && gh-pages -d .docz/dist" 31 | }, 32 | "devDependencies": { 33 | "@garfieldduck/eslint-config": "^0.0.3", 34 | "@garfieldduck/prettier-config": "^1.0.2", 35 | "@garfieldduck/typescript-config": "^1.0.3", 36 | "@types/node": "^14.0.23", 37 | "@types/react": "^16.9.43", 38 | "@types/styled-components": "5.1.1", 39 | "docz": "^1.3.2", 40 | "docz-theme-default": "^1.2.0", 41 | "eslint": "^6.7.2", 42 | "gh-pages": "^2.0.1", 43 | "lerna": "^3.15.0", 44 | "npm-run-all": "^4.1.5", 45 | "polish": "^0.2.3", 46 | "prettier": "^2.0.5", 47 | "prop-types": "^15.7.2", 48 | "react": "^16.13.1", 49 | "react-dom": "^16.13.1", 50 | "observable-hooks": "^3.1.2", 51 | "rxjs": "^6.6.2", 52 | "react-spring": "^8.0.18", 53 | "rimraf": "^3.0.2", 54 | "styled-components": "^5.1.1", 55 | "styled-media-query": "^2.1.2", 56 | "ts-loader": "^5.3.3", 57 | "typescript": "^3.9.7", 58 | "webpack": "^4.29.6", 59 | "webpack-cli": "^3.3.0" 60 | }, 61 | "publishConfig": { 62 | "registry": "https://registry.npmjs.org" 63 | }, 64 | "resolutions": { 65 | "ansi-styles": "^3.2.0" 66 | }, 67 | "eslintConfig": { 68 | "root": true, 69 | "extends": "./configs/.eslintrc.js" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /doczrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DEPRECATED: 3 | * docz is upgraded to v2 in ./docz 4 | */ 5 | import * as path from 'path'; 6 | import { css } from 'styled-components'; 7 | import { defaultColors } from './_docs/src/config/theme'; 8 | 9 | const modifyBundlerConfig = (config) => { 10 | config.resolve.alias = { 11 | ...config.resolve.alias, 12 | '@react-scrolly/core': path.resolve(__dirname, 'packages/core/src'), 13 | '@react-scrolly/scene': path.resolve(__dirname, 'packages/scene/src'), 14 | '@react-scrolly/plot': path.resolve(__dirname, 'packages/plot/src'), 15 | '@react-scrolly/trigger': path.resolve(__dirname, 'packages/trigger/src'), 16 | }; 17 | return config; 18 | }; 19 | 20 | export default { 21 | title: 'React-Scrolly', 22 | typescript: true, 23 | repository: 'https://github.com/garfieldduck/react-scrolly', 24 | public: '_docs/public', 25 | // set hashRouter as `true` for Github 26 | hashRouter: true, 27 | htmlContext: { 28 | favicon: 29 | 'https://user-images.githubusercontent.com/1139698/57021930-34341700-6c60-11e9-876f-62d613f02178.png', 30 | head: { 31 | links: [ 32 | { 33 | rel: 'stylesheet', 34 | href: 'https://codemirror.net/theme/oceanic-next.css', 35 | }, 36 | ], 37 | }, 38 | }, 39 | // order of the menu 40 | menu: ['Introduction', 'Scroll Tracking', 'Pinning Sections', 'Revealing Animations'], 41 | modifyBundlerConfig, 42 | themeConfig: { 43 | // See: https://github.com/pedronauck/docz/tree/master/core/docz-theme-default 44 | mode: 'light', 45 | codemirrorTheme: 'oceanic-next', 46 | showPlaygroundEditor: true, // always display the code in 47 | colors: { 48 | primary: defaultColors.primary, 49 | background: defaultColors.background, 50 | text: defaultColors.text, 51 | blue: defaultColors.blue, 52 | sidebarBg: defaultColors.background, 53 | sidebarBorder: '#a3a4a5', 54 | border: '#a3a4a5', 55 | codeBg: defaultColors.white, 56 | codeColor: defaultColors.primary, 57 | theadColor: '#79878e', 58 | }, 59 | logo: { 60 | src: 61 | 'https://user-images.githubusercontent.com/1139698/57021934-37c79e00-6c60-11e9-8451-2b0cf4016492.png', 62 | width: 200, 63 | }, 64 | styles: { 65 | body: css` 66 | font-family: 'Source Sans Pro', helvetica, 'PingFang TC', 'Noto Sans TC', 67 | 'Microsoft JhengHei', sans-serif; 68 | line-height: 1.6; 69 | img { 70 | max-width: 100%; 71 | } 72 | `, 73 | playground: css` 74 | background: #ffffff; 75 | `, 76 | }, 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Get Started 3 | route: / 4 | menu: Introduction 5 | --- 6 | 7 | # Introduction 8 | 9 | ## Scrolly-telling made easy 10 | 11 | Magical scroll-based interactions made easy with `react-scrolly`. 12 | 13 | Scroll-based interactions create an incredible experience by letting users explore the story with simple scrolls, 14 | such highlighting a portion of the content, 15 | or animating the route on a map based on the position of the section user is reading. 16 | 17 | However, tracking the scrolling progress by yourself is burdensome, 18 | and binding the window scroll tracking for each component is prone to cause performance issues as the number of tracked components increase, 19 | and thus making users see the screen juddering when scrolling. 20 | 21 | With this in mind, `react-scrolly` is created to allow you to track the progress of scrolling with minimum efforts and the performance impact. 22 | 23 | `react-scrolly` is perfect for the following use cases: 24 | 25 | - Track the scrolled ratio (ratio of a section being read) by the user. 26 | - Track the section the user is currently reading (closest to the bottom of the viewport) and its scrolled ratio in another component. 27 | - Pin components on the scroll based on the scroll position. 28 | - Making scrolled-based animations or parallax effects (by combining the scrolled ratio provided `react-scrolly` with animation libraries such as [react-spring](https://github.com/react-spring/react-spring), 29 | you are able to make stunning scroll-based visual effects with concise and declarative code). 30 | 31 | ### Definition of the scrolled ratio 32 | The `scrolled ratio` is defined by the ratio of a component being scrolled over **the bottom of the screen** (viewport). 33 | 34 | ![Scrolled ratio](https://user-images.githubusercontent.com/1139698/57021937-3a29f800-6c60-11e9-89d8-51959a7ca60e.png) 35 | 36 | 37 | ## Why is it performant? 38 | 39 | In contrast to the traditional scroll tracking by binding window scroll event listeners to components and calling `getBoundingClientRect()` on scroll 40 | which potentially causes many unnecessary re-renderings and [reflows](https://gist.github.com/paulirish/5d52fb081b3570c81e3a), 41 | `react-scrolly` only notifies the scrolling position changes to the components currently intersected with the viewport, 42 | which is made possible by utilizing the `IntersectionObserver`, `RxJS`, the context API, and React hooks. 43 | 44 | ## How to design scrolly-telling 45 | 46 | Here are some references to help you design better scrolly-telling: 47 | 48 | - How To Scroll by Mike Bostock: https://bost.ocks.org/mike/scroll/ 49 | - Responsive scrollytelling best practices (The Pudding): https://pudding.cool/process/responsive-scrollytelling/ 50 | -------------------------------------------------------------------------------- /_docs/src/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Get Started 3 | route: / 4 | menu: Introduction 5 | --- 6 | 7 | # Introduction 8 | 9 | ## Scrolly-telling made easy 10 | 11 | Magical scroll-based interactions made easy with `react-scrolly`. 12 | 13 | Scroll-based interactions create an incredible experience by letting users explore the story with simple scrolls, 14 | such highlighting a portion of the content, 15 | or animating the route on a map based on the position of the section user is reading. 16 | 17 | However, tracking the scrolling progress by yourself is burdensome, 18 | and binding the window scroll tracking for each component is prone to cause performance issues as the number of tracked components increase, 19 | and thus making users see the screen juddering when scrolling. 20 | 21 | With this in mind, `react-scrolly` is created to allow you to track the progress of scrolling with minimum efforts and the performance impact. 22 | 23 | `react-scrolly` is perfect for the following use cases: 24 | 25 | - Track the scrolled ratio (ratio of a section being read) by the user. 26 | - Track the section the user is currently reading (closest to the bottom of the viewport) and its scrolled ratio in another component. 27 | - Pin components on the scroll based on the scroll position. 28 | - Making scrolled-based animations or parallax effects (by combining the scrolled ratio provided `react-scrolly` with animation libraries such as [react-spring](https://github.com/react-spring/react-spring), 29 | you are able to make stunning scroll-based visual effects with concise and declarative code). 30 | 31 | ### Definition of the scrolled ratio 32 | The `scrolled ratio` is defined by the ratio of a component being scrolled over **the bottom of the screen** (viewport). 33 | 34 | ![Scrolled ratio](https://user-images.githubusercontent.com/1139698/57021937-3a29f800-6c60-11e9-89d8-51959a7ca60e.png) 35 | 36 | 37 | ## Why is it performant? 38 | 39 | In contrast to the traditional scroll tracking by binding window scroll event listeners to components and calling `getBoundingClientRect()` on scroll 40 | which potentially causes many unnecessary re-renderings and [reflows](https://gist.github.com/paulirish/5d52fb081b3570c81e3a), 41 | `react-scrolly` only notifies the scrolling position changes to the components currently intersected with the viewport, 42 | which is made possible by utilizing the `IntersectionObserver`, `RxJS`, the context API, and React hooks. 43 | 44 | ## How to design scrolly-telling 45 | 46 | Here are some references to help you design better scrolly-telling: 47 | 48 | - How To Scroll by Mike Bostock: https://bost.ocks.org/mike/scroll/ 49 | - Responsive scrollytelling best practices (The Pudding): https://pudding.cool/process/responsive-scrollytelling/ 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![react-scrolly](https://user-images.githubusercontent.com/1139698/56862995-5cfba880-69e3-11e9-85ec-3a051659a324.jpg) 2 | 3 | # Scrolly-telling made easy 4 | 5 | Magical scroll-based interactions made easy with `react-scrolly`. 6 | 7 | Scroll-based interactions create an incredible experience by letting users explore the story with simple scrolls, 8 | such highlighting a portion of the content, 9 | or animating the route on a map based on the position of the section user is reading. 10 | 11 | However, tracking the scrolling progress by yourself is burdensome, 12 | and binding the window scroll tracking for each component is prone to cause performance issues as the number of tracked components increase, 13 | and thus making users see the screen juddering when scrolling. 14 | 15 | With this in mind, `react-scrolly` is created to allow you to track the progress of scrolling with minimum efforts and the performance impact. 16 | 17 | `react-scrolly` is perfect for the following use cases: 18 | 19 | - Track the scrolled ratio (ratio of a section being read) by the user. 20 | - Track the section the user is currently reading (closest to the bottom of the viewport) and its scrolled ratio in another component. 21 | - Pin components on the scroll based on the scroll position. 22 | - Making scrolled-based animations or parallax effects (by combining the scrolled ratio provided `react-scrolly` with animation libraries such as [react-spring](https://github.com/react-spring/react-spring), 23 | you are able to make stunning scroll-based visual effects with concise and declarative code). 24 | 25 | ### Definition of the scrolled ratio 26 | The `scrolled ratio` is defined by the ratio of a component being scrolled over **the bottom of the screen** (viewport). 27 | 28 | ![Scrolled ratio](https://user-images.githubusercontent.com/1139698/57021937-3a29f800-6c60-11e9-89d8-51959a7ca60e.png) 29 | 30 | 31 | ## Why is it performant? 32 | 33 | In contrast to the traditional scroll tracking by binding window scroll event listeners to components and calling `getBoundingClientRect()` on scroll 34 | which potentially causes many unnecessary re-renderings and [reflows](https://gist.github.com/paulirish/5d52fb081b3570c81e3a), 35 | `react-scrolly` only notifies the scrolling position changes to the components currently intersected with the viewport, 36 | which is made possible by utilizing the `IntersectionObserver`, `RxJS`, the context API, and React hooks. 37 | 38 | ## How to design scrolly-telling 39 | 40 | Here are some references to help you design better scrolly-telling: 41 | 42 | - How To Scroll by Mike Bostock: https://bost.ocks.org/mike/scroll/ 43 | - Responsive scrollytelling best practices (The Pudding): https://pudding.cool/process/responsive-scrollytelling/ 44 | 45 | -------------------------------------------------------------------------------- /packages/core/src/hooks/common/useIntersectionObservable.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useCallback } from 'react'; 2 | import { Observable, Subject } from 'rxjs'; 3 | 4 | import { getIntersectionObserver } from '../../utils/getIntersectionObserver'; 5 | import { IntersectionObserverConfig } from '../../types/IntersectionObserverConfig'; 6 | 7 | export interface IntersectionInfo { 8 | /** From IntersectionObserver: whether the `
` is intersecting the root */ 9 | isIntersecting: boolean; 10 | 11 | /** Tracking ID of the section */ 12 | trackingId?: string; 13 | 14 | /** The bounding rectangle of `
` */ 15 | sectionBoundingRect: ClientRect; 16 | } 17 | 18 | export function useIntersectionObservable( 19 | /** Ref which is binded to the section */ 20 | sectionRef: React.RefObject, 21 | 22 | /** Provide the ID if the section is going to be tracked on the page */ 23 | trackingId: IntersectionInfo['trackingId'], 24 | 25 | /** Margin and threshold configurations for IntersectionObserver */ 26 | intersectionConfig?: IntersectionObserverConfig 27 | ): Observable { 28 | /** 29 | * Stores references to the observer listening to section intersection with the viewport 30 | */ 31 | const intersectionObserverRef = useRef(null); 32 | 33 | // transform the intersectionObserver as a RX Observable 34 | const intersectSubjectRef = useRef(new Subject()); 35 | const intersectObservableRef = useRef(intersectSubjectRef.current.asObservable()); 36 | 37 | /** Use browser's IntersectionObserver to record whether the section is inside the viewport */ 38 | const recordIntersection = useCallback( 39 | (entries: IntersectionObserverEntry[]) => { 40 | const [entry] = entries; 41 | const { isIntersecting, boundingClientRect } = entry; 42 | const intersecting: IntersectionInfo = { 43 | isIntersecting, 44 | trackingId, 45 | sectionBoundingRect: boundingClientRect, 46 | }; 47 | 48 | intersectSubjectRef.current.next(intersecting); 49 | }, 50 | [trackingId] 51 | ); 52 | 53 | useEffect(() => { 54 | // check if it's not on SSR 55 | if (window && IntersectionObserver) { 56 | // start observing whether the section is scrolled into the viewport 57 | intersectionObserverRef.current = getIntersectionObserver( 58 | recordIntersection, 59 | intersectionConfig 60 | ); 61 | 62 | intersectionObserverRef.current.observe(sectionRef.current!); 63 | } 64 | 65 | const intersectSubject = intersectSubjectRef.current; 66 | 67 | // unsubscribe to the intersection observer on unmounting 68 | return () => { 69 | if (intersectionObserverRef.current) { 70 | intersectionObserverRef.current.disconnect(); 71 | intersectSubject.complete(); 72 | } 73 | }; 74 | }, [intersectionConfig, recordIntersection, sectionRef]); 75 | 76 | return intersectObservableRef.current; 77 | } 78 | -------------------------------------------------------------------------------- /_docs/src/PinningSections/StickyScene.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 1. StickyScene 3 | route: /pinning_sections/sticky_scene 4 | menu: Pinning Sections 5 | --- 6 | 7 | import { Playground } from 'docz' 8 | import { PageProvider } from '@react-scrolly/core' 9 | 10 | import { DemoWrapper } from '../components/DemoWrapper' 11 | import { CenterBox } from '../components/CenterBox'; 12 | import { DescriptionBox } from '../components/DescriptionBox'; 13 | import { BorderedStickyScene as StickyScene } from '../components/DemoStickyScene' 14 | import { defaultColors } from '../config/theme'; 15 | 16 | 17 | # StickyScene 18 | 19 | `` is another kind of [Section](/scroll_tracking/section) which sticks its main content on the background when the top of the section reaches the top of the viewport, 20 | and it lets the supplementary content scroll through its sticky background. 21 | 22 | ## Example 23 | 24 | 25 | 29 |
30 | 31 |

Description #1

32 |

Supplementary content #1

33 |
34 |
35 |
36 | 37 |

Description #2

38 |

Supplementary content #2

39 |
40 |
41 |
42 | 43 |

Description #3

44 |

Supplementary content #3

45 |
46 |
47 | 48 | } 49 | > 50 | {({ isIntersecting, scrolledRatio }) => ( 51 | 52 |

53 | {' background'} 54 |

55 |
  • isIntersecting: {`${isIntersecting}`}
  • 56 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 57 |
    58 | )} 59 |
    60 |
    61 |
    62 | 63 | ## Usage 64 | 65 | 66 | Import ``: 67 | ```jsx 68 | import { PageProvider } from '@react-scrolly/core'; 69 | import { StickyScene } from '@react-scrolly/plot' 70 | ``` 71 | 72 | Use it in your component: 73 | 74 | `` takes two components from the following props to render: 75 | 76 | - **children**: 77 | Background content; it sticks to the viewport while the user is scrolling through the foreground content. 78 | 79 | Since `` is just a `
    ` that helps you achieve the sticky scrolling effect with ease, it provides the same information like `scrolledRatio` as [Section](/scroll_tracking/section#properties). 80 | - **renderOverlay**: Foreground content; normally acts as the descriptions for the background content. 81 | Its rendered content accounts for the real height of ``, i.e., `scrolledRatio` given in the background is calculated by the reading progress of the foreground content. 82 | -------------------------------------------------------------------------------- /docs/src/PinningSections/StickyScene.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 1. StickyScene 3 | route: /pinning_sections/sticky_scene 4 | menu: Pinning Sections 5 | --- 6 | 7 | import { Playground } from 'docz' 8 | import { PageProvider } from '@react-scrolly/core' 9 | 10 | import { DemoWrapper } from '../components/DemoWrapper' 11 | import { CenterBox } from '../components/CenterBox'; 12 | import { DescriptionBox } from '../components/DescriptionBox'; 13 | import { BorderedStickyScene as StickyScene } from '../components/DemoStickyScene' 14 | import { defaultColors } from '../config/theme'; 15 | 16 | 17 | # StickyScene 18 | 19 | `` is another kind of [Section](/scroll_tracking/section) which sticks its main content on the background when the top of the section reaches the top of the viewport, 20 | and it lets the supplementary content scroll through its sticky background. 21 | 22 | ## Example 23 | 24 | 25 | 29 |
    30 | 31 |

    Description #1

    32 |

    Supplementary content #1

    33 |
    34 |
    35 |
    36 | 37 |

    Description #2

    38 |

    Supplementary content #2

    39 |
    40 |
    41 |
    42 | 43 |

    Description #3

    44 |

    Supplementary content #3

    45 |
    46 |
    47 | 48 | } 49 | > 50 | {({ isIntersecting, scrolledRatio }) => ( 51 | 52 |

    53 | {' background'} 54 |

    55 |
  • isIntersecting: {`${isIntersecting}`}
  • 56 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 57 |
    58 | )} 59 |
    60 |
    61 |
    62 | 63 | ## Usage 64 | 65 | 66 | Import ``: 67 | ```jsx 68 | import { PageProvider } from '@react-scrolly/core'; 69 | import { StickyScene } from '@react-scrolly/plot' 70 | ``` 71 | 72 | Use it in your component: 73 | 74 | `` takes two components from the following props to render: 75 | 76 | - **children**: 77 | Background content; it sticks to the viewport while the user is scrolling through the foreground content. 78 | 79 | Since `` is just a `
    ` that helps you achieve the sticky scrolling effect with ease, it provides the same information like `scrolledRatio` as [Section](/scroll_tracking/section#properties). 80 | - **renderOverlay**: Foreground content; normally acts as the descriptions for the background content. 81 | Its rendered content accounts for the real height of ``, i.e., `scrolledRatio` given in the background is calculated by the reading progress of the foreground content. 82 | -------------------------------------------------------------------------------- /packages/core/src/hooks/common/usePageScroll.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of, zip } from 'rxjs'; 2 | import { map, switchMap, merge, take, filter } from 'rxjs/operators'; 3 | import { useCallback, useContext, useMemo } from 'react'; 4 | import { useObservableState } from 'observable-hooks'; 5 | 6 | import { PageContext, PageContextInterface } from '../../context/PageContext'; 7 | import { IntersectionInfo } from './useIntersectionObservable'; 8 | import { ScrollPosition } from '../../types/ScrollPosition'; 9 | 10 | export function usePageScroll(intersectObsr$: Observable) { 11 | const context = useContext(PageContext); 12 | const { scrollObs$ } = context!; 13 | 14 | const isIntersectingObs = useMemo( 15 | () => intersectObsr$.pipe(map(({ isIntersecting }) => isIntersecting)), 16 | [intersectObsr$] 17 | ); 18 | const isInterSectingFunc = useCallback(() => isIntersectingObs, [isIntersectingObs]); 19 | const [intersecting] = useObservableState(isInterSectingFunc, false); 20 | 21 | /** 22 | * Observer to track the scroll position 23 | * emitted when Page is mounted 24 | */ 25 | const mountScrollObs = useMemo( 26 | () => 27 | zip(scrollObs$, isIntersectingObs).pipe( 28 | map(([scrollPos, isIntersecting]): { 29 | isIntersecting: boolean; 30 | scrollPos: ScrollPosition; 31 | } => ({ 32 | isIntersecting, 33 | scrollPos: { 34 | ...scrollPos, 35 | scrollOffset: 0, 36 | }, 37 | })), 38 | take(1) 39 | ), 40 | [isIntersectingObs, scrollObs$] 41 | ); 42 | /** 43 | * Observer to track the scroll position 44 | * when real scrolling events are triggered 45 | */ 46 | const windowScrollObs = useMemo( 47 | () => 48 | isIntersectingObs.pipe( 49 | switchMap((isIntersecting: boolean) => { 50 | // use `isIntersecting` to determine whether to take the scrolling info 51 | return isIntersecting 52 | ? scrollObs$.pipe( 53 | map((scrollPos: ScrollPosition) => ({ 54 | isIntersecting, 55 | scrollPos, 56 | })) 57 | ) 58 | : // when the section is scrolled out of the viewport, update its dimension 59 | of({ 60 | isIntersecting, 61 | scrollPos: null, 62 | }); 63 | }) 64 | ), 65 | [isIntersectingObs, scrollObs$] 66 | ); 67 | 68 | /** Observer to track the page scrolling by combining mountScrollObs and windowScrollObs */ 69 | const pageScrollObsrFunc = useCallback( 70 | () => 71 | mountScrollObs.pipe( 72 | merge(windowScrollObs), 73 | filter(({ isIntersecting, scrollPos }) => isIntersecting && scrollPos !== null), 74 | map(({ scrollPos }) => { 75 | return scrollPos!; 76 | }) 77 | ), 78 | [mountScrollObs, windowScrollObs] 79 | ); 80 | 81 | const [scrollInfo] = useObservableState(pageScrollObsrFunc, { 82 | scrollTop: 0, 83 | scrollBottom: 0, 84 | windowHeight: 0, 85 | scrollOffset: 0, 86 | }); 87 | 88 | return { 89 | scrollInfo, 90 | isIntersecting: intersecting, 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /packages/core/src/hooks/section/useScrolledRatio.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { useRef, useMemo, useEffect, useContext } from 'react'; 3 | 4 | import { PageContext, PageContextInterface } from '../../context/PageContext'; 5 | import { useSectionPosition, SectionPosition } from '../common/useSectionPosition'; 6 | import { usePageScroll } from '../common/usePageScroll'; 7 | import { IntersectionInfo } from '../common/useIntersectionObservable'; 8 | import { ScrollPosition } from '../../types/ScrollPosition'; 9 | 10 | export interface SectionInfo extends SectionPosition { 11 | /** Whether the section is intersecting with the viewport */ 12 | isIntersecting: boolean; 13 | 14 | /** Information related to the window scrolling and the ratio of the section being scrolled */ 15 | scrollInfo: ScrollPosition; 16 | 17 | /** Ratio of the section being scrolled */ 18 | scrolledRatio: number; 19 | } 20 | 21 | export function useScrolledRatio( 22 | /** Ref of the section being tracked */ 23 | sectionRef: React.RefObject, 24 | 25 | intersectObsr$: Observable, 26 | 27 | /** 28 | * By setting an unique Section ID, you can know which section the user is currently viewing. 29 | * If `trackingId` is not null, 30 | * it will trigger the update of the active section infomation managed in ``. 31 | * Please make sure that on the same `scrollTop`, 32 | * there is **NO** more than one tracked section (section with `trackingId`). 33 | */ 34 | trackingId?: string 35 | ): SectionInfo { 36 | const context = useContext(PageContext); 37 | const { addActiveSection, removeActiveSection, updateScrollRatio } = context!; 38 | 39 | const { sectionTop, boundingRect } = useSectionPosition(sectionRef, intersectObsr$); 40 | 41 | const { isIntersecting, scrollInfo } = usePageScroll(intersectObsr$); 42 | 43 | const preIntersecting = useRef(false); 44 | 45 | // update the active section info if `isIntersecting` changes 46 | useEffect(() => { 47 | const curInter = isIntersecting; 48 | const preInter = preIntersecting.current; 49 | 50 | if (trackingId) { 51 | if (!preInter && curInter) { 52 | // update the section currently being scrolled 53 | addActiveSection(trackingId, sectionTop, scrollInfo.scrollBottom); 54 | } else if (preInter && !curInter) { 55 | // clear the section ID tracked in the page 56 | removeActiveSection(trackingId, scrollInfo.scrollBottom); 57 | } 58 | } 59 | 60 | preIntersecting.current = curInter; 61 | }, [trackingId, isIntersecting, sectionTop, scrollInfo, addActiveSection, removeActiveSection]); 62 | 63 | const scrolledRatio = useMemo(() => { 64 | const { scrollBottom } = scrollInfo; 65 | const { height } = boundingRect; 66 | 67 | const distance = scrollBottom - sectionTop; 68 | let ratio = distance / height; 69 | 70 | if (ratio >= 1) { 71 | ratio = 1; 72 | } else if (ratio <= 0) { 73 | ratio = 0; 74 | } 75 | 76 | return ratio; 77 | }, [scrollInfo, boundingRect, sectionTop]); 78 | 79 | useEffect(() => { 80 | // if the section is tracked, 81 | // let `useActiveSectionTracker()`to determine whether it is active, 82 | // and if it is active, the scrolled ratio which it keeps track of will be updated 83 | if (trackingId) { 84 | updateScrollRatio(trackingId, scrolledRatio); 85 | } 86 | }, [scrolledRatio, trackingId, updateScrollRatio]); 87 | 88 | return { 89 | isIntersecting, 90 | scrolledRatio, 91 | sectionTop, 92 | scrollInfo, 93 | boundingRect, 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | # Ref: https://github.com/azu/lerna-monorepo-github-actions-release/blob/master/.github/workflows/publish.yml 3 | 4 | on: 5 | push: 6 | branches: [main, master] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 # Required to retrieve git history 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - name: Get yarn cache directory path 21 | id: yarn-cache-dir-path 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | - uses: actions/cache@v1 24 | id: yarn-cache 25 | with: 26 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-yarn- 30 | - name: Install packages 31 | run: | 32 | npm config set //npm.pkg.github.com/:_authToken=\${NPM_TOKEN} 33 | yarn install --frozen-lockfile --ignore-engines 34 | env: 35 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Build packages 37 | run: yarn clean && yarn build 38 | env: 39 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | # Define ${CURRENT_VERSION} 41 | - name: Set Current Version 42 | shell: bash -ex {0} 43 | run: | 44 | CURRENT_VERSION=$(node -p 'require("./lerna.json").version') 45 | echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV 46 | - name: Tag Check 47 | id: tag_check 48 | shell: bash -ex {0} 49 | run: | 50 | GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/v${CURRENT_VERSION}" 51 | http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s \ 52 | -H "Authorization: token ${GITHUB_TOKEN}") 53 | if [ "$http_status_code" -ne "404" ] ; then 54 | echo "::set-output name=exists_tag::true" 55 | else 56 | echo "::set-output name=exists_tag::false" 57 | fi 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | - name: Create Git Tag 61 | if: steps.tag_check.outputs.exists_tag == 'false' 62 | uses: azu/action-package-version-to-git-tag@v1 63 | with: 64 | version: ${{ env.CURRENT_VERSION }} 65 | github_token: ${{ secrets.GITHUB_TOKEN }} 66 | github_repo: ${{ github.repository }} 67 | git_commit_sha: ${{ github.sha }} 68 | git_tag_prefix: 'v' 69 | - name: Create Release 70 | id: create_release 71 | if: steps.tag_check.outputs.exists_tag == 'false' && github.event.pull_request.merged == true 72 | uses: actions/create-release@v1 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | with: 76 | tag_name: v${{ env.CURRENT_VERSION }} 77 | # Copy Pull Request's tile and body to Release Note 78 | release_name: ${{ github.event.pull_request.title }} 79 | body: | 80 | ${{ github.event.pull_request.body }} 81 | draft: false 82 | prerelease: false 83 | - name: Publish as NPM packages 84 | run: | 85 | git stash 86 | npm config set //registry.npmjs.org/:_authToken=\${NPM_TOKEN} 87 | yarn release 88 | env: 89 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 90 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 91 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | -------------------------------------------------------------------------------- /_docs/src/PinningSections/StickyPlot.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 2. StickyPlot 3 | route: /pinning_sections/sticky_plot 4 | menu: Pinning Sections 5 | --- 6 | 7 | import { Playground } from 'docz'; 8 | import { PageProvider, Section } from '@react-scrolly/core'; 9 | 10 | import { DemoWrapper } from '../components/DemoWrapper'; 11 | import { CenterBox } from '../components/CenterBox'; 12 | import { DescriptionBox } from '../components/DescriptionBox'; 13 | import { BorderedStickyPlot as StickyPlot } from '../components/DemoStickyPlot'; 14 | import { defaultColors } from '../config/theme'; 15 | 16 | 17 | # StickyPlot 18 | 19 | `` is another kind of [Plot](/scroll_tracking/plot) which sticks its main content on the background when the top of the plot reaches the top of the viewport, 20 | and it lets the supplementary content scroll through its sticky background. 21 | 22 | Its usage is close to [StickyScene](/pinning_sections/sticky_scene), 23 | and it provides additional function by letting you track the scrolling progress of `
    ` closet to the bottom of the viewport with `trackingId`. 24 | 25 | ## Example 26 | 27 | 28 | 32 |
    33 | {() => ( 34 | 35 |

    Description #1

    36 |

    Supplementary content #1

    37 |
    38 | )} 39 |
    40 |
    41 | {() => ( 42 | 43 |

    Description #2

    44 |

    Supplementary content #2

    45 |
    46 | )} 47 |
    48 |
    49 | {() => ( 50 | 51 |

    Description #3

    52 |

    Supplementary content #3

    53 |
    54 | )} 55 |
    56 | 57 | } 58 | > 59 | {({ activeSection, isIntersecting, scrolledRatio }) => ( 60 | activeSection && ( 61 | 62 |

    Description box you are viewing:

    63 |
  • You are viewing {activeSection.id} section
  • 64 |
  • Reading Ratio: {activeSection.ratio.toFixed(4)}
  • 65 |

    Whole {''}:

    66 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 67 |
    68 | ))} 69 |
    70 |
    71 |
    72 | 73 | ## Usage 74 | 75 | 76 | Import ``: 77 | ```jsx 78 | import { PageProvider } from '@react-scrolly/core'; 79 | import { StickyPlot } from '@react-scrolly/plot'; 80 | ``` 81 | 82 | Use it in your component: 83 | 84 | `` takes two components from the following props to render: 85 | 86 | - **children**: 87 | Background content; it sticks to the viewport while the user is scrolling through the foreground content. 88 | 89 | Since `` is just a `` that helps you achieve the sticky scrolling effect with ease, 90 | it provides the same information: `activeSection`, `isIntersecting`, and `scrolledRatio` as [Plot](/scroll_tracking/plot#properties) provides. 91 | 92 | - **renderOverlay**: Foreground content; you can put sections with `trackedId` here. 93 | Its rendered content accounts for the real height of ``, i.e., `scrolledRatio` given in the background is calculated by the reading progress of the foreground content. 94 | -------------------------------------------------------------------------------- /docs/src/PinningSections/StickyPlot.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 2. StickyPlot 3 | route: /pinning_sections/sticky_plot 4 | menu: Pinning Sections 5 | --- 6 | 7 | import { Playground } from 'docz'; 8 | import { PageProvider, Section } from '@react-scrolly/core'; 9 | 10 | import { DemoWrapper } from '../components/DemoWrapper'; 11 | import { CenterBox } from '../components/CenterBox'; 12 | import { DescriptionBox } from '../components/DescriptionBox'; 13 | import { BorderedStickyPlot as StickyPlot } from '../components/DemoStickyPlot'; 14 | import { defaultColors } from '../config/theme'; 15 | 16 | 17 | # StickyPlot 18 | 19 | `` is another kind of [Plot](/scroll_tracking/plot) which sticks its main content on the background when the top of the plot reaches the top of the viewport, 20 | and it lets the supplementary content scroll through its sticky background. 21 | 22 | Its usage is close to [StickyScene](/pinning_sections/sticky_scene), 23 | and it provides additional function by letting you track the scrolling progress of `
    ` closet to the bottom of the viewport with `trackingId`. 24 | 25 | ## Example 26 | 27 | 28 | 32 |
    33 | {() => ( 34 | 35 |

    Description #1

    36 |

    Supplementary content #1

    37 |
    38 | )} 39 |
    40 |
    41 | {() => ( 42 | 43 |

    Description #2

    44 |

    Supplementary content #2

    45 |
    46 | )} 47 |
    48 |
    49 | {() => ( 50 | 51 |

    Description #3

    52 |

    Supplementary content #3

    53 |
    54 | )} 55 |
    56 | 57 | } 58 | > 59 | {({ activeSection, isIntersecting, scrolledRatio }) => ( 60 | activeSection && ( 61 | 62 |

    Description box you are viewing:

    63 |
  • You are viewing {activeSection.id} section
  • 64 |
  • Reading Ratio: {activeSection.ratio.toFixed(4)}
  • 65 |

    Whole {''}:

    66 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 67 |
    68 | ))} 69 |
    70 |
    71 |
    72 | 73 | ## Usage 74 | 75 | 76 | Import ``: 77 | ```jsx 78 | import { PageProvider } from '@react-scrolly/core'; 79 | import { StickyPlot } from '@react-scrolly/plot'; 80 | ``` 81 | 82 | Use it in your component: 83 | 84 | `` takes two components from the following props to render: 85 | 86 | - **children**: 87 | Background content; it sticks to the viewport while the user is scrolling through the foreground content. 88 | 89 | Since `` is just a `` that helps you achieve the sticky scrolling effect with ease, 90 | it provides the same information: `activeSection`, `isIntersecting`, and `scrolledRatio` as [Plot](/scroll_tracking/plot#properties) provides. 91 | 92 | - **renderOverlay**: Foreground content; you can put sections with `trackedId` here. 93 | Its rendered content accounts for the real height of ``, i.e., `scrolledRatio` given in the background is calculated by the reading progress of the foreground content. 94 | -------------------------------------------------------------------------------- /packages/core/src/components/PageProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useRef, useLayoutEffect } from 'react'; 2 | import { Subject, fromEvent, animationFrameScheduler, of } from 'rxjs'; 3 | import { debounceTime, map, pairwise, merge } from 'rxjs/operators'; 4 | 5 | import { PageContext, PageContextInterface } from '../context/PageContext'; 6 | import { getScrollPosition } from '../utils/getScrollPosition'; 7 | import { useActiveSectionTracker } from '../hooks/page/useActiveSectionTracker'; 8 | import { ScrollPosition } from '../types/ScrollPosition'; 9 | 10 | export interface PageProps { 11 | children: React.ReactNode; 12 | 13 | /** 14 | * Allows the window resizing event to go through again after the `resizeThrottleTime` 15 | */ 16 | resizeThrottleTime?: number; 17 | } 18 | 19 | export const PageProvider: FunctionComponent = ({ 20 | children, 21 | resizeThrottleTime = 300, 22 | }) => { 23 | const { Provider } = PageContext; 24 | 25 | const { 26 | updateScrollRatio, 27 | addActiveSection, 28 | removeActiveSection, 29 | activeSectionObs$, 30 | } = useActiveSectionTracker(); 31 | 32 | /** 33 | * Subject to be combined with `scrollSubjectRef` 34 | * in order to listen to the initial window scroll position on mounted 35 | */ 36 | const scrollSubjectRef = useRef(new Subject()); 37 | /** 38 | * Observer to listen to page scroll 39 | */ 40 | const scrollObserverRef = useRef( 41 | // detect window object to prevent issues on SSR 42 | typeof window !== 'undefined' 43 | ? scrollSubjectRef.current.asObservable().pipe( 44 | merge( 45 | fromEvent(window, 'scroll').pipe( 46 | // throttled by the animation frame 47 | debounceTime(0, animationFrameScheduler), 48 | map(() => getScrollPosition()), 49 | // use pairwise to group pairs of consecutive emissions 50 | // so that we can calculate `scrollOffset` 51 | pairwise(), 52 | map( 53 | ([previousScroll, currentScroll]): ScrollPosition => { 54 | // amount of pixels scrolled by 55 | // - postive: scroll down 56 | // - negative: scroll up 57 | const scrollOffset = currentScroll.scrollTop - previousScroll.scrollTop; 58 | 59 | return { 60 | ...currentScroll, 61 | scrollOffset, 62 | }; 63 | } 64 | ) 65 | ) 66 | ) 67 | ) 68 | : of({ 69 | scrollTop: 0, 70 | scrollBottom: 0, 71 | windowHeight: 10, 72 | scrollOffset: 0, 73 | }) 74 | ); 75 | 76 | /** 77 | * Observer to listen to window resize 78 | */ 79 | const resizeObserverRef = useRef( 80 | // detect window object to prevent issues on SSR 81 | typeof window !== 'undefined' 82 | ? fromEvent(window, 'resize').pipe(debounceTime(resizeThrottleTime)) 83 | : undefined 84 | ); 85 | 86 | const context: PageContextInterface = { 87 | addActiveSection, 88 | removeActiveSection, 89 | updateScrollRatio, 90 | activeSectionObs$, 91 | scrollObs$: scrollObserverRef.current, 92 | resizeObs$: resizeObserverRef.current, 93 | }; 94 | 95 | useLayoutEffect(() => { 96 | const initialScroll = { 97 | ...getScrollPosition(), 98 | scrollOffset: 0, 99 | }; 100 | 101 | const scrollSubject = scrollSubjectRef.current; 102 | // send the initial window scrolling position on mounted 103 | scrollSubject.next(initialScroll); 104 | 105 | return () => { 106 | // complete the scrolling subject 107 | scrollSubject.complete(); 108 | }; 109 | }, []); 110 | 111 | return {children}; 112 | }; 113 | -------------------------------------------------------------------------------- /packages/core/src/hooks/common/useSectionPosition.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { map, filter, switchMap, merge } from 'rxjs/operators'; 3 | import React, { useContext, useMemo, useCallback, useLayoutEffect } from 'react'; 4 | import { useObservableState } from 'observable-hooks'; 5 | 6 | import { PageContext, PageContextInterface } from '../../context/PageContext'; 7 | 8 | import { IntersectionInfo } from './useIntersectionObservable'; 9 | 10 | export interface SectionPosition { 11 | /** From IntersectionObserver: the top of the `
    ` + scrollTop */ 12 | sectionTop: number; 13 | 14 | /** The bounding rectangle of `
    ` */ 15 | boundingRect: ClientRect; 16 | } 17 | 18 | export function useSectionPosition( 19 | /** Ref of the section being tracked */ 20 | sectionRef: React.RefObject, 21 | intersectObsr$: Observable 22 | ): SectionPosition { 23 | const context = useContext(PageContext); 24 | const { resizeObs$ } = context!; 25 | 26 | /** Observer to the window resizing events */ 27 | const combinedResizeObs = useMemo( 28 | () => 29 | resizeObs$ 30 | ? intersectObsr$.pipe( 31 | switchMap((intersectInfo) => { 32 | const { isIntersecting, sectionBoundingRect } = intersectInfo; 33 | return isIntersecting 34 | ? // return the bounding rect from `intersectObsr$` when it appears in the viewport 35 | of(sectionBoundingRect) 36 | // merge it with the intersection observer 37 | .pipe( 38 | merge( 39 | resizeObs$.pipe( 40 | map(() => { 41 | const currentSect = sectionRef.current; 42 | if (currentSect) { 43 | const rect = currentSect.getBoundingClientRect(); 44 | return rect; 45 | } 46 | return undefined; 47 | }) 48 | ) 49 | ) 50 | ) 51 | : // when the section is scrolled out of the viewport 52 | of(undefined); 53 | }) 54 | ) 55 | : // resizeObs$ does not exist on SSR 56 | of(undefined), 57 | [intersectObsr$, resizeObs$, sectionRef] 58 | ); 59 | 60 | /** 61 | * Observable for the absolute position of the section 62 | */ 63 | const sectionSizeObsFunc = useCallback( 64 | () => 65 | combinedResizeObs.pipe( 66 | filter((boundingClientRect) => typeof boundingClientRect !== 'undefined'), 67 | map((boundingClientRect) => { 68 | const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 69 | const { top } = boundingClientRect!; 70 | return { 71 | sectionTop: top + scrollTop, 72 | boundingRect: boundingClientRect as ClientRect, 73 | }; 74 | }) 75 | ), 76 | [combinedResizeObs] 77 | ); 78 | 79 | const [sectionPosition, updateSectionPosition] = useObservableState( 80 | sectionSizeObsFunc, 81 | { 82 | sectionTop: 0, 83 | boundingRect: { 84 | top: 0, 85 | right: 0, 86 | left: 0, 87 | bottom: 0, 88 | // TODO: think of a way to deal with the case that sectionPosition 89 | // cannot be correctly updated occasionally in 90 | // see also `getStickyPosition()`: width < 0 91 | height: -1, 92 | width: -1, 93 | }, 94 | } 95 | ); 96 | 97 | useLayoutEffect(() => { 98 | // update the dimension of the section when it's mounted 99 | 100 | if (sectionRef.current) { 101 | updateSectionPosition({ 102 | ...sectionPosition, 103 | boundingRect: sectionRef.current.getBoundingClientRect(), 104 | }); 105 | } 106 | }, [sectionPosition, sectionRef, updateSectionPosition]); 107 | 108 | return sectionPosition; 109 | } 110 | -------------------------------------------------------------------------------- /_docs/src/RevealingAnimations/Trigger.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Trigger 3 | route: /revealing_animations/use_intersecting_trigger 4 | menu: Revealing Animations 5 | --- 6 | 7 | import { Props } from 'docz' 8 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 9 | 10 | import { DemoWrapper } from '../components/DemoWrapper' 11 | import { CenterBox } from '../components/CenterBox'; 12 | import { AnimatedFadeIn } from '../components/AnimatedFadeIn' 13 | import { AnimatedTrail } from '../components/AnimatedTrail' 14 | import { defaultColors } from '../config/theme'; 15 | 16 | # Trigger 17 | 18 | On occasion, all you want is just to track **whether a component is scrolled into the viewport**, 19 | instead of getting additional information such as the scroll position to engender the potential performance impact. 20 | 21 | That is the moment when the `useIntersectingTrigger` hook comes into play. 22 | 23 | `useIntersectingTrigger` is particularly well-suited for the animations triggered when the elements are scrolled into the viewport. 24 | 25 | ## Example 26 | 27 | 31 | 32 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 33 | 34 | 38 | 39 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 40 | 41 | 45 | 46 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 47 | 48 | 49 | 50 | 51 | ## Usage 52 | 53 | Import the `useIntersectingTrigger` hook. 54 | 55 | ```jsx 56 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 57 | ``` 58 | 59 | Then, you can know whether a component is inside the viewport by giving [the `ref` of a component](https://reactjs.org/docs/refs-and-the-dom.html) to `useIntersectingTrigger` hook. 60 | 61 | By combining with CSS styles or animation libraries such as [react-spring](https://github.com/react-spring/react-spring), 62 | you are able to make various visual effects triggered when components are scrolled into the viewport. 63 | 64 | ```jsx 65 | ... 66 | const ref = useRef(null); 67 | const isIntersecting = useIntersectingTrigger(ref); 68 | 69 | return ( 70 |
    71 | {children} 72 |
    73 | }; 74 | ``` 75 | 76 | 77 | Note that unlike `
    `, ``, or ``, it is not required to wrap 78 | `` outside the `useIntersectingTrigger` 79 | because `useIntersectingTrigger` does not take information related to the window resizing or the scroll effects. 80 | 81 | The detection of `isIntersecting` of `useIntersectingTrigger` is implemented using the `IntersectionObserver` directly. 82 | 83 | 84 | ## Advanced Usage 85 | 86 | ``` 87 | useIntersectingTrigger(containerRef, trackOnce, intersectionConfig) 88 | ``` 89 | 90 | The `useIntersectingTrigger` hook takes three parameters: 91 | 92 | * **containerRef**: `ref` of a component. 93 | * **trackOnce**: finish the tracking once the component scrolled into the viewport. Default value: `false`. 94 | * **intersectionConfig**: 95 | custom config of the [IntersectionObserver](https://developers.google.com/web/updates/2016/04/intersectionobserver); 96 | Default value: `{ threshold: 0, rootMargin: {top: 0, right: 0, bottom: 0, left: 0} }`. 97 | 98 | ### Example of trackOnce 99 | 100 | 101 | Play the fade-in effect just once with trackOnce 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/src/RevealingAnimations/Trigger.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Trigger 3 | route: /revealing_animations/use_intersecting_trigger 4 | menu: Revealing Animations 5 | --- 6 | 7 | import { Props } from 'docz' 8 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 9 | 10 | import { DemoWrapper } from '../components/DemoWrapper' 11 | import { CenterBox } from '../components/CenterBox'; 12 | import { AnimatedFadeIn } from '../components/AnimatedFadeIn' 13 | import { AnimatedTrail } from '../components/AnimatedTrail' 14 | import { defaultColors } from '../config/theme'; 15 | 16 | # Trigger 17 | 18 | On occasion, all you want is just to track **whether a component is scrolled into the viewport**, 19 | instead of getting additional information such as the scroll position to engender the potential performance impact. 20 | 21 | That is the moment when the `useIntersectingTrigger` hook comes into play. 22 | 23 | `useIntersectingTrigger` is particularly well-suited for the animations triggered when the elements are scrolled into the viewport. 24 | 25 | ## Example 26 | 27 | 31 | 32 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 33 | 34 | 38 | 39 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 40 | 41 | 45 | 46 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 47 | 48 | 49 | 50 | 51 | ## Usage 52 | 53 | Import the `useIntersectingTrigger` hook. 54 | 55 | ```jsx 56 | import { useIntersectingTrigger } from '@react-scrolly/trigger'; 57 | ``` 58 | 59 | Then, you can know whether a component is inside the viewport by giving [the `ref` of a component](https://reactjs.org/docs/refs-and-the-dom.html) to `useIntersectingTrigger` hook. 60 | 61 | By combining with CSS styles or animation libraries such as [react-spring](https://github.com/react-spring/react-spring), 62 | you are able to make various visual effects triggered when components are scrolled into the viewport. 63 | 64 | ```jsx 65 | ... 66 | const ref = useRef(null); 67 | const isIntersecting = useIntersectingTrigger(ref); 68 | 69 | return ( 70 |
    71 | {children} 72 |
    73 | }; 74 | ``` 75 | 76 | 77 | Note that unlike `
    `, ``, or ``, it is not required to wrap 78 | `` outside the `useIntersectingTrigger` 79 | because `useIntersectingTrigger` does not take information related to the window resizing or the scroll effects. 80 | 81 | The detection of `isIntersecting` of `useIntersectingTrigger` is implemented using the `IntersectionObserver` directly. 82 | 83 | 84 | ## Advanced Usage 85 | 86 | ``` 87 | useIntersectingTrigger(containerRef, trackOnce, intersectionConfig) 88 | ``` 89 | 90 | The `useIntersectingTrigger` hook takes three parameters: 91 | 92 | * **containerRef**: `ref` of a component. 93 | * **trackOnce**: finish the tracking once the component scrolled into the viewport. Default value: `false`. 94 | * **intersectionConfig**: 95 | custom config of the [IntersectionObserver](https://developers.google.com/web/updates/2016/04/intersectionobserver); 96 | Default value: `{ threshold: 0, rootMargin: {top: 0, right: 0, bottom: 0, left: 0} }`. 97 | 98 | ### Example of trackOnce 99 | 100 | 101 | Play the fade-in effect just once with trackOnce 102 | 103 | 104 | -------------------------------------------------------------------------------- /packages/core/src/hooks/page/useActiveSectionTracker.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react'; 2 | import { Subject } from 'rxjs'; 3 | 4 | import { ActiveSectionInfo, ActiveSectionTracker, sectionID } from '../../context/PageContext'; 5 | 6 | type SectionDistance = { 7 | idx: string; 8 | distance: number; 9 | } | null; 10 | 11 | /** 12 | * Manage the current active section tracking ID 13 | * by selecting the section closest to the scroll bottom 14 | */ 15 | export function useActiveSectionTracker(): ActiveSectionTracker { 16 | /** 17 | * keep track of the all the sections appeared in the viewport, 18 | * - Key: `trackingId` 19 | * - Value: `sectionTop`. 20 | * @example { 21 | * 'section-2': 1000, 22 | * } 23 | */ 24 | const visibleSections = useRef<{ [key: string]: number }>({}); 25 | 26 | /** keep track of the scrollRatios updated by `updateScrollRatio` */ 27 | const sectionScrollRatios = useRef<{ [key: string]: number }>({}); 28 | 29 | /** keep track of the `trackingId` of the section closet to the bottom of the viewport */ 30 | const activeSectionId = useRef(null); 31 | 32 | // make a Subject to take all the changes and transform it as a RX Observable 33 | const activeSectionSubjectRef = useRef(new Subject()); 34 | const activeSectionObservableRef = useRef(activeSectionSubjectRef.current.asObservable()); 35 | 36 | /** 37 | * Let Section set the scrolled ratio if it is active 38 | */ 39 | const updateScrollRatio = useCallback((trackingId: string, scrolledRatio: number) => { 40 | const activeId = activeSectionId.current; 41 | if (activeId && activeId === trackingId) { 42 | // notify all sections subscribing to ActiveSectionInfo 43 | activeSectionSubjectRef.current.next({ 44 | id: trackingId, 45 | ratio: scrolledRatio, 46 | }); 47 | } 48 | 49 | // update the scroll ratios of the sections 50 | sectionScrollRatios.current[trackingId] = scrolledRatio; 51 | }, []); 52 | 53 | /** 54 | * Update the current active section by selecting the section 55 | * closest to the bottom of the viewport 56 | */ 57 | const updateActiveSection = useCallback((scrollBottom: number) => { 58 | const trackedSects = visibleSections.current; 59 | 60 | // find the item closest to the bottom of the viewport 61 | const closest: SectionDistance = Object.keys(trackedSects).reduce( 62 | (accum: SectionDistance, idx) => { 63 | const sectionTop = trackedSects[idx]; 64 | const distance = scrollBottom - sectionTop; 65 | 66 | if (!accum || distance < accum.distance) { 67 | return { idx, distance }; 68 | } 69 | return accum; 70 | }, 71 | null 72 | ); 73 | 74 | if (!closest) { 75 | // there is no section in the viewport 76 | activeSectionId.current = null; 77 | 78 | // notify all sections subscribing to ActiveSectionInfo 79 | activeSectionSubjectRef.current.next(null); 80 | } else if (activeSectionId.current !== closest.idx) { 81 | activeSectionId.current = closest.idx; 82 | 83 | // notify there is a new section being added 84 | activeSectionSubjectRef.current.next({ 85 | id: closest.idx, 86 | ratio: sectionScrollRatios.current[closest.idx], 87 | }); 88 | } 89 | }, []); 90 | 91 | /** 92 | * Add a section that is in the viewport 93 | */ 94 | const addActiveSection = useCallback( 95 | (trackingId: string, sectionTop: number, scrollBottom: number) => { 96 | visibleSections.current[trackingId] = sectionTop; 97 | 98 | updateActiveSection(scrollBottom); 99 | }, 100 | [updateActiveSection] 101 | ); 102 | 103 | /** 104 | * Remove a section from the active sections 105 | */ 106 | const removeActiveSection = useCallback( 107 | (trackingId: string, scrollBottom: number) => { 108 | delete visibleSections.current[trackingId]; 109 | updateActiveSection(scrollBottom); 110 | }, 111 | [updateActiveSection] 112 | ); 113 | 114 | return { 115 | addActiveSection, 116 | removeActiveSection, 117 | updateScrollRatio, 118 | activeSectionObs$: activeSectionObservableRef.current, 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /docs/src/ScrollTracking/Section.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 1. Section 3 | route: /scroll_tracking/section 4 | menu: Scroll Tracking 5 | --- 6 | 7 | import { 8 | PageProvider, 9 | Section, 10 | } from '@react-scrolly/core' 11 | 12 | import { Playground, Link, Props } from 'docz' 13 | import { DemoWrapper } from '../components/DemoWrapper' 14 | import { BorderedDemoSection } from '../components/DemoSection' 15 | import { CenterBox } from '../components/CenterBox'; 16 | import { defaultColors, sectionStyle } from '../config/theme'; 17 | 18 | # Section 19 | ## Basic scroll tracking of a component 20 | 21 | Using `
    ` and ``, you can track the following information of a component (`
    `) when it is scrolling with ease: 22 | 23 | - **Visibility**: Whether the component is intersecting with the viewport. 24 | - **Scroll progress**: The ratio of the component being scrolled. 25 | - **Position**: The position of the component relative to the viewport. 26 | 27 | ## Example 28 | 29 | 30 | 31 | {({isIntersecting, scrolledRatio}) => ( 32 | 33 |

    34 | Section #1 35 |

    36 |
  • isIntersecting: {`${isIntersecting}`}
  • 37 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 38 |
    39 | )} 40 |
    41 | 42 | {({isIntersecting, scrolledRatio}) => ( 43 | 44 |

    45 | Section #2 46 |

    47 |
  • isIntersecting: {`${isIntersecting}`}
  • 48 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 49 |
    50 | )} 51 |
    52 |
    53 |
    54 | 55 | ## Usage 56 | 57 | ### Make sure to wrap `` at the page level 58 | 59 | Before we get started, it is important to note that you have to wrap `` at the page level to track window scrolling and resizing. 60 | 61 | `` contains observers which are essential to `
    `, 62 | by allowing it to subscribe to the events such as window scrolling and resizing. 63 | 64 | ### Use `
    ` to get `sectionInfo` 65 | 66 | Import `
    `: 67 | ```jsx 68 | import { PageProvider, Section } from '@react-scrolly/core'; 69 | ``` 70 | 71 | Use it in your component: 72 | ```jsx 73 | 74 | ... 75 |
    76 | {(sectionInfo) => ( 77 |
    {sectionInfo.isIntersecting}, {sectionInfo.scrolledRatio}
    78 | )} 79 |
    80 |
    ...
    81 | ... 82 |
    83 | ``` 84 | 85 | 86 | ## Properties 87 | 88 | What does `sectionInfo` provides? 89 | 90 | * **isIntersecting**: `boolean` - Whether the section is intersecting with the viewport. 91 | * **scrollInfo**: `object` - With the following properties: 92 | * **scrollTop**: `number` - The top of the viewport, i.e., `pageYOffset` of the window. 93 | * **scrollBottom**: `number` - The bottom of the viewport, i.e., the `pageYOffset` + height of the window. 94 | * **windowHeight**: `number` - The height of the window. 95 | * **scrollOffset**: `number` - The difference between the current scrolltop and previous scrolltop. Positive: if the user scroll down the page. 96 | * **scrolledRatio**: `number` - Ratio of the section being scrolled. It is computed by the ratio of the section surpasses the bottom of the viewport. 97 | 98 | 99 | ## Advanced usages 100 | 101 | If you don't want to use the render props to get `sectionInfo`, you can also use the hooks to get the same information by yourself: 102 | 103 | ### `useSection` hook 104 | 105 | If you prefer using hooks instead of the `
    ` render props, 106 | you can use `useSection` hook to obtain the same information: 107 | 108 | Import the hook: 109 | ```jsx 110 | import { useSection } from '@react-scrolly/core'; 111 | ``` 112 | 113 | Use them in your functional components: 114 | ```jsx 115 | const sectionRef = useRef(null); 116 | const sectionInfo = useSection(sectionRef); 117 | 118 | return ( 119 |
    125 | {children(sectionInfo)} 126 |
    127 | ) 128 | ``` 129 | 130 | ### Adding `trackingId` to sections you want to track in `` 131 | Components like [Plot](/scroll_tracking/plot) or [StickyPlot](/pinning_sections/sticky_plot) 132 | allow you to track the ID and the scroll progress of the `
    ` closest to the bottom of the viewport. 133 | 134 | By adding `trackingId` to sections, `` select the section closest to the bottom of the viewport while scrolling: 135 | 136 | ```jsx 137 |
    ...
    138 | ``` 139 | 140 | or 141 | ```jsx 142 | const sectionRef = useRef(null); 143 | const sectionInfo = useSection(sectionRef, 'TRACKING_ID'); 144 | ``` 145 | 146 | ## Complete Example 147 | 148 | 149 |
    150 | 151 |
    152 | {({isIntersecting, scrolledRatio}) =>
    Section # 1: {`${isIntersecting}`}, {scrolledRatio}
    } 153 |
    154 |
    155 | {({isIntersecting, scrolledRatio}) =>
    Section # 2: {`${isIntersecting}`}, {scrolledRatio}
    } 156 |
    157 |
    158 |
    159 |
    160 | 161 | 162 | -------------------------------------------------------------------------------- /_docs/src/ScrollTracking/Section.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 1. Section 3 | route: /scroll_tracking/section 4 | menu: Scroll Tracking 5 | --- 6 | 7 | import { 8 | PageProvider, 9 | Section, 10 | } from '@react-scrolly/core' 11 | 12 | import { Playground, Link, Props } from 'docz' 13 | import { DemoWrapper } from '../components/DemoWrapper' 14 | import { BorderedDemoSection } from '../components/DemoSection' 15 | import { CenterBox } from '../components/CenterBox'; 16 | import { defaultColors, sectionStyle } from '../config/theme'; 17 | 18 | # Section 19 | ## Basic scroll tracking of a component 20 | 21 | Using `
    ` and ``, you can track the following information of a component (`
    `) when it is scrolling with ease: 22 | 23 | - **Visibility**: Whether the component is intersecting with the viewport. 24 | - **Scroll progress**: The ratio of the component being scrolled. 25 | - **Position**: The position of the component relative to the viewport. 26 | 27 | ## Example 28 | 29 | 30 | 31 | {({isIntersecting, scrolledRatio}) => ( 32 | 33 |

    34 | Section #1 35 |

    36 |
  • isIntersecting: {`${isIntersecting}`}
  • 37 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 38 |
    39 | )} 40 |
    41 | 42 | {({isIntersecting, scrolledRatio}) => ( 43 | 44 |

    45 | Section #2 46 |

    47 |
  • isIntersecting: {`${isIntersecting}`}
  • 48 |
  • scrolledRatio: {scrolledRatio.toFixed(4)}
  • 49 |
    50 | )} 51 |
    52 |
    53 |
    54 | 55 | ## Usage 56 | 57 | ### Make sure to wrap `` at the page level 58 | 59 | Before we get started, it is important to note that you have to wrap `` at the page level to track window scrolling and resizing. 60 | 61 | `` contains observers which are essential to `
    `, 62 | by allowing it to subscribe to the events such as window scrolling and resizing. 63 | 64 | ### Use `
    ` to get `sectionInfo` 65 | 66 | Import `
    `: 67 | ```jsx 68 | import { PageProvider, Section } from '@react-scrolly/core'; 69 | ``` 70 | 71 | Use it in your component: 72 | ```jsx 73 | 74 | ... 75 |
    76 | {(sectionInfo) => ( 77 |
    {sectionInfo.isIntersecting}, {sectionInfo.scrolledRatio}
    78 | )} 79 |
    80 |
    ...
    81 | ... 82 |
    83 | ``` 84 | 85 | 86 | ## Properties 87 | 88 | What does `sectionInfo` provides? 89 | 90 | * **isIntersecting**: `boolean` - Whether the section is intersecting with the viewport. 91 | * **scrollInfo**: `object` - With the following properties: 92 | * **scrollTop**: `number` - The top of the viewport, i.e., `pageYOffset` of the window. 93 | * **scrollBottom**: `number` - The bottom of the viewport, i.e., the `pageYOffset` + height of the window. 94 | * **windowHeight**: `number` - The height of the window. 95 | * **scrollOffset**: `number` - The difference between the current scrolltop and previous scrolltop. Positive: if the user scroll down the page. 96 | * **scrolledRatio**: `number` - Ratio of the section being scrolled. It is computed by the ratio of the section surpasses the bottom of the viewport. 97 | 98 | 99 | ## Advanced usages 100 | 101 | If you don't want to use the render props to get `sectionInfo`, you can also use the hooks to get the same information by yourself: 102 | 103 | ### `useSection` hook 104 | 105 | If you prefer using hooks instead of the `
    ` render props, 106 | you can use `useSection` hook to obtain the same information: 107 | 108 | Import the hook: 109 | ```jsx 110 | import { useSection } from '@react-scrolly/core'; 111 | ``` 112 | 113 | Use them in your functional components: 114 | ```jsx 115 | const sectionRef = useRef(null); 116 | const sectionInfo = useSection(sectionRef); 117 | 118 | return ( 119 |
    125 | {children(sectionInfo)} 126 |
    127 | ) 128 | ``` 129 | 130 | ### Adding `trackingId` to sections you want to track in `` 131 | Components like [Plot](/scroll_tracking/plot) or [StickyPlot](/pinning_sections/sticky_plot) 132 | allow you to track the ID and the scroll progress of the `
    ` closest to the bottom of the viewport. 133 | 134 | By adding `trackingId` to sections, `` select the section closest to the bottom of the viewport while scrolling: 135 | 136 | ```jsx 137 |
    ...
    138 | ``` 139 | 140 | or 141 | ```jsx 142 | const sectionRef = useRef(null); 143 | const sectionInfo = useSection(sectionRef, 'TRACKING_ID'); 144 | ``` 145 | 146 | ## Complete Example 147 | 148 | 149 |
    150 | 151 |
    152 | {({isIntersecting, scrolledRatio}) =>
    Section # 1: {`${isIntersecting}`}, {scrolledRatio}
    } 153 |
    154 |
    155 | {({isIntersecting, scrolledRatio}) =>
    Section # 2: {`${isIntersecting}`}, {scrolledRatio}
    } 156 |
    157 |
    158 |
    159 |
    160 | 161 | 162 | -------------------------------------------------------------------------------- /docs/src/ScrollTracking/Plot.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 2. Plot 3 | route: /scroll_tracking/plot 4 | menu: Scroll Tracking 5 | --- 6 | 7 | import { Props } from 'docz'; 8 | import { PageProvider, Section } from '@react-scrolly/core'; 9 | import { StickyPlot } from '@react-scrolly/plot'; 10 | 11 | import { DemoWrapper } from '../components/DemoWrapper'; 12 | import { BorderedDemoSection } from '../components/DemoSection'; 13 | import { CenterBox } from '../components/CenterBox'; 14 | 15 | import { defaultColors } from '../config/theme'; 16 | 17 | 18 | # Plot 19 | ## Track scroll progress of the section closest to the bottom of the viewport 20 | 21 | `` is another kind of section which allows you to track the reading progress of `
    ` users are currently viewing 22 | (closest to the bottom of the viewport). 23 | 24 | This is particularly handy when you want to display some effects based on the section and its progress users are currently reading. 25 | 26 | Before you start using ``, you have to assign unique `trackingId` to sections you are going to track: 27 | 28 | ```jsx 29 |
    ...
    30 | ``` 31 | 32 | After that, `` will keep the scrolling information of all the sections of `trackingId`, 33 | and `` will to be updated with the scrolling information of the section closet to the bottom of the viewport when it is scrolled into the viewport. 34 | 35 | 36 | In other words, `` provides: 37 | 38 | - All the information provided by `
    ` 39 | - The `trackingId` of the `
    ` currently closet to the bottom of the viewport, and its scroll progress. 40 | 41 | 42 | ## Example 43 | 44 | 45 | 48 | 49 | {() => ( 50 |
    51 | RED 52 |
    53 | )} 54 |
    55 | 56 | {() => ( 57 |
    58 | ORANGE 59 |
    60 | )} 61 |
    62 | 63 | {() => ( 64 |
    65 | YELLOW 66 |
    67 | )} 68 |
    69 | 70 | {() => ( 71 |
    72 | GREEN 73 |
    74 | )} 75 |
    76 | 77 | {() => ( 78 |
    79 | BLUE 80 |
    81 | )} 82 |
    83 | 84 | {() => ( 85 |
    86 | PURPLE 87 |
    88 | )} 89 |
    90 | 91 | } 92 | > 93 | {({activeSection}) => ( 94 | activeSection && ( 95 | 96 |
  • You are viewing {activeSection.id} section
  • 97 |
  • Reading Ratio: {activeSection.ratio.toFixed(4)}
  • 98 |
    99 | ) 100 | )} 101 |
    102 |
    103 |
    104 | 105 | ## Usage 106 | 107 | Import ``: 108 | ```jsx 109 | import { PageProvider } from '@react-scrolly/core'; 110 | import { Plot } from '@react-scrolly/plot'; 111 | ``` 112 | 113 | Use it in your component: 114 | ```jsx 115 | 116 | ... 117 | 118 | {({ sectionInfo, activeSection }) => ( 119 |
    120 |

    Scroll info of this Plot

    121 | {sectionInfo.isIntersecting}, {sectionInfo.scrolledRatio} 122 |
    123 |
    124 |

    Section closet to the bottom of viewport

    125 | `trackingId`: {activeSection.id} 126 | Ratio of the scrolling progress: {activeSection.ratio} 127 |
    128 | )} 129 |
    130 |
    ...
    131 |
    ...
    132 |
    ...
    133 | ... 134 |
    135 | ``` 136 | 137 | ### `` 138 | 139 | If you want to stick the `` like the demo above, 140 | you can use [StickyPlot](/pinning_sections/sticky_plot) which provides the same information in the render props as ``, 141 | while sticking the background content in the viewport when its foreground content is visible the viewport. 142 | 143 | ## Properties 144 | 145 | 1. `isIntersecting`: same as the `isIntersecting` of `sectionInfo` in [Section](/scroll_tracking/section#properties) 146 | 2. `scrolledRatio`: same as the `isIntersecting` of `sectionInfo` in [Section](/scroll_tracking/section#properties) 147 | 2. `activeSection` provides: 148 | - `id`: `trackingId` of the tracked `
    ` closest to the bottom of 149 | - `ratio`: Ratio of the tracked `
    ` closest to the bottom being read 150 | 151 | 152 | ## Advanced usages 153 | 154 | ### `useActiveSectionInfo` hook 155 | 156 | Import the hooks: 157 | 158 | ```jsx 159 | import { useIntersectionObservable, useScrolledRatio } from '@react-scrolly/core'; 160 | import { usePlot } from '@react-scrolly/plot'; 161 | ``` 162 | 163 | You can get the same information using the hooks: 164 | 165 | ```jsx 166 | // assign the ref to the component 167 | const plotRef = useRef(null); 168 | const { sectionInfo, activeSection } = usePlot(plotRef, 'TRACKING_ID'); 169 | ``` 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /_docs/src/ScrollTracking/Plot.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: 2. Plot 3 | route: /scroll_tracking/plot 4 | menu: Scroll Tracking 5 | --- 6 | 7 | import { Props } from 'docz'; 8 | import { PageProvider, Section } from '@react-scrolly/core'; 9 | import { StickyPlot } from '@react-scrolly/plot'; 10 | 11 | import { DemoWrapper } from '../components/DemoWrapper'; 12 | import { BorderedDemoSection } from '../components/DemoSection'; 13 | import { CenterBox } from '../components/CenterBox'; 14 | 15 | import { defaultColors } from '../config/theme'; 16 | 17 | 18 | # Plot 19 | ## Track scroll progress of the section closest to the bottom of the viewport 20 | 21 | `` is another kind of section which allows you to track the reading progress of `
    ` users are currently viewing 22 | (closest to the bottom of the viewport). 23 | 24 | This is particularly handy when you want to display some effects based on the section and its progress users are currently reading. 25 | 26 | Before you start using ``, you have to assign unique `trackingId` to sections you are going to track: 27 | 28 | ```jsx 29 |
    ...
    30 | ``` 31 | 32 | After that, `` will keep the scrolling information of all the sections of `trackingId`, 33 | and `` will to be updated with the scrolling information of the section closet to the bottom of the viewport when it is scrolled into the viewport. 34 | 35 | 36 | In other words, `` provides: 37 | 38 | - All the information provided by `
    ` 39 | - The `trackingId` of the `
    ` currently closet to the bottom of the viewport, and its scroll progress. 40 | 41 | 42 | ## Example 43 | 44 | 45 | 48 | 49 | {() => ( 50 |
    51 | RED 52 |
    53 | )} 54 |
    55 | 56 | {() => ( 57 |
    58 | ORANGE 59 |
    60 | )} 61 |
    62 | 63 | {() => ( 64 |
    65 | YELLOW 66 |
    67 | )} 68 |
    69 | 70 | {() => ( 71 |
    72 | GREEN 73 |
    74 | )} 75 |
    76 | 77 | {() => ( 78 |
    79 | BLUE 80 |
    81 | )} 82 |
    83 | 84 | {() => ( 85 |
    86 | PURPLE 87 |
    88 | )} 89 |
    90 | 91 | } 92 | > 93 | {({activeSection}) => ( 94 | activeSection && ( 95 | 96 |
  • You are viewing {activeSection.id} section
  • 97 |
  • Reading Ratio: {activeSection.ratio.toFixed(4)}
  • 98 |
    99 | ) 100 | )} 101 |
    102 |
    103 |
    104 | 105 | ## Usage 106 | 107 | Import ``: 108 | ```jsx 109 | import { PageProvider } from '@react-scrolly/core'; 110 | import { Plot } from '@react-scrolly/plot'; 111 | ``` 112 | 113 | Use it in your component: 114 | ```jsx 115 | 116 | ... 117 | 118 | {({ sectionInfo, activeSection }) => ( 119 |
    120 |

    Scroll info of this Plot

    121 | {sectionInfo.isIntersecting}, {sectionInfo.scrolledRatio} 122 |
    123 |
    124 |

    Section closet to the bottom of viewport

    125 | `trackingId`: {activeSection.id} 126 | Ratio of the scrolling progress: {activeSection.ratio} 127 |
    128 | )} 129 |
    130 |
    ...
    131 |
    ...
    132 |
    ...
    133 | ... 134 |
    135 | ``` 136 | 137 | ### `` 138 | 139 | If you want to stick the `` like the demo above, 140 | you can use [StickyPlot](/pinning_sections/sticky_plot) which provides the same information in the render props as ``, 141 | while sticking the background content in the viewport when its foreground content is visible the viewport. 142 | 143 | ## Properties 144 | 145 | 1. `isIntersecting`: same as the `isIntersecting` of `sectionInfo` in [Section](/scroll_tracking/section#properties) 146 | 2. `scrolledRatio`: same as the `isIntersecting` of `sectionInfo` in [Section](/scroll_tracking/section#properties) 147 | 2. `activeSection` provides: 148 | - `id`: `trackingId` of the tracked `
    ` closest to the bottom of 149 | - `ratio`: Ratio of the tracked `
    ` closest to the bottom being read 150 | 151 | 152 | ## Advanced usages 153 | 154 | ### `useActiveSectionInfo` hook 155 | 156 | Import the hooks: 157 | 158 | ```jsx 159 | import { useIntersectionObservable, useScrolledRatio } from '@react-scrolly/core'; 160 | import { usePlot } from '@react-scrolly/plot'; 161 | ``` 162 | 163 | You can get the same information using the hooks: 164 | 165 | ```jsx 166 | // assign the ref to the component 167 | const plotRef = useRef(null); 168 | const { sectionInfo, activeSection } = usePlot(plotRef, 'TRACKING_ID'); 169 | ``` 170 | 171 | 172 | 173 | --------------------------------------------------------------------------------