├── .eslintrc.js ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── builds ├── cdn.js └── module.js ├── docs ├── index.html ├── using-css-animations.html ├── using-css-transitions.html └── using-tailwindcss.html ├── package.json ├── scripts └── build.js └── src └── index.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es6": true, 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 9, 10 | "sourceType": "module", 11 | "allowImportExportEverywhere": true 12 | }, 13 | "rules": { 14 | "linebreak-style": [ 15 | "error", 16 | "unix" 17 | ], 18 | "quotes": [ 19 | "error", 20 | "single" 21 | ], 22 | "semi": [ 23 | "error", 24 | "never" 25 | ], 26 | "space-in-parens": [ 27 | "error", 28 | "never" 29 | ], 30 | "no-template-curly-in-string": [ 31 | "error" 32 | ], 33 | "block-scoped-var": [ 34 | "error" 35 | ], 36 | "no-empty-function": [ 37 | "error" 38 | ], 39 | "brace-style": [ 40 | "error", 41 | "1tbs" 42 | ], 43 | "camelcase": [ 44 | "error" 45 | ], 46 | "comma-spacing": [ 47 | "error", 48 | { 49 | "before": false, 50 | "after": true 51 | } 52 | ], 53 | "comma-style": [ 54 | "error", 55 | "last" 56 | ], 57 | "computed-property-spacing": [ 58 | "error", 59 | "never" 60 | ], 61 | "eol-last": [ 62 | "error", 63 | "always" 64 | ], 65 | "func-call-spacing": [ 66 | "error", 67 | "never" 68 | ], 69 | "key-spacing": [ 70 | "error", 71 | { 72 | "mode": "minimum", 73 | "beforeColon": false, 74 | "afterColon": true 75 | } 76 | ], 77 | "keyword-spacing": [ 78 | "error", 79 | { 80 | "before": true, 81 | "after": true 82 | } 83 | ], 84 | "lines-between-class-members": [ 85 | "error", 86 | "always" 87 | ], 88 | "no-array-constructor": [ 89 | "error" 90 | ], 91 | "no-console": [ 92 | "warn" 93 | ], 94 | "no-multiple-empty-lines": [ 95 | "error", 96 | { 97 | "max": 2 98 | } 99 | ], 100 | "no-trailing-spaces": [ 101 | "error" 102 | ], 103 | "no-unused-vars": [ 104 | "error", 105 | { 106 | "ignoreRestSiblings": true 107 | } 108 | ], 109 | "one-var": [ 110 | "error", 111 | "never" 112 | ], 113 | "padded-blocks": [ 114 | "error", 115 | "never" 116 | ], 117 | "padding-line-between-statements": [ 118 | "error", 119 | { 120 | "blankLine": "always", 121 | "prev": "*", 122 | "next": "return" 123 | } 124 | ], 125 | "prefer-object-spread": [ 126 | "error" 127 | ], 128 | "no-fallthrough": "off", 129 | "no-var": [ 130 | "error" 131 | ], 132 | "object-shorthand": [ 133 | "error" 134 | ], 135 | "prefer-destructuring": [ 136 | "error" 137 | ], 138 | "prefer-rest-params": [ 139 | "error" 140 | ], 141 | "prefer-spread": [ 142 | "error" 143 | ] 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: '20.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - run: npm install 17 | - run: npm run build 18 | - run: npm version --no-git-tag-version ${{ github.event.release.tag_name }} 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | package-lock.json 4 | yarn.lock 5 | .DS_* 6 | .env 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lars Heidkämper 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alpine.js Intersect-Class Plugin 2 | 3 | An Alpine.js plugin to easily set CSS classes to an element when it enters the viewport.
4 | Especially useful for animating elements when scrolling. 5 | 6 | ```html 7 | 8 |
9 | ``` 10 | 11 | ### Demo 12 | [Click here to take a look at the examples](https://heidkaemper.github.io/alpinejs-intersect-class/using-tailwindcss.html) 13 | ([View Source](https://github.com/heidkaemper/alpinejs-intersect-class/tree/main/docs)) 14 | 15 | --- 16 | 17 | ## Installation 18 | You can use this plugin by either including it from a ` 26 | 27 | 28 | 29 | ``` 30 | 31 | ### Via NPM 32 | You can install Intersect from NPM for use inside your bundle like so: 33 | 34 | ```sh 35 | npm install alpinejs-intersect-class 36 | ``` 37 | 38 | Then initialize it from your bundle: 39 | 40 | ```js 41 | import Alpine from 'alpinejs' 42 | import intersectClass from 'alpinejs-intersect-class' 43 | 44 | Alpine.plugin(intersectClass) 45 | 46 | ... 47 | ``` 48 | 49 | ## Usage 50 | Use the `x-intersect-class` Attribute to set the CSS class that should be added to the element once it comes to the browsers viewport. 51 | 52 | ```html 53 |
54 | ``` 55 | Make sure that you have defined that class in your CSS files, like `fade-in` in this example. 56 | 57 | ### The .once Modifier 58 | 59 | You can use the `.once` modifier if you want to trigger the CSS class only on the first appearance of an element. 60 | 61 | ```html 62 |
63 | ``` 64 | 65 | ### .threshold, .half and .full Modifier 66 | 67 | Control the `threshold` property of the IntersectionObserver. This works the same way like it does with Alpines **[Intersect Plugin](https://alpinejs.dev/plugins/intersect#half)**. 68 | 69 | ```html 70 |
71 | ``` 72 | 73 | ## FAQ 74 | 75 | ### Couldn't I just use Alpines Intersect Plugin to do something like this? 76 | 77 | ```html 78 |
79 | ``` 80 | You definitely could. And if you are using the Intersect plugin already, then you probably should!
81 | The point of the Intersect-Class plugin is to provide this functionality with as few attributes as possible so that frontend developers will actually use it. 82 | 83 | ### Do I need the x-data attribute? 84 | 85 | Yes, you do. With x-data you define a Alpine.js Component.
86 | [Click here](https://alpinejs.dev/directives/data) to read more about x-data in the Alpine.js documentation. 87 | 88 | ### My elements are flashing when the page is loading 89 | 90 | This is a known problem with JavaScript animations. It's because the JS takes a moment to initialize. The solution can be different depending on what kind of animation you want to run. 91 | 92 | A good starting point could be the use of [x-cloak](https://alpinejs.dev/directives/cloak). 93 | Initial use of the CSS class can also be helpful, like I did for the [CSS animations demo](https://heidkaemper.github.io/alpinejs-intersect-class/using-css-animations.html). 94 | 95 | --- 96 | 97 |

98 | Version 99 | Bundle size 100 | Downloads 101 | License 102 |

103 | -------------------------------------------------------------------------------- /builds/cdn.js: -------------------------------------------------------------------------------- 1 | import intersectClass from './../src/index.js' 2 | 3 | document.addEventListener('alpine:init', () => { 4 | window.Alpine.plugin(intersectClass) 5 | }) 6 | -------------------------------------------------------------------------------- /builds/module.js: -------------------------------------------------------------------------------- 1 | import intersectClass from './../src/index.js' 2 | 3 | export default intersectClass 4 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Alpine.js Intersect-Class - Examples 9 | 24 | 25 | 26 | 27 |

Alpine.js Intersect-Class

28 | 29 |

Looking for Docs?

30 |

The README is there for you!

31 | 32 |

Examples

33 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/using-css-animations.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Alpine.js Intersect-Class - CSS Animations 8 | 68 | 69 | 70 | 71 |

Demo - CSS Animations

72 | 73 |

74 | This demo shows the Intersect-Class plugin in combination with CSS Animations.
75 | Demos for CSS Transitions and Tailwind CSS are also available. 76 |

77 | 78 |
79 | #1: Fade in 80 |
81 | 82 |
83 | #2: Fade in from right 84 |
85 | 86 |
87 | #3: Fade in from left 88 |
89 | 90 |
91 | #4: Fade in just once (no fade out) 92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /docs/using-css-transitions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Alpine.js Intersect-Class - CSS Transitions 8 | 50 | 51 | 52 | 53 |

Demo - CSS Transitions

54 | 55 |

56 | This demo shows the Intersect-Class plugin in combination with CSS Transitions.
57 | Demos for CSS Animations and Tailwind CSS are also available. 58 |

59 | 60 |
61 | #1: Fade in 62 |
63 | 64 |
65 | #2: Fade in from right 66 |
67 | 68 |
69 | #3: Fade in from left 70 |
71 | 72 |
73 | #4: Fade in just once (no fade out) 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/using-tailwindcss.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Alpine.js Intersect-Class - Tailwind CSS 8 | 9 | 10 | 11 | 12 |

Demo - Tailwind CSS

13 | 14 |

15 | This demo shows the Intersect-Class plugin in combination with Tailwind CSS.
16 | Demos for vanilla CSS Transitions and CSS Animations are also available. 17 |

18 | 19 |
20 | #1: Fade in 21 |
22 | 23 |
24 | #2: Fade in from right 25 |
26 | 27 |
28 | #3: Fade in from left 29 |
30 | 31 |
32 | #4: Fade in just once (no fade out) 33 |
34 | 35 |
36 | #5: Fade in when almost fully in viewport (threshold = 90) 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alpinejs-intersect-class", 3 | "description": "An Alpine.js plugin to easily add CSS classes to an element when it enters the viewport.", 4 | "author": "Lars Heidkämper", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/heidkaemper/alpinejs-intersect-class.git" 9 | }, 10 | "homepage": "https://github.com/heidkaemper/alpinejs-intersect-class", 11 | "keywords": [ 12 | "alpinejs", 13 | "alpine", 14 | "plugin", 15 | "intersect", 16 | "intersection", 17 | "animate", 18 | "animation" 19 | ], 20 | "main": "dist/module.cjs.js", 21 | "module": "dist/module.esm.js", 22 | "unpkg": "dist/cdn.min.js", 23 | "files": [ 24 | "/dist" 25 | ], 26 | "scripts": { 27 | "build": "node ./scripts/build.js", 28 | "lint": "npx eslint {builds,scripts,src}/**/*.js", 29 | "lint:fix": "npx eslint {builds,scripts,src}/**/*.js --fix" 30 | }, 31 | "devDependencies": { 32 | "esbuild": "^0.20.2", 33 | "eslint": "^8.57.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const esbuild = require('esbuild') 3 | 4 | if (! fs.existsSync('./dist')) { 5 | fs.mkdirSync('./dist') 6 | } 7 | 8 | build({ 9 | entryPoints: ['builds/cdn.js'], 10 | outfile: 'dist/cdn.min.js', 11 | platform: 'browser', 12 | define: { CDN: 'true' }, 13 | }) 14 | 15 | build({ 16 | entryPoints: ['builds/module.js'], 17 | outfile: 'dist/module.esm.js', 18 | platform: 'neutral', 19 | mainFields: ['main', 'module'], 20 | }) 21 | 22 | build({ 23 | entryPoints: ['builds/module.js'], 24 | outfile: 'dist/module.cjs.js', 25 | target: ['node10.4'], 26 | platform: 'node', 27 | }) 28 | 29 | function build(options) { 30 | options.define || (options.define = {}) 31 | 32 | return esbuild.build({ 33 | ...options, 34 | minify: true, 35 | bundle: true, 36 | }) 37 | .catch(() => process.exit(1)) 38 | } 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default function (Alpine) { 2 | Alpine.directive('intersect-class', (el, { expression, modifiers }, { cleanup }) => { 3 | const classes = expression.split(' ').filter(Boolean) 4 | 5 | const observer = new IntersectionObserver(entries => { 6 | entries.forEach(entry => { 7 | if (! entry.isIntersecting) { 8 | el.classList.remove(...classes) 9 | 10 | return 11 | } 12 | 13 | el.classList.add(...classes) 14 | 15 | modifiers.includes('once') && observer.disconnect() 16 | }) 17 | }, { threshold: getThreshold(modifiers) }) 18 | 19 | observer.observe(el) 20 | 21 | cleanup(() => { 22 | observer.disconnect() 23 | }) 24 | }) 25 | } 26 | 27 | function getThreshold(modifiers) { 28 | if (modifiers.includes('full')) { 29 | return 0.99 30 | } 31 | 32 | if (modifiers.includes('half')) { 33 | return 0.5 34 | } 35 | 36 | if (! modifiers.includes('threshold')) { 37 | return 0 38 | } 39 | 40 | const threshold = modifiers[modifiers.indexOf('threshold') + 1] 41 | 42 | return threshold === '1' ? 1 : Number(`.${threshold}`) 43 | } 44 | --------------------------------------------------------------------------------