├── .husky ├── pre-commit ├── pre-push └── commit-msg ├── .prettierignore ├── scripts ├── setup-tests.ts ├── tsconfig.build.json ├── rollup.config.ts ├── tsconfig.dev.json ├── tsconfig.base.json └── rollupConfigs.ts ├── .czrc ├── tsconfig.json ├── misc ├── demo.gif └── device_orientation.jpg ├── stories ├── TiltImg │ ├── TiltImg.demozap.css │ ├── img │ │ └── nyc.jpg │ ├── TiltImg.demozap.tsx │ └── _TiltImg.tsx ├── MultipleTiltScroll │ ├── MultipleTiltScroll.demozap.css │ ├── MultipleTiltScroll.demozap.tsx │ └── _MultipleTiltScroll.tsx ├── FlipPage │ ├── Page │ │ ├── lorem-picsum.png │ │ ├── Page.css │ │ └── Page.tsx │ ├── FlipPage.demozap.css │ ├── FlipPage.demozap.tsx │ └── _FlipPage.tsx ├── ParallaxEffectImg │ ├── img │ │ ├── tree.png │ │ └── background.jpg │ ├── ParallaxEffectImg.demozap.css │ ├── ParallaxEffectImg.demozap.tsx │ └── _ParallaxEffectImg.tsx ├── types │ └── images.d.ts ├── MultipleTilt │ ├── MultipleTilt.demozap.css │ ├── MultipleTilt.demozap.tsx │ └── _MultipleTilt.tsx ├── Default │ ├── Default.demozap.tsx │ └── _Default.tsx ├── _DefaultComponent │ ├── DefaultComponent.tsx │ └── DefaultComponent.css ├── KeepFloating │ ├── KeepFloating.demozap.tsx │ └── _KeepFloating.tsx ├── ReverseTilt │ ├── ReverseTilt.demozap.tsx │ └── _ReverseTilt.tsx ├── InitialTilt │ ├── InitialTilt.demozap.tsx │ └── _InitialTilt.tsx ├── GlareEffect │ ├── GlareEffect.demozap.tsx │ └── _GlareEffect.tsx ├── GlareEffect360 │ ├── GlareEffect360.demozap.tsx │ └── _GlareEffect360.tsx ├── ParallaxEffect │ ├── ParallaxEffect.demozap.tsx │ ├── ParallaxEffect.demozap.css │ └── _ParallaxEffect.tsx ├── GlareEffectNoTilt │ ├── GlareEffectNoTilt.demozap.tsx │ └── _GlareEffectNoTilt.tsx ├── EventTiltAngle │ ├── EventTiltAngle.demozap.css │ ├── EventTiltAngle.demozap.tsx │ └── _EventTiltAngle.tsx ├── ParallaxEffectGlareScale │ ├── ParallaxEffectGlareScale.demozap.tsx │ ├── ParallaxEffectGlareScale.demozap.css │ └── _ParallaxEffectGlareScale.tsx ├── TrackOnWindow │ ├── TrackOnWindow.demozap.tsx │ ├── TrackOnWindow.demozap.css │ └── _TrackOnWindow.tsx ├── ReactParallaxTilt.css ├── FlipVH │ ├── FlipVH.demozap.css │ ├── FlipVH.demozap.tsx │ └── _FlipVH.tsx ├── ScaleNoTilt │ ├── ScaleNoTilt.demozap.css │ ├── ScaleNoTilt.demozap.tsx │ └── _ScaleNoTilt.tsx ├── TiltDisableAxis │ ├── TiltDisableAxis.demozap.css │ ├── TiltDisableAxis.demozap.tsx │ └── _TiltDisableAxis.tsx ├── TiltScale │ ├── TiltScale.demozap.css │ ├── TiltScale.demozap.tsx │ └── _TiltScale.tsx ├── TiltManualInput │ ├── TiltManualInput.demozap.css │ ├── TiltManualInput.demozap.tsx │ └── _TiltManualInput.tsx ├── EventParams │ ├── EventParams.demozap.css │ ├── EventParams.demozap.tsx │ └── _EventParams.tsx └── ReactParallaxTilt.stories.tsx ├── .prettierrc ├── .lintstagedrc ├── .storybook ├── theme.ts ├── manager.ts ├── preview.tsx └── main.ts ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml ├── workflows │ ├── tsc.yml │ ├── lint.yml │ ├── build.yml │ ├── actions │ │ └── setup_node_npm │ │ │ └── action.yml │ ├── npm-release.yml │ ├── deploy-storybook.yml │ ├── test.yml │ └── test-e2e.yml └── pull_request_template.md ├── .commitlintrc.ts ├── e2e ├── consts.ts ├── global.setup.ts ├── events.spec.ts └── on-move-params.spec.ts ├── src ├── utils │ ├── types.ts │ ├── TiltTest.tsx │ └── helperFns.ts ├── index.ts ├── features │ ├── tilt │ │ ├── test │ │ │ ├── tiltOnEnter.test.tsx │ │ │ ├── tiltOnLeave.test.tsx │ │ │ ├── tiltDisable.test.tsx │ │ │ ├── tiltOnTouchMove.test.tsx │ │ │ ├── tiltTrackOnWindow.test.tsx │ │ │ ├── tiltMaxAngle.test.tsx │ │ │ ├── tiltReverse.test.tsx │ │ │ ├── tiltStyle.test.tsx │ │ │ ├── tiltReset.test.tsx │ │ │ ├── tiltOnPropChange.test.tsx │ │ │ ├── tiltAxis.test.tsx │ │ │ └── tiltManualAngle.test.tsx │ │ ├── types.public.ts │ │ └── Tilt.ts │ └── glare │ │ ├── types.public.ts │ │ ├── test │ │ ├── glareStyle.test.tsx │ │ └── glare.test.tsx │ │ └── Glare.ts └── react-parallax-tilt │ ├── types.ts │ ├── defaultProps.ts │ ├── types.public.ts │ └── ReactParallaxTilt.tsx ├── .gitignore ├── knip.ts ├── vitest.config.ts ├── LICENSE ├── playwright.config.ts ├── package.json ├── eslint.config.mjs └── README.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint:staged -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run test:report -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test-unit-report 2 | dist -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" -------------------------------------------------------------------------------- /scripts/setup-tests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "node_modules/cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./scripts/tsconfig.dev.json" 3 | } 4 | -------------------------------------------------------------------------------- /misc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosir/react-parallax-tilt/HEAD/misc/demo.gif -------------------------------------------------------------------------------- /stories/TiltImg/TiltImg.demozap.css: -------------------------------------------------------------------------------- 1 | .tilt-img { 2 | .inner-element { 3 | width: 70vw; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /misc/device_orientation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosir/react-parallax-tilt/HEAD/misc/device_orientation.jpg -------------------------------------------------------------------------------- /stories/TiltImg/img/nyc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosir/react-parallax-tilt/HEAD/stories/TiltImg/img/nyc.jpg -------------------------------------------------------------------------------- /stories/MultipleTiltScroll/MultipleTiltScroll.demozap.css: -------------------------------------------------------------------------------- 1 | .multiple-tilt-scroll > * { 2 | margin-bottom: 200px; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*": ["prettier --write --ignore-unknown"], 3 | "*.{js,jsx,ts,tsx}": ["eslint --max-warnings 0"] 4 | } 5 | -------------------------------------------------------------------------------- /stories/FlipPage/Page/lorem-picsum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosir/react-parallax-tilt/HEAD/stories/FlipPage/Page/lorem-picsum.png -------------------------------------------------------------------------------- /stories/ParallaxEffectImg/img/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosir/react-parallax-tilt/HEAD/stories/ParallaxEffectImg/img/tree.png -------------------------------------------------------------------------------- /stories/ParallaxEffectImg/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosir/react-parallax-tilt/HEAD/stories/ParallaxEffectImg/img/background.jpg -------------------------------------------------------------------------------- /stories/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: string; 3 | export = value; 4 | } 5 | declare module '*.jpg' { 6 | const value: string; 7 | export = value; 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'storybook/theming'; 2 | 3 | export const theme = create({ 4 | base: 'light', 5 | brandTitle: 'React Parallax Tilt 👀', 6 | brandUrl: 'https://github.com/mkosir/react-parallax-tilt', 7 | }); 8 | -------------------------------------------------------------------------------- /stories/FlipPage/FlipPage.demozap.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow-x: hidden; 3 | } 4 | 5 | .flip-page { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | width: 80vw; 12 | } 13 | -------------------------------------------------------------------------------- /stories/MultipleTilt/MultipleTilt.demozap.css: -------------------------------------------------------------------------------- 1 | .multiple-tilt { 2 | display: flex; 3 | flex-direction: row; 4 | 5 | .col { 6 | margin-right: 20px; 7 | 8 | :first-child { 9 | margin-bottom: 20px; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/mkosir/react-parallax-tilt/discussions 5 | about: Ask questions and discuss topics with other community members 6 | -------------------------------------------------------------------------------- /.commitlintrc.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from '@commitlint/types'; 2 | 3 | const commitlintConfig: UserConfig = { 4 | extends: ['@commitlint/config-conventional'], 5 | }; 6 | 7 | // eslint-disable-next-line import-x/no-default-export 8 | export default commitlintConfig; 9 | -------------------------------------------------------------------------------- /stories/Default/Default.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 4 | 5 | const Default = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default Default; 12 | -------------------------------------------------------------------------------- /stories/_DefaultComponent/DefaultComponent.tsx: -------------------------------------------------------------------------------- 1 | import './DefaultComponent.css'; 2 | 3 | export const DefaultComponent = () => ( 4 |
5 |
React
6 |
Parallax Tilt
7 |
👀
8 |
9 | ); 10 | -------------------------------------------------------------------------------- /scripts/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["../src/**/*"], 4 | "exclude": [ 5 | "../src/**/*.test.ts", 6 | "../src/**/*.test.tsx", 7 | "../src/**/TiltTest.tsx", 8 | "../src/**/*.spec.ts", 9 | "../src/**/*.spec.tsx" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /e2e/consts.ts: -------------------------------------------------------------------------------- 1 | import type { FrameLocator, Page } from '@playwright/test'; 2 | 3 | const IFRAME_SELECTOR = 'iframe[title="storybook-preview-iframe"]'; 4 | 5 | export const getIframeContent = (page: Page): FrameLocator => { 6 | const content = page.frameLocator(IFRAME_SELECTOR); 7 | return content; 8 | }; 9 | -------------------------------------------------------------------------------- /scripts/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup'; 2 | 3 | import { LEGACY_CONFIG, MODERN_CONFIG } from './rollupConfigs'; 4 | 5 | const rollupConfig = defineConfig([...LEGACY_CONFIG, ...MODERN_CONFIG]); 6 | 7 | // eslint-disable-next-line import-x/no-default-export 8 | export default rollupConfig; 9 | -------------------------------------------------------------------------------- /stories/KeepFloating/KeepFloating.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 4 | 5 | const KeepFloating = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default KeepFloating; 12 | -------------------------------------------------------------------------------- /stories/ReverseTilt/ReverseTilt.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 4 | 5 | const ReverseTilt = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default ReverseTilt; 12 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type ElementSizePosition = { 2 | width: number | null; 3 | height: number | null; 4 | left: number | null; 5 | top: number | null; 6 | }; 7 | 8 | export type ClientPosition = { 9 | x: number | null; 10 | y: number | null; 11 | xPercentage: number; 12 | yPercentage: number; 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | compiled 4 | build-storybook-static 5 | 6 | bundle-analysis.html 7 | 8 | .rpt2_cache 9 | 10 | npm-debug.log* 11 | 12 | .DS_Store 13 | .env 14 | *.log 15 | .vscode 16 | .idea 17 | test-unit-report 18 | /test-e2e-results/ 19 | /test-e2e-report/ 20 | /blob-report/ 21 | /playwright/.cache/ 22 | -------------------------------------------------------------------------------- /stories/InitialTilt/InitialTilt.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 4 | 5 | const Default = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default Default; 12 | -------------------------------------------------------------------------------- /scripts/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | // Type check and lint all files and folders. 4 | // Explicitly include those starting with a dot (.) as they are ignored by default in glob patterns. 5 | "include": ["../.", "../.storybook/**/*", "../.commitlintrc.ts"], 6 | "exclude": ["../dist", "../node_modules"] 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import-x/no-default-export 2 | export { ReactParallaxTilt as default } from './react-parallax-tilt/ReactParallaxTilt'; 3 | 4 | // Public exposed library types 5 | export * from './features/glare/types.public'; 6 | export * from './features/tilt/types.public'; 7 | export * from './react-parallax-tilt/types.public'; 8 | -------------------------------------------------------------------------------- /src/utils/TiltTest.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactParallaxTiltProps } from '@/index'; 2 | import Tilt from '@/index'; 3 | 4 | export const TiltTest = (reactParallaxTiltProps: ReactParallaxTiltProps) => ( 5 | 6 |
test
7 |
8 | ); 9 | -------------------------------------------------------------------------------- /.github/workflows/tsc.yml: -------------------------------------------------------------------------------- 1 | name: tsc 2 | 3 | on: push 4 | 5 | jobs: 6 | tsc: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v6 12 | 13 | - name: Setup Node/npm ⚙️ 14 | uses: ./.github/workflows/actions/setup_node_npm 15 | 16 | - name: tsc 🔎 17 | run: npm run tsc 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: push 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v6 12 | 13 | - name: Setup Node/npm ⚙️ 14 | uses: ./.github/workflows/actions/setup_node_npm 15 | 16 | - name: Lint ✅ 17 | run: npm run lint 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v6 12 | 13 | - name: Setup Node/npm ⚙️ 14 | uses: ./.github/workflows/actions/setup_node_npm 15 | 16 | - name: Build 🏗️ 17 | run: npm run build 18 | -------------------------------------------------------------------------------- /stories/GlareEffect/GlareEffect.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 4 | 5 | const GlareEffect = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default GlareEffect; 12 | -------------------------------------------------------------------------------- /stories/_DefaultComponent/DefaultComponent.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .default-component { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | font-size: 40px; 11 | font-style: italic; 12 | color: white; 13 | border: 5px solid black; 14 | border-radius: 20px; 15 | } 16 | -------------------------------------------------------------------------------- /stories/GlareEffect360/GlareEffect360.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 4 | 5 | const GlareEffect360 = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default GlareEffect360; 12 | -------------------------------------------------------------------------------- /stories/ParallaxEffectImg/ParallaxEffectImg.demozap.css: -------------------------------------------------------------------------------- 1 | .parallax-effect-img { 2 | transform-style: preserve-3d; 3 | transform: perspective(1000px); 4 | background-image: url('./img/background.jpg'); 5 | background-size: contain; 6 | background-repeat: no-repeat; 7 | 8 | .inner-element { 9 | transform: translateZ(40px) scale(0.8); 10 | width: 70%; 11 | margin-left: 25%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/helperFns.ts: -------------------------------------------------------------------------------- 1 | export const setTransition = ( 2 | element: HTMLElement, 3 | property: 'all' | 'opacity', 4 | duration: number, 5 | timing: string, 6 | ): void => { 7 | element.style.transition = `${property} ${duration}ms ${timing}`; 8 | }; 9 | 10 | export const constrainToRange = (value: number, rangeMin: number, rangeMax: number): number => 11 | Math.min(Math.max(value, rangeMin), rangeMax); 12 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from 'storybook/manager-api'; 2 | 3 | import { theme } from './theme'; 4 | 5 | addons.setConfig({ 6 | theme, 7 | toolbar: { 8 | title: { hidden: false }, 9 | zoom: { hidden: false }, 10 | eject: { hidden: false }, 11 | copy: { hidden: false }, 12 | fullscreen: { hidden: false }, 13 | }, 14 | bottomPanelHeight: 0, 15 | panelPosition: false, 16 | }); 17 | -------------------------------------------------------------------------------- /knip.ts: -------------------------------------------------------------------------------- 1 | import type { KnipConfig } from 'knip'; 2 | 3 | const knipConfig: KnipConfig = { 4 | entry: ['src/index.ts'], 5 | ignore: [ 6 | 'dist/**', 7 | 'compiled/**', 8 | 'build-storybook-static/**', 9 | 'test-unit-report/**', 10 | 'test-e2e-results/**', 11 | 'test-e2e-report/**', 12 | ], 13 | }; 14 | 15 | // eslint-disable-next-line import-x/no-default-export 16 | export default knipConfig; 17 | -------------------------------------------------------------------------------- /stories/ParallaxEffect/ParallaxEffect.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | import './ParallaxEffect.demozap.css'; 3 | 4 | const ParallaxEffect = () => ( 5 | 6 |
7 |
React
8 |
Parallax Tilt
9 |
👀
10 |
11 |
12 | ); 13 | 14 | export default ParallaxEffect; 15 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react-vite'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: '^on[A-Z].*' }, 6 | controls: { 7 | // Description toggle 8 | // expanded: true, 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | }, 15 | }; 16 | 17 | // eslint-disable-next-line import-x/no-default-export 18 | export default preview; 19 | -------------------------------------------------------------------------------- /stories/GlareEffectNoTilt/GlareEffectNoTilt.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 4 | 5 | const GlareEffectNoTilt = () => ( 6 | 14 | 15 | 16 | ); 17 | 18 | export default GlareEffectNoTilt; 19 | -------------------------------------------------------------------------------- /stories/TiltImg/TiltImg.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import './TiltImg.demozap.css'; 4 | import imgNyc from './img/nyc.jpg'; 5 | 6 | const TiltImg = () => ( 7 | 16 | pic 17 | 18 | ); 19 | 20 | export default TiltImg; 21 | -------------------------------------------------------------------------------- /stories/Default/_Default.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './Default.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 8 | 9 | const Default = () => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export default Default; 16 | `; 17 | 18 | export const _Default = () => ( 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /e2e/global.setup.ts: -------------------------------------------------------------------------------- 1 | import { test as setup } from '@playwright/test'; 2 | 3 | setup("remove Storybook 'what's new' popup", async ({ page }) => { 4 | await page.goto('/'); 5 | 6 | // Wait for sidebar to load. 7 | await page.getByRole('button', { name: 'Keep floating' }).waitFor({ 8 | state: 'visible', 9 | timeout: 5000, 10 | }); 11 | 12 | if (await page.isVisible("text='Learn what's new in Storybook'")) { 13 | await page.getByRole('button', { name: 'Dismiss notification' }).click(); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /stories/KeepFloating/_KeepFloating.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './KeepFloating.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 8 | 9 | const KeepFloating = () => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export default KeepFloating; 16 | `; 17 | 18 | export const _KeepFloating = () => ( 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /stories/ReverseTilt/_ReverseTilt.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './ReverseTilt.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 8 | 9 | const ReverseTilt = () => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export default ReverseTilt; 16 | `; 17 | 18 | export const _ReverseTilt = () => ( 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /stories/ParallaxEffectImg/ParallaxEffectImg.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import './ParallaxEffectImg.demozap.css'; 4 | import imgTree from './img/tree.png'; 5 | 6 | const ParallaxEffectImg = () => ( 7 | 16 | pic 17 | 18 | ); 19 | 20 | export default ParallaxEffectImg; 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### 🚀 Pull Request Template 2 | 3 | **Description** 4 | 5 | Feature Proposal ... 6 | 7 | OR 8 | 9 | Fixes a bug where '...' happened when '...' 10 | 11 | **Checklist** 12 | 13 | - [ ] Commit messages should follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) as much as possible. After staging your changes please run `npm run commit` 14 | - [ ] Lint, TypeScript, Prettier and all tests passing - `npm run lint && npm run tsc && npm run test:report` 15 | - [ ] Extended the Storybook / README / documentation, if necessary 16 | -------------------------------------------------------------------------------- /stories/InitialTilt/_InitialTilt.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './InitialTilt.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 8 | 9 | const Default = () => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export default Default; 16 | `; 17 | 18 | export const _InitialTilt = () => ( 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /stories/EventTiltAngle/EventTiltAngle.demozap.css: -------------------------------------------------------------------------------- 1 | .event-tilt-angle { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | 7 | .component-tilt { 8 | font-style: italic; 9 | margin-top: 30px; 10 | font-size: 24px; 11 | text-align: center; 12 | border: 2px solid black; 13 | border-radius: 8px; 14 | padding: 10px; 15 | 16 | .row { 17 | display: flex; 18 | flex-direction: row; 19 | justify-content: space-between; 20 | align-items: center; 21 | margin-top: 10px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | 4 | const storybookConfig: StorybookConfig = { 5 | stories: ['../stories/**/*.stories.tsx'], 6 | framework: { 7 | name: '@storybook/react-vite', 8 | options: {}, 9 | }, 10 | viteFinal: (storybookConfig) => { 11 | return { 12 | ...storybookConfig, 13 | plugins: [...(storybookConfig.plugins ?? []), tsconfigPaths()], 14 | }; 15 | }, 16 | }; 17 | 18 | // eslint-disable-next-line import-x/no-default-export 19 | export default storybookConfig; 20 | -------------------------------------------------------------------------------- /stories/ParallaxEffectGlareScale/ParallaxEffectGlareScale.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | import './ParallaxEffectGlareScale.demozap.css'; 3 | 4 | const ParallaxEffectGlareScale = () => ( 5 | 12 |
13 |
React
14 |
Parallax Tilt
15 |
👀
16 |
17 |
18 | ); 19 | 20 | export default ParallaxEffectGlareScale; 21 | -------------------------------------------------------------------------------- /stories/MultipleTiltScroll/MultipleTiltScroll.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import './MultipleTiltScroll.demozap.css'; 4 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 5 | 6 | const MultipleTiltScroll = () => ( 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | 23 | export default MultipleTiltScroll; 24 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltOnEnter.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent } from '@testing-library/react'; 2 | 3 | import type { OnEnter } from '@/index'; 4 | import { TiltTest } from '@/utils/TiltTest'; 5 | 6 | describe('Tilt - onEnter', () => { 7 | it('should trigger onEnter event when mouse enters an element', () => { 8 | const onEnter = vi.fn(); 9 | 10 | render(); 11 | 12 | fireEvent.mouseEnter(screen.getByText('test')); 13 | 14 | const onEnterParams = onEnter.mock.calls[0][0]; 15 | expect(onEnterParams.event.type).toBe('mouseenter'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltOnLeave.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent } from '@testing-library/react'; 2 | 3 | import type { OnLeave } from '@/index'; 4 | import { TiltTest } from '@/utils/TiltTest'; 5 | 6 | describe('Tilt - onLeave', () => { 7 | it('should trigger onLeave event when mouse leaves an element', () => { 8 | const onLeave = vi.fn(); 9 | 10 | render(); 11 | 12 | fireEvent.mouseLeave(screen.getByText('test')); 13 | 14 | const onLeaveParams = onLeave.mock.calls[0][0]; 15 | expect(onLeaveParams.event.type).toBe('mouseleave'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /stories/TrackOnWindow/TrackOnWindow.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | import './TrackOnWindow.demozap.css'; 3 | 4 | const TrackOnWindow = () => ( 5 | 15 |
16 |
React
17 |
Parallax Tilt
18 |
👀
19 |
20 |
21 | ); 22 | 23 | export default TrackOnWindow; 24 | -------------------------------------------------------------------------------- /stories/GlareEffect/_GlareEffect.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './GlareEffect.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 8 | 9 | const GlareEffect = () => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export default GlareEffect; 16 | `; 17 | 18 | export const _GlareEffect = () => ( 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /.github/workflows/actions/setup_node_npm/action.yml: -------------------------------------------------------------------------------- 1 | name: 'setup_node_npm' 2 | description: 'Setup Node/npm ⚙️' 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - name: Setup Node.js 7 | uses: actions/setup-node@v6 8 | with: 9 | node-version: '24.x' 10 | 11 | - name: Cache dependencies 12 | id: cache_dependencies 13 | uses: actions/cache@v4 14 | with: 15 | path: node_modules 16 | key: node-modules-${{ hashFiles('package-lock.json') }} 17 | 18 | - name: Install dependencies 19 | shell: bash 20 | if: steps.cache_dependencies.outputs.cache-hit != 'true' 21 | run: npm ci 22 | -------------------------------------------------------------------------------- /stories/GlareEffect360/_GlareEffect360.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './GlareEffect360.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 8 | 9 | const GlareEffect360 = () => ( 10 | 11 | 12 | 13 | ); 14 | 15 | export default GlareEffect360; 16 | `; 17 | 18 | export const _GlareEffect360 = () => ( 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /stories/ReactParallaxTilt.css: -------------------------------------------------------------------------------- 1 | .root-story { 2 | font-family: Arial, sans-serif; 3 | 4 | .tab-demo.react-tabs__tab-panel--selected { 5 | margin-top: 10vh; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | } 12 | 13 | :root { 14 | --default-stripe-bg-color: #077407; 15 | --default-stripe-color: #015f01; 16 | } 17 | 18 | .background-stripes { 19 | background: repeating-linear-gradient( 20 | 45deg, 21 | var(--default-stripe-color), 22 | var(--default-stripe-color) 35px, 23 | var(--default-stripe-bg-color) 35px, 24 | var(--default-stripe-bg-color) 70px 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /stories/FlipVH/FlipVH.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .flip-vh { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | font-size: 30px; 11 | font-style: italic; 12 | background-color: darkgreen; 13 | color: white; 14 | border: 5px solid black; 15 | border-radius: 20px; 16 | .header { 17 | font-size: 38px; 18 | } 19 | .form { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: flex-start; 23 | input { 24 | transform: scale(1.5); 25 | height: 20px; 26 | margin-right: 7px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stories/TrackOnWindow/TrackOnWindow.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .track-on-window { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | background-color: darkgreen; 11 | color: white; 12 | border: 5px solid black; 13 | border-radius: 20px; 14 | 15 | transform-style: preserve-3d; 16 | 17 | .inner-element { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | font-size: 35px; 23 | font-style: italic; 24 | color: white; 25 | transform: translateZ(60px); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stories/ParallaxEffect/ParallaxEffect.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .parallax-effect { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | background-color: darkgreen; 11 | color: white; 12 | border: 5px solid black; 13 | border-radius: 20px; 14 | 15 | transform-style: preserve-3d; 16 | 17 | .inner-element { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | font-size: 35px; 23 | font-style: italic; 24 | color: white; 25 | transform: translateZ(60px); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stories/MultipleTilt/MultipleTilt.demozap.tsx: -------------------------------------------------------------------------------- 1 | import Tilt from '@/index'; 2 | 3 | import './MultipleTilt.demozap.css'; 4 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 5 | 6 | const MultipleTilt = () => ( 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | ); 26 | 27 | export default MultipleTilt; 28 | -------------------------------------------------------------------------------- /stories/ScaleNoTilt/ScaleNoTilt.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .scale-no-tilt { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | font-size: 30px; 11 | font-style: italic; 12 | background-color: darkgreen; 13 | color: white; 14 | border: 5px solid black; 15 | border-radius: 20px; 16 | .header { 17 | font-size: 38px; 18 | } 19 | .form { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: flex-start; 23 | input { 24 | transform: scale(1.5); 25 | height: 20px; 26 | margin-right: 7px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stories/TiltDisableAxis/TiltDisableAxis.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .tilt-disable-axis { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | font-size: 30px; 11 | font-style: italic; 12 | background-color: darkgreen; 13 | color: white; 14 | border: 5px solid black; 15 | border-radius: 20px; 16 | .header { 17 | font-size: 38px; 18 | } 19 | .form { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: flex-start; 23 | input { 24 | transform: scale(1.5); 25 | height: 20px; 26 | margin-right: 7px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stories/TiltScale/TiltScale.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .tilt-scale { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | font-size: 30px; 11 | font-style: italic; 12 | background-color: darkgreen; 13 | color: white; 14 | border: 5px solid black; 15 | border-radius: 20px; 16 | 17 | .header { 18 | font-size: 38px; 19 | } 20 | 21 | .form { 22 | display: flex; 23 | flex-direction: column; 24 | align-items: flex-start; 25 | 26 | input { 27 | transform: scale(1.5); 28 | height: 20px; 29 | margin-right: 7px; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stories/ParallaxEffectGlareScale/ParallaxEffectGlareScale.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .parallax-effect-glare-scale { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 300px; 9 | height: 300px; 10 | background-color: darkgreen; 11 | color: white; 12 | border: 5px solid black; 13 | border-radius: 20px; 14 | 15 | transform-style: preserve-3d; 16 | 17 | .inner-element { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | font-size: 35px; 23 | font-style: italic; 24 | color: white; 25 | transform: translateZ(60px); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stories/GlareEffectNoTilt/_GlareEffectNoTilt.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './GlareEffectNoTilt.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 8 | 9 | const GlareEffectNoTilt = () => ( 10 | 18 | 19 | 20 | ); 21 | 22 | export default GlareEffectNoTilt; 23 | `; 24 | 25 | export const _GlareEffectNoTilt = () => ( 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /stories/TiltManualInput/TiltManualInput.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .tilt-manual-input { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | 9 | .react-parallax-tilt { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | width: 300px; 15 | height: 300px; 16 | font-size: 35px; 17 | font-style: italic; 18 | background-color: darkgreen; 19 | color: white; 20 | border: 5px solid black; 21 | border-radius: 20px; 22 | } 23 | 24 | .manual-input { 25 | display: flex; 26 | flex-direction: column; 27 | align-items: flex-start; 28 | margin-top: 20px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | // eslint-disable-next-line import-x/no-default-export 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths()], 7 | test: { 8 | environment: 'jsdom', 9 | globals: true, 10 | setupFiles: './scripts/setup-tests.ts', 11 | include: ['src/**/*.test.{ts,tsx}'], 12 | coverage: { 13 | include: ['src/**/*'], 14 | reportsDirectory: 'test-unit-report', 15 | provider: 'v8', 16 | reporter: ['text', 'text-summary', 'html', 'lcov'], 17 | thresholds: { 18 | branches: 90, 19 | functions: 90, 20 | lines: 90, 21 | statements: 90, 22 | }, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /.github/workflows/npm-release.yml: -------------------------------------------------------------------------------- 1 | name: npm-release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [test] 6 | types: [completed] 7 | branches: [main] 8 | 9 | jobs: 10 | npm-release: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 13 | env: 14 | NODE_ENV: 'production' 15 | 16 | steps: 17 | - name: Checkout 🛎️ 18 | uses: actions/checkout@v6 19 | 20 | - name: Setup Node/npm ⚙️ 21 | uses: ./.github/workflows/actions/setup_node_npm 22 | 23 | - name: Build 🏗️ 24 | run: npm run build 25 | 26 | - name: Release 🚀 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | run: npm run release 31 | -------------------------------------------------------------------------------- /src/features/glare/types.public.ts: -------------------------------------------------------------------------------- 1 | export type GlareProps = { 2 | /** 3 | * Enables/disables the glare effect. 4 | */ 5 | glareEnable?: boolean; 6 | /** 7 | * Maximum glare opacity (`0.5 = 50%, 1 = 100%`). Range: `0-1` 8 | */ 9 | glareMaxOpacity?: number; 10 | /** 11 | * Sets the color of the glare effect. 12 | */ 13 | glareColor?: string; 14 | /** 15 | * Sets the position of the glare effect. 16 | */ 17 | glarePosition?: GlarePosition; 18 | /** 19 | * Reverses the glare direction. 20 | */ 21 | glareReverse?: boolean; 22 | /** 23 | * Sets the border radius of the glare. Accepts any standard CSS border radius value. 24 | */ 25 | glareBorderRadius?: string; 26 | }; 27 | 28 | export type GlarePosition = 'top' | 'right' | 'bottom' | 'left' | 'all'; 29 | -------------------------------------------------------------------------------- /scripts/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "outDir": "../compiled", 5 | "paths": { 6 | "@/*": ["../src/*"] 7 | }, 8 | "importHelpers": true, 9 | "jsx": "react-jsx", 10 | "lib": ["ESNext", "DOM"], 11 | "module": "ESNext", 12 | "moduleResolution": "Bundler", 13 | "target": "ES2020", 14 | "allowSyntheticDefaultImports": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "resolveJsonModule": true, 18 | "pretty": true, 19 | "sourceMap": false, 20 | "strict": true, 21 | "stripInternal": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "skipLibCheck": true, 24 | "esModuleInterop": true, 25 | "types": ["vitest/globals"], 26 | "allowJs": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stories/FlipPage/FlipPage.demozap.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import Tilt from '@/index'; 4 | 5 | import './FlipPage.demozap.css'; 6 | import { Page } from './Page/Page'; 7 | 8 | const FlipPage = () => { 9 | const [[flipVertically, flipHorizontally], toggleFlip] = useState([false, false]); 10 | 11 | return ( 12 |
13 | 14 | toggleFlip([checked, flipHorizontally])} 18 | toggleFlipHorizontally={(checked) => toggleFlip([flipVertically, checked])} 19 | /> 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default FlipPage; 26 | -------------------------------------------------------------------------------- /stories/TiltScale/TiltScale.demozap.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import Tilt from '@/index'; 4 | import './TiltScale.demozap.css'; 5 | 6 | const TiltScale = () => { 7 | const [scale, setScale] = useState(1.15); 8 | 9 | return ( 10 | 11 |
12 |
13 |
Scale x{scale}
14 |
15 |
16 |
17 | setScale(parseFloat(ev.target.value))} 24 | /> 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default TiltScale; 32 | -------------------------------------------------------------------------------- /stories/TiltImg/_TiltImg.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './TiltImg.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import './TiltImg.demozap.css'; 8 | import imgNyc from './img/nyc.jpg'; 9 | 10 | const TiltImg = () => ( 11 | 20 | pic 21 | 22 | ); 23 | 24 | export default TiltImg; 25 | `; 26 | 27 | const style = `.tilt-img { 28 | .inner-element { 29 | width: 70vw; 30 | } 31 | } 32 | `; 33 | 34 | export const _TiltImg = () => ( 35 | 36 | 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /stories/ScaleNoTilt/ScaleNoTilt.demozap.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import Tilt from '@/index'; 4 | import './ScaleNoTilt.demozap.css'; 5 | 6 | const ScaleNoTilt = () => { 7 | const [scale, setScale] = useState(1.3); 8 | 9 | return ( 10 | 11 |
12 |
13 |
Scale x{scale}
14 |
15 |
16 |
17 | setScale(parseFloat(ev.target.value))} 24 | /> 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default ScaleNoTilt; 32 | -------------------------------------------------------------------------------- /src/react-parallax-tilt/types.ts: -------------------------------------------------------------------------------- 1 | import type { ElementSizePosition, ClientPosition } from '@/utils/types'; 2 | 3 | export type WrapperElement = { 4 | node: HTMLDivElement | null; 5 | size: ElementSizePosition; 6 | clientPosition: ClientPosition; 7 | updateAnimationId: number | null; 8 | scale: number; 9 | }; 10 | 11 | type DOMSupportedEvent = MouseEvent | React.MouseEvent | TouchEvent | React.TouchEvent | DeviceOrientationEvent; 12 | export type SupportedEvent = DOMSupportedEvent | CustomEvent; 13 | 14 | type DOMEventType = 'touchmove' | 'mousemove' | 'deviceorientation'; 15 | export type CustomEventType = 'autoreset' | 'initial' | 'propChange'; 16 | export type EventType = DOMEventType | CustomEventType; 17 | 18 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions 19 | export interface DeviceOrientationEventiOS extends DeviceOrientationEvent { 20 | requestPermission?: () => Promise<'granted' | 'denied'>; 21 | } 22 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltDisable.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent } from '@testing-library/react'; 2 | 3 | import type { OnMove, OnMoveParams } from '@/index'; 4 | import { TiltTest } from '@/utils/TiltTest'; 5 | 6 | describe('Tilt - Disable', () => { 7 | it('should not calculate tilt when disabled', () => { 8 | const onMove = vi.fn(); 9 | 10 | render(); 11 | 12 | fireEvent.touchMove(screen.getByText('test'), { touches: [{ pageX: 100, pageY: 50 }] }); 13 | 14 | expect(onMove).toHaveBeenLastCalledWith<[OnMoveParams]>({ 15 | tiltAngleX: 0, 16 | tiltAngleY: 0, 17 | tiltAngleXPercentage: 0, 18 | tiltAngleYPercentage: 0, 19 | glareAngle: 0, 20 | glareOpacity: 0, 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 22 | event: expect.objectContaining({ 23 | type: 'touchmove', 24 | }), 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltOnTouchMove.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent } from '@testing-library/react'; 2 | 3 | import type { OnMove, OnMoveParams } from '@/index'; 4 | import { TiltTest } from '@/utils/TiltTest'; 5 | 6 | describe('Tilt - onTouchMove', () => { 7 | it('should call onMove prop when onTouchMove event is triggered', () => { 8 | const onMove = vi.fn(); 9 | 10 | render(); 11 | 12 | fireEvent.touchMove(screen.getByText('test'), { touches: [{ pageX: 100, pageY: 50 }] }); 13 | 14 | expect(onMove).toHaveBeenLastCalledWith<[OnMoveParams]>({ 15 | tiltAngleX: 20, 16 | tiltAngleY: -20, 17 | tiltAngleXPercentage: 100, 18 | tiltAngleYPercentage: -100, 19 | glareAngle: 0, 20 | glareOpacity: 0, 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 22 | event: expect.objectContaining({ 23 | type: 'touchmove', 24 | }), 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /stories/MultipleTiltScroll/_MultipleTiltScroll.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './MultipleTiltScroll.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import './MultipleTiltScroll.demozap.css'; 8 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 9 | 10 | const MultipleTiltScroll = () => ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | ); 26 | 27 | export default MultipleTiltScroll; 28 | `; 29 | 30 | const style = `.multiple-tilt-scroll > * { 31 | margin-bottom: 200px; 32 | } 33 | `; 34 | 35 | export const _MultipleTiltScroll = () => ( 36 | 37 | 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltTrackOnWindow.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import type { OnMove, OnMoveParams } from '@/index'; 5 | import { TiltTest } from '@/utils/TiltTest'; 6 | 7 | describe('Tilt - Track On Window', () => { 8 | it('should calculate tilt when hover on window', async () => { 9 | const onMove = vi.fn(); 10 | 11 | render(); 12 | 13 | await userEvent.hover(screen.getByText('test')); 14 | 15 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 16 | tiltAngleX: 20, 17 | tiltAngleY: 15, 18 | tiltAngleXPercentage: 100, 19 | tiltAngleYPercentage: 75, 20 | glareAngle: 0, 21 | glareOpacity: 0, 22 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 23 | event: expect.objectContaining({ 24 | type: 'initial', 25 | }), 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy-storybook.yml: -------------------------------------------------------------------------------- 1 | name: deploy-storybook 2 | 3 | on: 4 | workflow_run: 5 | workflows: [test] 6 | types: [completed] 7 | branches: [main] 8 | 9 | env: 10 | FOLDER_PATH_STORYBOOK_BUILD: ./build-storybook-static 11 | 12 | jobs: 13 | deploy-storybook: 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 16 | 17 | environment: 18 | name: storybook-demos 19 | url: https://mkosir.github.io/react-parallax-tilt/?path=/story/react-parallax-tilt--event-params 20 | 21 | steps: 22 | - name: Checkout 🛎️ 23 | uses: actions/checkout@v6 24 | 25 | - name: Setup Node/npm ⚙️ 26 | uses: ./.github/workflows/actions/setup_node_npm 27 | 28 | - name: Build 🏗️ 29 | run: npm run storybook:build 30 | 31 | - name: Deploy 🚀 32 | uses: JamesIves/github-pages-deploy-action@v4 33 | with: 34 | branch: gh-pages 35 | folder: ${{ env.FOLDER_PATH_STORYBOOK_BUILD }} 36 | clean: true 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: push 4 | 5 | env: 6 | FOLDER_PATH_TEST_UNIT_REPORT: ./test-unit-report 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@v6 15 | 16 | - name: Setup Node/npm ⚙️ 17 | uses: ./.github/workflows/actions/setup_node_npm 18 | 19 | - name: Test 🧪 20 | run: npm run test:report 21 | 22 | - name: Upload report 📈 23 | uses: nwtgck/actions-netlify@v3 24 | with: 25 | publish-dir: ${{ env.FOLDER_PATH_TEST_UNIT_REPORT }} 26 | production-deploy: true 27 | env: 28 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 29 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_TEST_UNIT_REPORT }} 30 | 31 | - name: Upload coverage to Codecov 📶 32 | uses: codecov/codecov-action@v5 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | fail_ci_if_error: true 36 | verbose: true 37 | -------------------------------------------------------------------------------- /.github/workflows/test-e2e.yml: -------------------------------------------------------------------------------- 1 | name: test-e2e 2 | 3 | on: push 4 | 5 | env: 6 | FOLDER_PATH_TEST_E2E_REPORT: ./test-e2e-report 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@v6 15 | 16 | - name: Setup Node/npm ⚙️ 17 | uses: ./.github/workflows/actions/setup_node_npm 18 | 19 | - name: Install Playwright Browsers 20 | run: npx playwright install --with-deps 21 | 22 | - name: Test E2E 🧪 23 | run: npx playwright test 24 | # run: npm run test:e2e 25 | 26 | - name: Upload report 📈 27 | uses: nwtgck/actions-netlify@v3 28 | # Upload test report also in case of test failure. 29 | if: always() 30 | with: 31 | publish-dir: ${{ env.FOLDER_PATH_TEST_E2E_REPORT }} 32 | production-deploy: true 33 | env: 34 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 35 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_TEST_E2E_REPORT }} 36 | -------------------------------------------------------------------------------- /src/features/tilt/types.public.ts: -------------------------------------------------------------------------------- 1 | export type TiltProps = { 2 | /** 3 | * Enables/disables the tilt effect. 4 | */ 5 | tiltEnable?: boolean; 6 | /** 7 | * Reverses the tilt direction. 8 | */ 9 | tiltReverse?: boolean; 10 | /** 11 | * Initial tilt angle (in degrees) on the x-axis. 12 | */ 13 | tiltAngleXInitial?: number; 14 | /** 15 | * Initial tilt angle (in degrees) on the y-axis. 16 | */ 17 | tiltAngleYInitial?: number; 18 | /** 19 | * Maximum tilt rotation (in degrees) on the x-axis (range: `0°-90°`). 20 | */ 21 | tiltMaxAngleX?: number; 22 | /** 23 | * Maximum tilt rotation (in degrees) on the y-axis (range: `0°-90°`). 24 | */ 25 | tiltMaxAngleY?: number; 26 | /** 27 | * Enables tilt on a single axis only. 28 | */ 29 | tiltAxis?: Axis; 30 | /** 31 | * Manual tilt rotation (in degrees) on the x-axis. 32 | */ 33 | tiltAngleXManual?: number | null; 34 | /** 35 | * Manual tilt rotation (in degrees) on the y-axis. 36 | */ 37 | tiltAngleYManual?: number | null; 38 | }; 39 | 40 | export type Axis = 'x' | 'y'; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marko Kosir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/react-parallax-tilt/defaultProps.ts: -------------------------------------------------------------------------------- 1 | import type { GlareProps } from '@/features/glare/types.public'; 2 | import type { TiltProps } from '@/features/tilt/types.public'; 3 | 4 | import type { ReactParallaxTiltProps } from './types.public'; 5 | 6 | const defaultGlareProps: GlareProps = { 7 | glareEnable: false, 8 | glareMaxOpacity: 0.7, 9 | glareColor: '#ffffff', 10 | glarePosition: 'bottom', 11 | glareReverse: false, 12 | glareBorderRadius: '0', 13 | }; 14 | 15 | const defaultTiltProps: TiltProps = { 16 | tiltEnable: true, 17 | tiltReverse: false, 18 | tiltAngleXInitial: 0, 19 | tiltAngleYInitial: 0, 20 | tiltMaxAngleX: 20, 21 | tiltMaxAngleY: 20, 22 | tiltAxis: undefined, 23 | tiltAngleXManual: null, 24 | tiltAngleYManual: null, 25 | }; 26 | 27 | export const defaultProps: ReactParallaxTiltProps = { 28 | scale: 1, 29 | perspective: 1000, 30 | flipVertically: false, 31 | flipHorizontally: false, 32 | reset: true, 33 | transitionEasing: 'cubic-bezier(.03,.98,.52,.99)', 34 | transitionSpeed: 400, 35 | trackOnWindow: false, 36 | gyroscope: false, 37 | ...defaultTiltProps, 38 | ...defaultGlareProps, 39 | }; 40 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltMaxAngle.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import type { OnMove, OnMoveParams } from '@/index'; 5 | import { TiltTest } from '@/utils/TiltTest'; 6 | 7 | describe('Tilt - Max Angle', () => { 8 | it('should constrain tilt angles to default constant when hover on element', async () => { 9 | const onMove = vi.fn(); 10 | 11 | render( 12 | , 19 | ); 20 | 21 | await userEvent.hover(screen.getByText('test')); 22 | 23 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 24 | tiltAngleX: 90, 25 | tiltAngleY: 90, 26 | tiltAngleXPercentage: 30, 27 | tiltAngleYPercentage: 30, 28 | glareAngle: 0, 29 | glareOpacity: 0, 30 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 31 | event: expect.objectContaining({ 32 | type: 'initial', 33 | }), 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltReverse.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import type { OnMove, OnMoveParams } from '@/index'; 5 | import { TiltTest } from '@/utils/TiltTest'; 6 | 7 | describe('Tilt - Reverse', () => { 8 | it('should calculate reverse tilt when hover on element', async () => { 9 | const onMove = vi.fn(); 10 | 11 | render( 12 | , 20 | ); 21 | 22 | await userEvent.hover(screen.getByText('test')); 23 | 24 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 25 | tiltAngleX: -60, 26 | tiltAngleY: -45, 27 | tiltAngleXPercentage: -100, 28 | tiltAngleYPercentage: -75, 29 | glareAngle: 0, 30 | glareOpacity: 0, 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 32 | event: expect.objectContaining({ 33 | type: 'initial', 34 | }), 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent } from '@testing-library/react'; 2 | 3 | import type { OnMove } from '@/index'; 4 | import { TiltTest } from '@/utils/TiltTest'; 5 | 6 | describe('Tilt - Style', () => { 7 | it('should update tilt style when hover on element', () => { 8 | vi.useFakeTimers(); 9 | 10 | const onMove = vi.fn(); 11 | 12 | const { container } = render(); 13 | 14 | const positionStart = [{ pageX: 50, pageY: 50 }]; 15 | const positionEnd = [{ pageX: 100, pageY: 100 }]; 16 | 17 | const testElement = screen.getByText('test'); 18 | fireEvent.touchStart(testElement, { touches: positionStart }); 19 | fireEvent.touchMove(testElement, { touches: positionEnd }); 20 | 21 | vi.runAllTimers(); 22 | 23 | // eslint-disable-next-line testing-library/no-node-access 24 | const tiltElement = container.firstElementChild; 25 | expect(tiltElement).toHaveStyle({ 26 | 'will-change': 'transform', 27 | transition: 'all 400ms cubic-bezier(.03,.98,.52,.99)', 28 | transform: 'perspective(1000px) rotateX(20deg) rotateY(-20deg) scale3d(1,1,1)', 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /stories/MultipleTilt/_MultipleTilt.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './MultipleTilt.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import './MultipleTilt.demozap.css'; 8 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 9 | 10 | const MultipleTilt = () => ( 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | ); 30 | 31 | export default MultipleTilt; 32 | `; 33 | 34 | const style = `.multiple-tilt { 35 | display: flex; 36 | flex-direction: row; 37 | 38 | .col { 39 | margin-right: 20px; 40 | 41 | :first-child { 42 | margin-bottom: 20px; 43 | } 44 | } 45 | } 46 | `; 47 | 48 | export const _MultipleTilt = () => ( 49 | 50 | 51 | 52 | ); 53 | -------------------------------------------------------------------------------- /stories/FlipVH/FlipVH.demozap.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import Tilt from '@/index'; 4 | import './FlipVH.demozap.css'; 5 | 6 | const FlipVH = () => { 7 | const [[flipVertically, flipHorizontally], toggleFlip] = useState([false, false]); 8 | 9 | return ( 10 | 11 |
12 |
13 |
Toggle Axis
14 |
15 |
16 |
17 | 25 | 33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default FlipVH; 40 | -------------------------------------------------------------------------------- /stories/ParallaxEffectImg/_ParallaxEffectImg.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './ParallaxEffectImg.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | 7 | import './ParallaxEffectImg.demozap.css'; 8 | import imgTree from './img/tree.png'; 9 | 10 | const ParallaxEffectImg = () => ( 11 | 20 | pic 21 | 22 | ); 23 | 24 | export default ParallaxEffectImg; 25 | `; 26 | 27 | const style = `.parallax-effect-img { 28 | transform-style: preserve-3d; 29 | transform: perspective(1000px); 30 | background-image: url('./img/background.jpg'); 31 | background-size: contain; 32 | background-repeat: no-repeat; 33 | 34 | .inner-element { 35 | transform: translateZ(40px) scale(0.8); 36 | width: 70%; 37 | margin-left: 25%; 38 | } 39 | } 40 | `; 41 | 42 | export const _ParallaxEffectImg = () => ( 43 | 44 | 45 | 46 | ); 47 | -------------------------------------------------------------------------------- /stories/TiltDisableAxis/TiltDisableAxis.demozap.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import type { Axis } from '@/index'; 4 | import Tilt from '@/index'; 5 | 6 | import './TiltDisableAxis.demozap.css'; 7 | 8 | const TiltDisableAxis = () => { 9 | const [axisEnabled, toggleAxis] = useState('x'); 10 | 11 | return ( 12 | 13 |
14 |
15 |
Toggle Axis
16 |
17 |
18 |
19 | 28 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default TiltDisableAxis; 44 | -------------------------------------------------------------------------------- /stories/TiltManualInput/TiltManualInput.demozap.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Joystick } from 'react-joystick-component'; 3 | import type { IJoystickUpdateEvent } from 'react-joystick-component/build/lib/Joystick'; 4 | 5 | import Tilt from '@/index'; 6 | import './TiltManualInput.demozap.css'; 7 | 8 | const TiltManualInput = () => { 9 | const [[manualTiltAngleX, manualTiltAngleY], setManualTiltAngle] = useState([0, 0]); 10 | 11 | const onMove = (stick: IJoystickUpdateEvent) => { 12 | setManualTiltAngle([stick.y ? stick.y * 100 : 0, stick.x ? stick.x * 100 : 0]); 13 | }; 14 | 15 | const onStop = () => { 16 | setManualTiltAngle([0, 0]); 17 | }; 18 | 19 | return ( 20 |
21 | 22 |
23 |
Axis x: {manualTiltAngleX.toFixed(0)}°
24 |
Axis y: {manualTiltAngleY.toFixed(0)}°
25 |
26 |
27 |
28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default TiltManualInput; 35 | -------------------------------------------------------------------------------- /stories/EventTiltAngle/EventTiltAngle.demozap.tsx: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react'; 2 | 3 | import type { OnMoveParams } from '@/index'; 4 | import Tilt from '@/index'; 5 | 6 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 7 | import './EventTiltAngle.demozap.css'; 8 | 9 | class EventTiltAngle extends PureComponent { 10 | state = { 11 | tiltAngleX: 0, 12 | tiltAngleY: 0, 13 | }; 14 | 15 | onMove = ({ tiltAngleX, tiltAngleY }: OnMoveParams) => { 16 | this.setState({ tiltAngleX, tiltAngleY }); 17 | }; 18 | 19 | render() { 20 | const { tiltAngleX, tiltAngleY } = this.state; 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 |
29 | Component tilt: 30 |
31 | x axis:
{tiltAngleX.toFixed(2)}°
32 |
33 |
34 | y axis:
{tiltAngleY.toFixed(2)}°
35 |
36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default EventTiltAngle; 44 | -------------------------------------------------------------------------------- /stories/FlipPage/_FlipPage.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './FlipPage.demozap'; 4 | 5 | const code = `import { useState } from 'react'; 6 | 7 | import Tilt from '@/index'; 8 | 9 | import './FlipPage.demozap.css'; 10 | import { Page } from './Page/Page'; 11 | 12 | const FlipPage = () => { 13 | const [[flipVertically, flipHorizontally], toggleFlip] = useState([false, false]); 14 | 15 | return ( 16 |
17 | 18 | toggleFlip([checked, flipHorizontally])} 22 | toggleFlipHorizontally={(checked) => toggleFlip([flipVertically, checked])} 23 | /> 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default FlipPage; 30 | `; 31 | 32 | const style = `body { 33 | overflow-x: hidden; 34 | } 35 | 36 | .flip-page { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | 42 | width: 80vw; 43 | } 44 | `; 45 | 46 | export const _FlipPage = () => ( 47 | 48 | 49 | 50 | ); 51 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltReset.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent } from '@testing-library/react'; 2 | 3 | import type { OnEnter, OnLeave, OnMove, OnMoveParams } from '@/index'; 4 | import { TiltTest } from '@/utils/TiltTest'; 5 | 6 | describe('Tilt - Reset', () => { 7 | it('should not reset tilt when mouse leave an element', () => { 8 | const onEnter = vi.fn(); 9 | const onMove = vi.fn(); 10 | const onLeave = vi.fn(); 11 | 12 | render(); 13 | 14 | fireEvent.mouseEnter(screen.getByText('test')); 15 | const onEnterParams = onEnter.mock.calls[0][0]; 16 | expect(onEnterParams.event.type).toBe('mouseenter'); 17 | 18 | expect(onMove).toHaveBeenCalledExactlyOnceWith<[OnMoveParams]>({ 19 | tiltAngleX: 0, 20 | tiltAngleY: -0, 21 | tiltAngleXPercentage: 0, 22 | tiltAngleYPercentage: -0, 23 | glareAngle: 0, 24 | glareOpacity: 0, 25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 26 | event: expect.objectContaining({ 27 | type: 'initial', 28 | }), 29 | }); 30 | 31 | fireEvent.mouseLeave(screen.getByText('test')); 32 | const onLeaveParams = onLeave.mock.calls[0][0]; 33 | expect(onLeaveParams.event.type).toBe('mouseleave'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /stories/ParallaxEffect/_ParallaxEffect.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './ParallaxEffect.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | import './ParallaxEffect.demozap.css'; 7 | 8 | const ParallaxEffect = () => ( 9 | 10 |
11 |
React
12 |
Parallax Tilt
13 |
👀
14 |
15 |
16 | ); 17 | 18 | export default ParallaxEffect; 19 | `; 20 | 21 | const style = `@import '../ReactParallaxTilt.css'; 22 | 23 | .parallax-effect { 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: center; 27 | align-items: center; 28 | width: 300px; 29 | height: 300px; 30 | background-color: darkgreen; 31 | color: white; 32 | border: 5px solid black; 33 | border-radius: 20px; 34 | 35 | transform-style: preserve-3d; 36 | 37 | .inner-element { 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: center; 41 | align-items: center; 42 | font-size: 35px; 43 | font-style: italic; 44 | color: white; 45 | transform: translateZ(60px); 46 | } 47 | } 48 | `; 49 | 50 | export const _ParallaxEffect = () => ( 51 | 52 | 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /stories/EventParams/EventParams.demozap.css: -------------------------------------------------------------------------------- 1 | @import '../ReactParallaxTilt.css'; 2 | 3 | .event-params { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | font-size: 20px; 9 | 10 | .tilt-content { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | width: 350px; 16 | height: 350px; 17 | font-size: 28px; 18 | font-style: italic; 19 | background-color: darkgreen; 20 | color: white; 21 | border: 5px solid black; 22 | border-radius: 20px; 23 | text-align: center; 24 | 25 | .param { 26 | margin-top: 12px; 27 | border-top: 2px solid white; 28 | min-width: 220px; 29 | 30 | .header { 31 | font-size: 35px; 32 | } 33 | } 34 | 35 | .test-params { 36 | display: none; 37 | } 38 | 39 | .test-ref { 40 | position: absolute; 41 | width: 30px; 42 | height: 30px; 43 | background-color: transparent; 44 | } 45 | .top-left { 46 | top: -10px; 47 | left: -10px; 48 | } 49 | .top-mid-left { 50 | top: 80px; 51 | left: 80px; 52 | } 53 | .top-right { 54 | top: -10px; 55 | right: -10px; 56 | } 57 | .bottom-right { 58 | bottom: -10px; 59 | right: -10px; 60 | } 61 | .bottom-left { 62 | bottom: -11px; 63 | left: -11px; 64 | } 65 | } 66 | 67 | .event-type { 68 | display: flex; 69 | margin-top: 20px; 70 | margin-bottom: 10px; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /stories/TrackOnWindow/_TrackOnWindow.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './TrackOnWindow.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | import './TrackOnWindow.demozap.css'; 7 | 8 | const TrackOnWindow = () => ( 9 | 19 |
20 |
React
21 |
Parallax Tilt
22 |
👀
23 |
24 |
25 | ); 26 | 27 | export default TrackOnWindow; 28 | `; 29 | 30 | const style = `@import '../ReactParallaxTilt.css'; 31 | 32 | .track-on-window { 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | align-items: center; 37 | width: 300px; 38 | height: 300px; 39 | background-color: darkgreen; 40 | color: white; 41 | border: 5px solid black; 42 | border-radius: 20px; 43 | 44 | transform-style: preserve-3d; 45 | 46 | .inner-element { 47 | display: flex; 48 | flex-direction: column; 49 | justify-content: center; 50 | align-items: center; 51 | font-size: 35px; 52 | font-style: italic; 53 | color: white; 54 | transform: translateZ(60px); 55 | } 56 | } 57 | `; 58 | 59 | export const _TrackOnWindow = () => ( 60 | 61 | 62 | 63 | ); 64 | -------------------------------------------------------------------------------- /stories/ParallaxEffectGlareScale/_ParallaxEffectGlareScale.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './ParallaxEffectGlareScale.demozap'; 4 | 5 | const code = `import Tilt from '@/index'; 6 | import './ParallaxEffectGlareScale.demozap.css'; 7 | 8 | const ParallaxEffectGlareScale = () => ( 9 | 16 |
17 |
React
18 |
Parallax Tilt
19 |
👀
20 |
21 |
22 | ); 23 | 24 | export default ParallaxEffectGlareScale; 25 | `; 26 | 27 | const style = `@import '../ReactParallaxTilt.css'; 28 | 29 | .parallax-effect-glare-scale { 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | align-items: center; 34 | width: 300px; 35 | height: 300px; 36 | background-color: darkgreen; 37 | color: white; 38 | border: 5px solid black; 39 | border-radius: 20px; 40 | 41 | transform-style: preserve-3d; 42 | 43 | .inner-element { 44 | display: flex; 45 | flex-direction: column; 46 | justify-content: center; 47 | align-items: center; 48 | font-size: 35px; 49 | font-style: italic; 50 | color: white; 51 | transform: translateZ(60px); 52 | } 53 | } 54 | `; 55 | 56 | export const _ParallaxEffectGlareScale = () => ( 57 | 58 | 59 | 60 | ); 61 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltOnPropChange.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import type { OnMove, OnMoveParams } from '@/index'; 5 | import { TiltTest } from '@/utils/TiltTest'; 6 | 7 | describe('Tilt - Prop change', () => { 8 | it('should re-calculate tilt when manual tilt angle y prop changes', async () => { 9 | const onMove = vi.fn(); 10 | 11 | const { rerender } = render( 12 | , 13 | ); 14 | 15 | await userEvent.hover(screen.getByText('test')); 16 | 17 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 18 | tiltAngleX: 60, 19 | tiltAngleY: 45, 20 | tiltAngleXPercentage: 100, 21 | tiltAngleYPercentage: 75, 22 | glareAngle: 0, 23 | glareOpacity: 0, 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 25 | event: expect.objectContaining({ 26 | type: 'initial', 27 | }), 28 | }); 29 | 30 | const onMoveRerender = vi.fn(); 31 | 32 | rerender( 33 | , 40 | ); 41 | 42 | expect(onMoveRerender).toHaveBeenCalledExactlyOnceWith<[OnMoveParams]>({ 43 | tiltAngleX: 60, 44 | tiltAngleY: 30, 45 | tiltAngleXPercentage: 100, 46 | tiltAngleYPercentage: 50, 47 | glareAngle: 0, 48 | glareOpacity: 0, 49 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 50 | event: expect.objectContaining({ 51 | type: 'propChange', 52 | }), 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /stories/TiltScale/_TiltScale.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './TiltScale.demozap'; 4 | 5 | const code = `import { useState } from 'react'; 6 | 7 | import Tilt from '@/index'; 8 | import './TiltScale.demozap.css'; 9 | 10 | const TiltScale = () => { 11 | const [scale, setScale] = useState(1.15); 12 | 13 | return ( 14 | 15 |
16 |
17 |
Scale x{scale}
18 |
19 |
20 |
21 | setScale(parseFloat(ev.target.value))} 28 | /> 29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default TiltScale; 36 | `; 37 | 38 | const style = `@import '../ReactParallaxTilt.css'; 39 | 40 | .tilt-scale { 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: center; 44 | align-items: center; 45 | width: 300px; 46 | height: 300px; 47 | font-size: 30px; 48 | font-style: italic; 49 | background-color: darkgreen; 50 | color: white; 51 | border: 5px solid black; 52 | border-radius: 20px; 53 | 54 | .header { 55 | font-size: 38px; 56 | } 57 | 58 | .form { 59 | display: flex; 60 | flex-direction: column; 61 | align-items: flex-start; 62 | 63 | input { 64 | transform: scale(1.5); 65 | height: 20px; 66 | margin-right: 7px; 67 | } 68 | } 69 | } 70 | `; 71 | 72 | export const _TiltScale = () => ( 73 | 74 | 75 | 76 | ); 77 | -------------------------------------------------------------------------------- /stories/ScaleNoTilt/_ScaleNoTilt.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './ScaleNoTilt.demozap'; 4 | 5 | const code = `import { useState } from 'react'; 6 | 7 | import Tilt from '@/index'; 8 | import './ScaleNoTilt.demozap.css'; 9 | 10 | const ScaleNoTilt = () => { 11 | const [scale, setScale] = useState(1.3); 12 | 13 | return ( 14 | 15 |
16 |
17 |
Scale x{scale}
18 |
19 |
20 |
21 | setScale(parseFloat(ev.target.value))} 28 | /> 29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default ScaleNoTilt; 36 | `; 37 | 38 | const style = `@import '../ReactParallaxTilt.css'; 39 | 40 | .scale-no-tilt { 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: center; 44 | align-items: center; 45 | width: 300px; 46 | height: 300px; 47 | font-size: 30px; 48 | font-style: italic; 49 | background-color: darkgreen; 50 | color: white; 51 | border: 5px solid black; 52 | border-radius: 20px; 53 | .header { 54 | font-size: 38px; 55 | } 56 | .form { 57 | display: flex; 58 | flex-direction: column; 59 | align-items: flex-start; 60 | input { 61 | transform: scale(1.5); 62 | height: 20px; 63 | margin-right: 7px; 64 | } 65 | } 66 | } 67 | `; 68 | 69 | export const _ScaleNoTilt = () => ( 70 | 71 | 72 | 73 | ); 74 | -------------------------------------------------------------------------------- /src/features/glare/test/glareStyle.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent } from '@testing-library/react'; 2 | 3 | import type { OnMove } from '@/index'; 4 | import { TiltTest } from '@/utils/TiltTest'; 5 | 6 | describe('Glare - Style', () => { 7 | it('should update glare style when hover on element', () => { 8 | vi.useFakeTimers(); 9 | 10 | const onMove = vi.fn(); 11 | 12 | const { container } = render(); 13 | 14 | const positionStart = [{ pageX: 50, pageY: 50 }]; 15 | const positionEnd = [{ pageX: 100, pageY: 100 }]; 16 | 17 | const testElement = screen.getByText('test'); 18 | fireEvent.touchStart(testElement, { touches: positionStart }); 19 | fireEvent.touchMove(testElement, { touches: positionEnd }); 20 | 21 | vi.runAllTimers(); 22 | 23 | // eslint-disable-next-line 24 | const tiltElement = container.firstElementChild as Element; 25 | 26 | // eslint-disable-next-line 27 | const glareWrapperElement = tiltElement.lastElementChild as Element; 28 | expect(glareWrapperElement).toHaveStyle({ 29 | position: 'absolute', 30 | top: '0px', 31 | left: '0px', 32 | width: '100%', 33 | height: '100%', 34 | overflow: 'hidden', 35 | 'border-radius': 0, 36 | 'pointer-events': 'none', 37 | }); 38 | expect(glareWrapperElement).toHaveClass('glare-wrapper'); 39 | 40 | // eslint-disable-next-line 41 | const glareElement = glareWrapperElement.firstElementChild; 42 | expect(glareElement).toHaveStyle({ 43 | position: 'absolute', 44 | top: '50%', 45 | left: '50%', 46 | 'transform-origin': '0% 0%', 47 | 'pointer-events': 'none', 48 | width: '0px', 49 | height: '0px', 50 | transition: 'opacity 400ms cubic-bezier(.03,.98,.52,.99)', 51 | transform: 'rotate(135deg) translate(-50%, -50%)', 52 | opacity: 0.7, 53 | }); 54 | expect(glareElement).toHaveClass('glare'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltAxis.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import type { OnMove, OnMoveParams } from '@/index'; 5 | import { TiltTest } from '@/utils/TiltTest'; 6 | 7 | describe('Tilt - Axis', () => { 8 | it('should disable y axis when only x tilt axis prop is enabled', async () => { 9 | const onMove = vi.fn(); 10 | 11 | render( 12 | , 20 | ); 21 | 22 | await userEvent.hover(screen.getByText('test')); 23 | 24 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 25 | tiltAngleX: 60, 26 | tiltAngleY: 0, 27 | tiltAngleXPercentage: 100, 28 | tiltAngleYPercentage: 0, 29 | glareAngle: 0, 30 | glareOpacity: 0, 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 32 | event: expect.objectContaining({ 33 | type: 'initial', 34 | }), 35 | }); 36 | }); 37 | 38 | it('should disable x axis when only y tilt axis prop is enabled', async () => { 39 | const onMove = vi.fn(); 40 | 41 | render( 42 | , 50 | ); 51 | 52 | await userEvent.hover(screen.getByText('test')); 53 | 54 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 55 | tiltAngleX: 0, 56 | tiltAngleY: 45, 57 | tiltAngleXPercentage: 0, 58 | tiltAngleYPercentage: 75, 59 | glareAngle: 0, 60 | glareOpacity: 0, 61 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 62 | event: expect.objectContaining({ 63 | type: 'initial', 64 | }), 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /e2e/events.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | import { getIframeContent } from './consts'; 4 | 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('/'); 7 | await page.getByRole('button', { name: 'Event - Params' }).click(); 8 | }); 9 | 10 | test("should trigger 'onMove' event with 'mousemove' event type when mouse hovers tilt element", async ({ page }) => { 11 | const content = getIframeContent(page); 12 | 13 | await content.getByTestId('topMidLeft').hover({ position: { x: 10, y: 10 } }); 14 | await expect(content.getByTestId('evenDescription')).toHaveText( 15 | "Event 'onMove' triggered by 'mousemove' event type.", 16 | ); 17 | }); 18 | 19 | test("should trigger 'onMove' event with 'autoreset' event type when mouse leaves tilt element", async ({ page }) => { 20 | const content = getIframeContent(page); 21 | 22 | await content.getByTestId('topMidLeft').hover({ position: { x: 10, y: 10 } }); 23 | await content.getByText('Track events:').hover(); 24 | await expect(content.getByTestId('evenDescription')).toHaveText( 25 | "Event 'onMove' triggered by 'autoreset' event type.", 26 | ); 27 | }); 28 | 29 | test("should trigger 'onEnter' event with 'mouseenter' event type when mouse enters tilt element", async ({ page }) => { 30 | const content = getIframeContent(page); 31 | 32 | await content.getByLabel('onMove').uncheck(); 33 | await content.getByTestId('topMidLeft').hover({ position: { x: 10, y: 10 } }); 34 | await expect(content.getByTestId('evenDescription')).toHaveText( 35 | "Event 'onEnter' triggered by 'mouseenter' event type.", 36 | ); 37 | }); 38 | 39 | test("should trigger 'onLeave' event with 'mouseleave' event type when mouse enters tilt element", async ({ page }) => { 40 | const content = getIframeContent(page); 41 | 42 | await content.getByLabel('onMove').uncheck(); 43 | await content.getByTestId('topMidLeft').hover({ position: { x: 10, y: 10 } }); 44 | await content.getByText('Track events:').hover(); 45 | await expect(content.getByTestId('evenDescription')).toHaveText( 46 | "Event 'onLeave' triggered by 'mouseleave' event type.", 47 | ); 48 | }); 49 | -------------------------------------------------------------------------------- /stories/EventTiltAngle/_EventTiltAngle.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './EventTiltAngle.demozap'; 4 | 5 | const code = `import { PureComponent } from 'react'; 6 | 7 | import type { OnMoveParams } from '@/index'; 8 | import Tilt from '@/index'; 9 | 10 | import { DefaultComponent } from '../_DefaultComponent/DefaultComponent'; 11 | import './EventTiltAngle.demozap.css'; 12 | 13 | class EventTiltAngle extends PureComponent { 14 | state = { 15 | tiltAngleX: 0, 16 | tiltAngleY: 0, 17 | }; 18 | 19 | onMove = ({ tiltAngleX, tiltAngleY }: OnMoveParams) => { 20 | this.setState({ tiltAngleX, tiltAngleY }); 21 | }; 22 | 23 | render() { 24 | const { tiltAngleX, tiltAngleY } = this.state; 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 |
33 | Component tilt: 34 |
35 | x axis:
{tiltAngleX.toFixed(2)}°
36 |
37 |
38 | y axis:
{tiltAngleY.toFixed(2)}°
39 |
40 |
41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | export default EventTiltAngle; 48 | `; 49 | 50 | const style = `.event-tilt-angle { 51 | display: flex; 52 | flex-direction: column; 53 | justify-content: center; 54 | align-items: center; 55 | 56 | .component-tilt { 57 | font-style: italic; 58 | margin-top: 30px; 59 | font-size: 24px; 60 | text-align: center; 61 | border: 2px solid black; 62 | border-radius: 8px; 63 | padding: 10px; 64 | 65 | .row { 66 | display: flex; 67 | flex-direction: row; 68 | justify-content: space-between; 69 | align-items: center; 70 | margin-top: 10px; 71 | } 72 | } 73 | } 74 | `; 75 | 76 | export const _EventTiltAngle = () => ( 77 | 78 | 79 | 80 | ); 81 | -------------------------------------------------------------------------------- /stories/FlipVH/_FlipVH.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './FlipVH.demozap'; 4 | 5 | const code = `import { useState } from 'react'; 6 | 7 | import Tilt from '@/index'; 8 | import './FlipVH.demozap.css'; 9 | 10 | const FlipVH = () => { 11 | const [[flipVertically, flipHorizontally], toggleFlip] = useState([false, false]); 12 | 13 | return ( 14 | 15 |
16 |
17 |
Toggle Axis
18 |
19 |
20 |
21 | 29 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default FlipVH; 44 | `; 45 | 46 | const style = `@import '../ReactParallaxTilt.css'; 47 | 48 | .flip-vh { 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: center; 52 | align-items: center; 53 | width: 300px; 54 | height: 300px; 55 | font-size: 30px; 56 | font-style: italic; 57 | background-color: darkgreen; 58 | color: white; 59 | border: 5px solid black; 60 | border-radius: 20px; 61 | .header { 62 | font-size: 38px; 63 | } 64 | .form { 65 | display: flex; 66 | flex-direction: column; 67 | align-items: flex-start; 68 | input { 69 | transform: scale(1.5); 70 | height: 20px; 71 | margin-right: 7px; 72 | } 73 | } 74 | } 75 | `; 76 | 77 | export const _FlipVH = () => ( 78 | 79 | 80 | 81 | ); 82 | -------------------------------------------------------------------------------- /stories/FlipPage/Page/Page.css: -------------------------------------------------------------------------------- 1 | .page { 2 | font-size: 13px; 3 | 4 | ul { 5 | list-style-type: none; 6 | margin: 0; 7 | padding: 0; 8 | overflow: hidden; 9 | background-color: #333; 10 | font-size: 1.1em; 11 | 12 | li { 13 | float: left; 14 | } 15 | 16 | li { 17 | display: block; 18 | color: white; 19 | text-align: center; 20 | padding: 14px 16px; 21 | text-decoration: none; 22 | } 23 | 24 | li:hover { 25 | background-color: #111; 26 | cursor: pointer; 27 | } 28 | } 29 | 30 | .content { 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | align-items: center; 35 | margin: 15px; 36 | text-align: justify; 37 | 38 | .controls { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: center; 43 | border: 2px solid black; 44 | border-radius: 10px; 45 | padding: 10px; 46 | margin-bottom: 30px; 47 | 48 | .title { 49 | font-size: 22px; 50 | } 51 | .form { 52 | display: flex; 53 | align-items: flex-start; 54 | input { 55 | transform: scale(1.2); 56 | height: 13px; 57 | margin-right: 7px; 58 | margin-left: 20px; 59 | } 60 | } 61 | } 62 | 63 | img { 64 | width: 400px; 65 | margin: 10px; 66 | } 67 | } 68 | 69 | .contact-form { 70 | margin: 10px 15px; 71 | 72 | .field { 73 | margin-bottom: 10px; 74 | } 75 | 76 | input[type='text'], 77 | textarea { 78 | width: 50%; 79 | padding: 8px; 80 | border: 1px solid #ccc; 81 | resize: vertical; 82 | } 83 | 84 | input[type='submit'] { 85 | background-color: #333; 86 | color: white; 87 | padding: 12px 20px; 88 | border: none; 89 | cursor: pointer; 90 | } 91 | 92 | input[type='submit']:hover { 93 | background-color: #111; 94 | } 95 | } 96 | 97 | .footer { 98 | text-align: center; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | const IS_CI = process.env.CI === 'true'; 4 | const BASE_URL = 'http://localhost:9009'; 5 | const TIMEOUT_SECONDS = IS_CI ? 15 : 5; 6 | 7 | // eslint-disable-next-line import-x/no-default-export 8 | export default defineConfig({ 9 | testDir: './e2e', 10 | fullyParallel: true, 11 | forbidOnly: IS_CI, 12 | retries: IS_CI ? 2 : 0, 13 | maxFailures: 1, 14 | workers: IS_CI ? 1 : undefined, 15 | outputDir: 'test-e2e-results', 16 | reporter: [['list'], ['html', { outputFolder: 'test-e2e-report' }]], 17 | timeout: TIMEOUT_SECONDS * 1000, 18 | use: { 19 | baseURL: BASE_URL, 20 | trace: 'on', 21 | }, 22 | 23 | projects: [ 24 | { 25 | name: 'setup', 26 | testMatch: /global.setup\.ts/, 27 | }, 28 | { 29 | name: 'chromium', 30 | use: { ...devices['Desktop Chrome'] }, 31 | dependencies: ['setup'], 32 | }, 33 | 34 | { 35 | name: 'webkit', 36 | use: { ...devices['Desktop Safari'] }, 37 | dependencies: ['setup'], 38 | }, 39 | 40 | // Disable Firefox 41 | // { 42 | // name: 'firefox', 43 | // use: { ...devices['Desktop Firefox'] }, 44 | // dependencies: ['setup'], 45 | // }, 46 | 47 | /* Test against mobile viewports. */ 48 | // { 49 | // name: 'Mobile Chrome', 50 | // use: { ...devices['Pixel 5'] }, 51 | // dependencies: ['setup'], 52 | // }, 53 | // { 54 | // name: 'Mobile Safari', 55 | // use: { ...devices['iPhone 12'] }, 56 | // dependencies: ['setup'], 57 | // }, 58 | 59 | /* Test against branded browsers. */ 60 | // { 61 | // name: 'Microsoft Edge', 62 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 63 | // dependencies: ['setup'], 64 | // }, 65 | // { 66 | // name: 'Google Chrome', 67 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 68 | // dependencies: ['setup'], 69 | // }, 70 | ], 71 | 72 | /* Run dev server before starting the tests */ 73 | webServer: { 74 | command: 'npm run start', 75 | url: BASE_URL, 76 | // reuseExistingServer: !IS_CI, 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /stories/TiltDisableAxis/_TiltDisableAxis.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './TiltDisableAxis.demozap'; 4 | 5 | const code = `import { useState } from 'react'; 6 | 7 | import type { Axis } from '@/index'; 8 | import Tilt from '@/index'; 9 | 10 | import './TiltDisableAxis.demozap.css'; 11 | 12 | const TiltDisableAxis = () => { 13 | const [axisEnabled, toggleAxis] = useState('x'); 14 | 15 | return ( 16 | 17 |
18 |
19 |
Toggle Axis
20 |
21 |
22 |
23 | 32 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | export default TiltDisableAxis; 48 | `; 49 | 50 | const style = `@import '../ReactParallaxTilt.css'; 51 | 52 | .tilt-disable-axis { 53 | display: flex; 54 | flex-direction: column; 55 | justify-content: center; 56 | align-items: center; 57 | width: 300px; 58 | height: 300px; 59 | font-size: 30px; 60 | font-style: italic; 61 | background-color: darkgreen; 62 | color: white; 63 | border: 5px solid black; 64 | border-radius: 20px; 65 | .header { 66 | font-size: 38px; 67 | } 68 | .form { 69 | display: flex; 70 | flex-direction: column; 71 | align-items: flex-start; 72 | input { 73 | transform: scale(1.5); 74 | height: 20px; 75 | margin-right: 7px; 76 | } 77 | } 78 | } 79 | `; 80 | 81 | export const _TiltDisableAxis = () => ( 82 | 83 | 84 | 85 | ); 86 | -------------------------------------------------------------------------------- /stories/TiltManualInput/_TiltManualInput.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'react-demo-tab'; 2 | 3 | import Demo from './TiltManualInput.demozap'; 4 | 5 | const code = `import { useState } from 'react'; 6 | import { Joystick } from 'react-joystick-component'; 7 | import type { IJoystickUpdateEvent } from 'react-joystick-component/build/lib/Joystick'; 8 | 9 | import Tilt from '@/index'; 10 | import './TiltManualInput.demozap.css'; 11 | 12 | const TiltManualInput = () => { 13 | const [[manualTiltAngleX, manualTiltAngleY], setManualTiltAngle] = useState([0, 0]); 14 | 15 | const onMove = (stick: IJoystickUpdateEvent) => { 16 | setManualTiltAngle([stick.y ? stick.y * 100 : 0, stick.x ? stick.x * 100 : 0]); 17 | }; 18 | 19 | const onStop = () => { 20 | setManualTiltAngle([0, 0]); 21 | }; 22 | 23 | return ( 24 |
25 | 26 |
27 |
Axis x: {manualTiltAngleX.toFixed(0)}°
28 |
Axis y: {manualTiltAngleY.toFixed(0)}°
29 |
30 |
31 |
32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default TiltManualInput; 39 | `; 40 | 41 | const style = `@import '../ReactParallaxTilt.css'; 42 | 43 | .tilt-manual-input { 44 | display: flex; 45 | flex-direction: column; 46 | justify-content: center; 47 | align-items: center; 48 | 49 | .react-parallax-tilt { 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: center; 53 | align-items: center; 54 | width: 300px; 55 | height: 300px; 56 | font-size: 35px; 57 | font-style: italic; 58 | background-color: darkgreen; 59 | color: white; 60 | border: 5px solid black; 61 | border-radius: 20px; 62 | } 63 | 64 | .manual-input { 65 | display: flex; 66 | flex-direction: column; 67 | align-items: flex-start; 68 | margin-top: 20px; 69 | } 70 | } 71 | `; 72 | 73 | export const _TiltManualInput = () => ( 74 | 75 | 76 | 77 | ); 78 | -------------------------------------------------------------------------------- /src/react-parallax-tilt/types.public.ts: -------------------------------------------------------------------------------- 1 | import type { GlareProps } from '@/features/glare/types.public'; 2 | import type { TiltProps } from '@/features/tilt/types.public'; 3 | 4 | import type { SupportedEvent } from './types'; 5 | 6 | export type OnMoveParams = { 7 | tiltAngleX: number; 8 | tiltAngleY: number; 9 | tiltAngleXPercentage: number; 10 | tiltAngleYPercentage: number; 11 | glareAngle: number; 12 | glareOpacity: number; 13 | event: SupportedEvent; 14 | }; 15 | 16 | export type OnMove = (onMoveParams: OnMoveParams) => void; 17 | 18 | export type OnEnterParams = { 19 | event: MouseEvent | React.MouseEvent | TouchEvent | React.TouchEvent; 20 | }; 21 | 22 | export type OnEnter = (onEnterParams: OnEnterParams) => void; 23 | 24 | export type OnLeaveParams = { 25 | event: MouseEvent | React.MouseEvent | TouchEvent | React.TouchEvent; 26 | }; 27 | 28 | export type OnLeave = (onLeaveParams: OnLeaveParams) => void; 29 | 30 | type HtmlDivTilt = Pick, 'className' | 'style'>; 31 | 32 | export type ReactParallaxTiltProps = TiltProps & 33 | GlareProps & 34 | HtmlDivTilt & { 35 | /** 36 | * Tilt children component 37 | */ 38 | children?: React.ReactNode; 39 | /** 40 | * Scale of the component (`1.5 = 150%, 2 = 200%`). 41 | */ 42 | scale?: number; 43 | /** 44 | * Defines how far the tilt component appears from the user. Lower values create more extreme tilt effects. 45 | */ 46 | perspective?: number; 47 | /** 48 | * Enables/disables vertical flipping of the component. 49 | */ 50 | flipVertically?: boolean; 51 | /** 52 | * Enables/disables horizontal flipping of the component. 53 | */ 54 | flipHorizontally?: boolean; 55 | /** 56 | * Determines if effects should reset on `onLeave` event. 57 | */ 58 | reset?: boolean; 59 | /** 60 | * Easing function for the transition. 61 | */ 62 | transitionEasing?: string; 63 | /** 64 | * Speed of the transition. 65 | */ 66 | transitionSpeed?: number; 67 | /** 68 | * Tracks mouse and touch events across the entire window. 69 | */ 70 | trackOnWindow?: boolean; 71 | /** 72 | * Enables/disables device orientation detection. 73 | */ 74 | gyroscope?: boolean; 75 | /** 76 | * Callback triggered when user moves on the component. 77 | */ 78 | onMove?: OnMove; 79 | /** 80 | * Callback triggered when user enters the component. 81 | */ 82 | onEnter?: OnEnter; 83 | /** 84 | * Callback triggered when user leaves the component. 85 | */ 86 | onLeave?: OnLeave; 87 | }; 88 | -------------------------------------------------------------------------------- /src/features/tilt/test/tiltManualAngle.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import type { OnMove, OnMoveParams } from '@/index'; 5 | import { TiltTest } from '@/utils/TiltTest'; 6 | 7 | describe('Tilt - Manual Angle', () => { 8 | it('should calculate tilt when manual input is provided', async () => { 9 | const onMove = vi.fn(); 10 | 11 | render( 12 | , 13 | ); 14 | 15 | await userEvent.hover(screen.getByText('test')); 16 | 17 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 18 | tiltAngleX: 60, 19 | tiltAngleY: 45, 20 | tiltAngleXPercentage: 100, 21 | tiltAngleYPercentage: 75, 22 | glareAngle: 0, 23 | glareOpacity: 0, 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 25 | event: expect.objectContaining({ 26 | type: 'initial', 27 | }), 28 | }); 29 | }); 30 | 31 | it('should calculate tilt when only X manual input is provided', async () => { 32 | const onMove = vi.fn(); 33 | 34 | render( 35 | , 44 | ); 45 | 46 | await userEvent.hover(screen.getByText('test')); 47 | 48 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 49 | tiltAngleX: 60, 50 | tiltAngleY: 0, 51 | tiltAngleXPercentage: 100, 52 | tiltAngleYPercentage: 0, 53 | glareAngle: 180, 54 | glareOpacity: 1, 55 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 56 | event: expect.objectContaining({ 57 | type: 'initial', 58 | }), 59 | }); 60 | }); 61 | 62 | it('should calculate tilt when only Y manual input is provided', async () => { 63 | const onMove = vi.fn(); 64 | 65 | render(); 66 | 67 | await userEvent.hover(screen.getByText('test')); 68 | 69 | expect(onMove).toBeCalledWith<[OnMoveParams]>({ 70 | tiltAngleX: 0, 71 | tiltAngleY: 45, 72 | tiltAngleXPercentage: 0, 73 | tiltAngleYPercentage: 75, 74 | glareAngle: 0, 75 | glareOpacity: 0, 76 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 77 | event: expect.objectContaining({ 78 | type: 'initial', 79 | }), 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 'Bug Report' 2 | description: 'File a bug report' 3 | body: 4 | - type: 'textarea' 5 | id: 'description' 6 | attributes: 7 | label: 'Description' 8 | description: 'A clear and concise description of what the bug is.' 9 | placeholder: | 10 | Bug description 11 | validations: 12 | required: true 13 | - type: 'input' 14 | id: 'reproduction_link' 15 | attributes: 16 | label: 'Link to Reproduction - Please provide demo in online code editor [CodeSandbox](https://codesandbox.io/) or similar. - Issues without a reproduction link are likely to stall.' 17 | description: | 18 | Please provide demo in online code editor [CodeSandbox](https://codesandbox.io/) or similar. 19 | Issues without a reproduction link are likely to stall. 20 | placeholder: 'https://codesandbox.io/' 21 | validations: 22 | required: true 23 | - type: 'textarea' 24 | id: 'reproduction_steps' 25 | attributes: 26 | label: 'Steps to reproduce' 27 | description: | 28 | Steps to reproduce the behavior: 29 | value: | 30 | 1. Go to '...' 31 | 2. Click on '...' 32 | 3. Scroll down to '...' 33 | 4. See error 34 | - type: 'input' 35 | id: 'expected_behavior' 36 | attributes: 37 | label: 'Expected behavior' 38 | description: 'A clear and concise description of what you expected to happen.' 39 | validations: 40 | required: true 41 | - type: 'textarea' 42 | id: 'code_snippets' 43 | attributes: 44 | label: 'Code snippets' 45 | description: | 46 | If applicable, add code samples to help explain your problem. 47 | value: | 48 | ```jsx 49 | 50 | 51 | 52 | ``` 53 | - type: 'input' 54 | id: 'lib-version' 55 | attributes: 56 | label: 'React Parallax Tilt Version' 57 | description: 'The version of library you use.' 58 | placeholder: '1.5.36' 59 | validations: 60 | required: true 61 | - type: 'input' 62 | id: 'browser' 63 | attributes: 64 | label: 'Browser' 65 | description: 'The browser this issue occurred with.' 66 | placeholder: 'Google Chrome 93' 67 | - type: 'checkboxes' 68 | id: 'operating-system' 69 | attributes: 70 | label: 'Operating System' 71 | description: 'The operating system(s) this issue occurred with.' 72 | options: 73 | - label: 'macOS' 74 | - label: 'Windows' 75 | - label: 'Linux' 76 | - type: 'textarea' 77 | id: 'additional-information' 78 | attributes: 79 | label: 'Additional Information' 80 | description: | 81 | Add any other context about the problem here. 82 | -------------------------------------------------------------------------------- /src/features/tilt/Tilt.ts: -------------------------------------------------------------------------------- 1 | import { constrainToRange } from '@/utils/helperFns'; 2 | import type { ClientPosition } from '@/utils/types'; 3 | 4 | import type { TiltProps } from './types.public'; 5 | 6 | const TILT_ANGLE_CONSTRAINT = 90; 7 | 8 | // All props are initialized by default with non-null values 9 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 10 | 11 | export class Tilt { 12 | public tiltAngleX = 0; 13 | public tiltAngleY = 0; 14 | public tiltAngleXPercentage = 0; 15 | public tiltAngleYPercentage = 0; 16 | 17 | public update = (wrapperElClientPosition: ClientPosition, props: TiltProps): void => { 18 | this.updateTilt(wrapperElClientPosition, props); 19 | this.updateTiltManualInput(wrapperElClientPosition, props); 20 | this.updateTiltReverse(props); 21 | this.updateTiltLimits(props); 22 | }; 23 | 24 | private updateTilt = (wrapperElClientPosition: ClientPosition, props: TiltProps): void => { 25 | const { xPercentage, yPercentage } = wrapperElClientPosition; 26 | const { tiltMaxAngleX, tiltMaxAngleY } = props; 27 | 28 | const tiltTowardMouse = -1; 29 | this.tiltAngleX = (xPercentage * tiltMaxAngleX!) / 100; 30 | this.tiltAngleY = ((yPercentage * tiltMaxAngleY!) / 100) * tiltTowardMouse; 31 | }; 32 | 33 | private updateTiltManualInput = (wrapperElClientPosition: ClientPosition, props: TiltProps): void => { 34 | const { tiltAngleXManual, tiltAngleYManual, tiltMaxAngleX, tiltMaxAngleY } = props; 35 | 36 | const isManualInputIgnoreOtherInputs = tiltAngleXManual !== null || tiltAngleYManual !== null; 37 | if (isManualInputIgnoreOtherInputs) { 38 | this.tiltAngleX = tiltAngleXManual !== null ? tiltAngleXManual! : 0; 39 | this.tiltAngleY = tiltAngleYManual !== null ? tiltAngleYManual! : 0; 40 | wrapperElClientPosition.xPercentage = (100 * this.tiltAngleX) / tiltMaxAngleX!; 41 | wrapperElClientPosition.yPercentage = (100 * this.tiltAngleY) / tiltMaxAngleY!; 42 | } 43 | }; 44 | 45 | private updateTiltReverse = (props: TiltProps): void => { 46 | const tiltReverse = props.tiltReverse ? -1 : 1; 47 | this.tiltAngleX = tiltReverse * this.tiltAngleX; 48 | this.tiltAngleY = tiltReverse * this.tiltAngleY; 49 | }; 50 | 51 | private updateTiltLimits = (props: TiltProps): void => { 52 | const { tiltAxis } = props; 53 | 54 | this.tiltAngleX = constrainToRange(this.tiltAngleX, -TILT_ANGLE_CONSTRAINT, TILT_ANGLE_CONSTRAINT); 55 | this.tiltAngleY = constrainToRange(this.tiltAngleY, -TILT_ANGLE_CONSTRAINT, TILT_ANGLE_CONSTRAINT); 56 | 57 | const isOnlyOneAxisEnabledForTilting = tiltAxis; 58 | if (isOnlyOneAxisEnabledForTilting) { 59 | this.tiltAngleX = tiltAxis === 'x' ? this.tiltAngleX : 0; 60 | this.tiltAngleY = tiltAxis === 'y' ? this.tiltAngleY : 0; 61 | } 62 | }; 63 | 64 | public updateTiltAnglesPercentage = (props: TiltProps): void => { 65 | const { tiltMaxAngleX, tiltMaxAngleY } = props; 66 | 67 | this.tiltAngleXPercentage = (this.tiltAngleX / tiltMaxAngleX!) * 100; 68 | this.tiltAngleYPercentage = (this.tiltAngleY / tiltMaxAngleY!) * 100; 69 | }; 70 | 71 | public render = (element: HTMLDivElement): void => { 72 | element.style.transform += `rotateX(${this.tiltAngleX}deg) rotateY(${this.tiltAngleY}deg) `; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /scripts/rollupConfigs.ts: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import terser from '@rollup/plugin-terser'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import type { GlobalsOption, RollupOptions } from 'rollup'; 5 | import { dts } from 'rollup-plugin-dts'; 6 | 7 | import packageJson from '../package.json' with { type: 'json' }; 8 | 9 | import tsConfig from './tsconfig.base.json' with { type: 'json' }; 10 | 11 | const IS_PRODUCTION = process.env.NODE_ENV === 'production'; 12 | 13 | const PATH_INPUT_FILE = 'src/index.ts'; 14 | const PATH_TSCONFIG_BUILD = 'scripts/tsconfig.build.json'; 15 | 16 | const GLOBALS = { 17 | react: 'React', 18 | 'react-dom': 'ReactDOM', 19 | } as const satisfies GlobalsOption; 20 | 21 | export const LEGACY_CONFIG = [ 22 | { 23 | input: PATH_INPUT_FILE, 24 | output: [ 25 | { 26 | file: packageJson.module, 27 | format: 'esm', 28 | sourcemap: !IS_PRODUCTION, 29 | globals: GLOBALS, 30 | }, 31 | { 32 | file: packageJson.main, 33 | format: 'cjs', 34 | sourcemap: !IS_PRODUCTION, 35 | globals: GLOBALS, 36 | }, 37 | ], 38 | plugins: [ 39 | commonjs(), 40 | typescript({ 41 | tsconfig: PATH_TSCONFIG_BUILD, 42 | sourceMap: !IS_PRODUCTION, 43 | }), 44 | terser({ 45 | output: { comments: false }, 46 | compress: { 47 | pure_getters: true, 48 | }, 49 | toplevel: true, 50 | }), 51 | ], 52 | // Ensure dependencies are not bundled with the library 53 | external: [...Object.keys(packageJson.peerDependencies), 'react/jsx-runtime'], 54 | }, 55 | { 56 | input: PATH_INPUT_FILE, 57 | output: { file: packageJson.types, format: 'esm' }, 58 | plugins: [ 59 | dts({ 60 | compilerOptions: { 61 | baseUrl: './src', 62 | paths: tsConfig.compilerOptions.paths, 63 | }, 64 | }), 65 | ], 66 | }, 67 | ] as const satisfies ReadonlyArray; 68 | 69 | export const MODERN_CONFIG = [ 70 | { 71 | input: PATH_INPUT_FILE, 72 | output: [ 73 | { 74 | file: packageJson.exports.import.default, 75 | format: 'esm', 76 | sourcemap: !IS_PRODUCTION, 77 | globals: GLOBALS, 78 | }, 79 | { 80 | file: packageJson.exports.require.default, 81 | format: 'cjs', 82 | sourcemap: !IS_PRODUCTION, 83 | globals: GLOBALS, 84 | }, 85 | ], 86 | plugins: [ 87 | commonjs(), 88 | typescript({ 89 | tsconfig: PATH_TSCONFIG_BUILD, 90 | sourceMap: !IS_PRODUCTION, 91 | }), 92 | terser({ 93 | output: { comments: false }, 94 | compress: { 95 | pure_getters: true, 96 | }, 97 | toplevel: true, 98 | }), 99 | ], 100 | // Ensure dependencies are not bundled with the library 101 | external: [...Object.keys(packageJson.peerDependencies), 'react/jsx-runtime'], 102 | }, 103 | { 104 | input: PATH_INPUT_FILE, 105 | output: [ 106 | { file: packageJson.exports.import.types, format: 'esm' }, 107 | { file: packageJson.exports.require.types, format: 'cjs' }, 108 | ], 109 | plugins: [ 110 | dts({ 111 | compilerOptions: { 112 | baseUrl: './src', 113 | paths: tsConfig.compilerOptions.paths, 114 | }, 115 | }), 116 | ], 117 | }, 118 | ] as const satisfies ReadonlyArray; 119 | -------------------------------------------------------------------------------- /stories/FlipPage/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | import './Page.css'; 2 | import sampleImg from './lorem-picsum.png'; 3 | 4 | type PageProps = { 5 | flipVertically: boolean; 6 | flipHorizontally: boolean; 7 | toggleFlipVertically: (isEnabled: boolean) => void; 8 | toggleFlipHorizontally: (isEnabled: boolean) => void; 9 | }; 10 | 11 | export const Page = ({ flipVertically, flipHorizontally, toggleFlipVertically, toggleFlipHorizontally }: PageProps) => ( 12 |
13 |
    14 |
  • Home
  • 15 |
  • News
  • 16 |
  • Contact
  • 17 |
  • About
  • 18 |
19 |
20 |
21 |
22 |
Flip Page 👆
23 |
24 |
25 | 33 | 41 |
42 |
43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore 45 | magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 46 | const cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Ut enim ad minim 47 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor 48 | in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 49 |
50 | pic 51 |
52 | culpa qui officia deserunt mollit anim id est laborum.Ut enim ad minim veniam, quis nostrud exercitation ullamco 53 | laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 54 | cillum dolore eu fugiat nulla pariatur. 55 |
56 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Ut 57 | enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute 58 | irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 59 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 60 |
61 | 62 |
63 |
64 |
Your Name
65 | 66 |
67 |
68 |
Country
69 | 73 | 77 | 81 |
82 |
83 |
Subject
84 |