├── .gitignore
├── .github
└── workflows
│ └── npm-publish.yml
├── scripts
└── build.js
├── LICENSE
├── package.json
├── docs
├── index.html
├── using-tailwindcss.html
├── using-css-transitions.html
└── using-css-animations.html
├── src
└── index.js
├── .eslintrc.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | package-lock.json
4 | yarn.lock
5 | .DS_*
6 | .env
7 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------