@duboseweb/motus 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CUSTOMIZATION.md +150 -0
- package/LICENSE +21 -0
- package/README.md +355 -0
- package/dist/css/core.css +1 -0
- package/dist/css/fade.css +1 -0
- package/dist/css/flip.css +1 -0
- package/dist/css/motus.css +1 -0
- package/dist/css/slide.css +1 -0
- package/dist/css/zoom.css +1 -0
- package/dist/motus.cjs +800 -0
- package/dist/motus.d.cts +102 -0
- package/dist/motus.d.ts +102 -0
- package/dist/motus.js +791 -0
- package/dist/motus.umd.js +2 -0
- package/package.json +115 -0
- package/scss/animations/fade.scss +60 -0
- package/scss/animations/flip.scss +42 -0
- package/scss/animations/slide.scss +34 -0
- package/scss/animations/zoom.scss +58 -0
- package/scss/config.scss +3 -0
- package/scss/core.scss +18 -0
- package/scss/motus.scss +8 -0
package/CUSTOMIZATION.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Customization
|
|
2
|
+
|
|
3
|
+
## Custom animations
|
|
4
|
+
|
|
5
|
+
The JavaScript never knows animation names — it only toggles `motus-animate`. A new animation is therefore just CSS:
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
9
|
+
html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) {
|
|
10
|
+
[data-motus='rotate-in'] {
|
|
11
|
+
opacity: 0;
|
|
12
|
+
transform-origin: bottom left;
|
|
13
|
+
transform: rotate(-14deg) scale(0.96);
|
|
14
|
+
transition:
|
|
15
|
+
opacity 500ms ease,
|
|
16
|
+
transform 700ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-motus='rotate-in'].motus-animate {
|
|
20
|
+
opacity: 1;
|
|
21
|
+
transform: none;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<div data-motus="rotate-in"></div>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Both wrappers matter, and for the same reason: every one of them is an escape route out of the
|
|
32
|
+
hidden `opacity: 0` state, for a visitor whose animation is never going to run.
|
|
33
|
+
|
|
34
|
+
- **`prefers-reduced-motion`** — someone who asked for reduced motion.
|
|
35
|
+
- **`.no-js`** — the script never executed.
|
|
36
|
+
- **`[data-motus-disabled]`** — the consumer's kill switch.
|
|
37
|
+
- **`[data-motus-inactive]`** — the library set this on `<html>` because it is not running:
|
|
38
|
+
disabled by `disable` (which by default is on below 992px), torn down by `destroy()`, or in a
|
|
39
|
+
browser without IntersectionObserver.
|
|
40
|
+
|
|
41
|
+
Miss one and the affected visitors are left looking at a blank space where your content should
|
|
42
|
+
be. The shipped families use exactly this selector; custom animations need it too.
|
|
43
|
+
|
|
44
|
+
Two things make that read as a rotation rather than a drift: `transform-origin` gives it a
|
|
45
|
+
pivot, and there is no `translate` competing with it. A few degrees of tilt alongside a vertical
|
|
46
|
+
translate just looks like a fade upwards.
|
|
47
|
+
|
|
48
|
+
You are not limited to opacity and transform. `demo/headless.html` drives a `clip-path` wipe and
|
|
49
|
+
a `filter` blur off the same class — neither is expressible with the shipped families.
|
|
50
|
+
|
|
51
|
+
> **Do not prefix a custom name with `fade`, `zoom`, `slide` or `flip`.** The shipped stylesheets match those families with `[data-motus^='fade']`-style selectors, so `fade-slow` would silently inherit `opacity: 0` from the fade family. Pick a name outside those prefixes.
|
|
52
|
+
|
|
53
|
+
## Custom easing
|
|
54
|
+
|
|
55
|
+
Pass any `cubic-bezier()` directly:
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<div data-motus="fade-up" data-motus-easing="cubic-bezier(.25, .25, .75, .75)"></div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or set the default for every element:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
Motus.init({ easing: 'cubic-bezier(.25, .25, .75, .75)' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Changing the travel distance
|
|
68
|
+
|
|
69
|
+
`$motus-distance` controls how far `fade-*` and `zoom-*` elements translate (default `100px`). Slides always move 100% of their own size and ignore it.
|
|
70
|
+
|
|
71
|
+
```scss
|
|
72
|
+
@use '@duboseweb/motus/scss/config' with (
|
|
73
|
+
$motus-distance: 200px
|
|
74
|
+
);
|
|
75
|
+
@use '@duboseweb/motus/scss/core';
|
|
76
|
+
@use '@duboseweb/motus/scss/animations/fade';
|
|
77
|
+
@use '@duboseweb/motus/scss/animations/zoom';
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Configure `config` **before** any family is loaded — that is a Sass requirement for `!default` variables, not a quirk of this library.
|
|
81
|
+
|
|
82
|
+
## Using Animate.css instead
|
|
83
|
+
|
|
84
|
+
Skip the motus stylesheets entirely and let the library drive Animate.css class names:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import Motus from '@duboseweb/motus';
|
|
88
|
+
import 'animate.css';
|
|
89
|
+
|
|
90
|
+
Motus.init({
|
|
91
|
+
useClassNames: true, // applies the data-motus value as class names
|
|
92
|
+
initClassName: false,
|
|
93
|
+
animatedClassName: 'animated',
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<div data-motus="fadeInUp"></div>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
You will usually also want:
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
[data-motus] {
|
|
105
|
+
visibility: hidden;
|
|
106
|
+
}
|
|
107
|
+
[data-motus].animated {
|
|
108
|
+
visibility: visible;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Bring your own animations entirely
|
|
113
|
+
|
|
114
|
+
Import the JS and no CSS at all. The library adds `motus-init` on setup and `motus-animate` on entry, and fires `motus:in` / `motus:out` — everything visual is yours:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import Motus from '@duboseweb/motus'; // no stylesheet import
|
|
118
|
+
Motus.init({ initClassName: false });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
See `demo/headless.html` for a working example.
|
|
122
|
+
|
|
123
|
+
## Triggering from another element
|
|
124
|
+
|
|
125
|
+
`data-motus-anchor` takes a CSS selector. The element animates based on the **anchor's** position, not its own — useful for revealing a whole section when its heading arrives:
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<h2 id="section-2">Section two</h2>
|
|
129
|
+
|
|
130
|
+
<div data-motus="fade-up" data-motus-anchor="#section-2"></div>
|
|
131
|
+
<div data-motus="fade-up" data-motus-anchor="#section-2" data-motus-delay="100"></div>
|
|
132
|
+
<div data-motus="fade-up" data-motus-anchor="#section-2" data-motus-delay="200"></div>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Elements sharing an anchor share a single observer, so this is cheap. An invalid selector logs a warning and falls back to observing the element itself.
|
|
136
|
+
|
|
137
|
+
## Turning it off
|
|
138
|
+
|
|
139
|
+
At the HTML level, which disables both the CSS and the JS:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<html data-motus-disabled></html>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Or at the JS level, including by device class or a predicate of your own:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
Motus.init({ disable: 'phone' });
|
|
149
|
+
Motus.init({ disable: () => window.innerWidth < 640 });
|
|
150
|
+
```
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DuBose Web
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# dwg-motus
|
|
2
|
+
|
|
3
|
+
Animate elements as they scroll into view, using the native **IntersectionObserver** API instead of scroll event listeners.
|
|
4
|
+
|
|
5
|
+
- **Zero runtime dependencies**
|
|
6
|
+
- **Modular** — take the whole stylesheet, one animation family, or none at all
|
|
7
|
+
- **Typed** — TypeScript source, bundled `.d.ts`
|
|
8
|
+
- **Accessible** — every animation is gated behind `prefers-reduced-motion`
|
|
9
|
+
- ~3.3 kB JS + ~0.6 kB CSS, gzipped
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div data-motus="fade-up">I animate when you scroll to me.</div>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why IntersectionObserver?
|
|
18
|
+
|
|
19
|
+
The original approach to scroll animation is a `scroll` listener that measures every element on every frame. IntersectionObserver hands that work to the browser, which does it off the main thread.
|
|
20
|
+
|
|
21
|
+
| | Scroll events | IntersectionObserver |
|
|
22
|
+
| ---------------------- | --------------------------- | ------------------------------------------ |
|
|
23
|
+
| Checks while scrolling | ~1000/sec with 100 elements | only when an element crosses the threshold |
|
|
24
|
+
| CPU when idle | continuous polling | zero |
|
|
25
|
+
| Battery impact | higher | much lower |
|
|
26
|
+
| Frame drops | likely with many elements | rare |
|
|
27
|
+
| Browser optimisation | none | native |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install @duboseweb/motus
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import Motus from '@duboseweb/motus';
|
|
39
|
+
import '@duboseweb/motus/motus.css';
|
|
40
|
+
|
|
41
|
+
Motus.init();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The JS entry has **no stylesheet side effects** — CSS is always an explicit import. That is what makes the per-family imports below possible, and it keeps the package safe to import in SSR and CommonJS contexts.
|
|
45
|
+
|
|
46
|
+
### Only the animations you use
|
|
47
|
+
|
|
48
|
+
`core.css` is required. Add only the families you actually reference:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import '@duboseweb/motus/css/core.css';
|
|
52
|
+
import '@duboseweb/motus/css/fade.css';
|
|
53
|
+
// zoom.css, slide.css and flip.css are never shipped to the browser
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### From a CDN
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<link rel="stylesheet" href="https://unpkg.com/@duboseweb/motus/dist/css/motus.css" />
|
|
60
|
+
<script src="https://unpkg.com/@duboseweb/motus/dist/motus.umd.js"></script>
|
|
61
|
+
<script>
|
|
62
|
+
Motus.init();
|
|
63
|
+
</script>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Compiling the SCSS yourself
|
|
67
|
+
|
|
68
|
+
The Sass sources ship with the package, so you can override the translate distance and compile only what you need:
|
|
69
|
+
|
|
70
|
+
```scss
|
|
71
|
+
@use '@duboseweb/motus/scss/config' with (
|
|
72
|
+
$motus-distance: 200px
|
|
73
|
+
);
|
|
74
|
+
@use '@duboseweb/motus/scss/core';
|
|
75
|
+
@use '@duboseweb/motus/scss/animations/fade';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### 1. Initialise
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import Motus from '@duboseweb/motus';
|
|
86
|
+
|
|
87
|
+
Motus.init({
|
|
88
|
+
offset: 120, // px before the trigger point
|
|
89
|
+
delay: 0, // ms
|
|
90
|
+
duration: 400, // ms
|
|
91
|
+
easing: 'ease',
|
|
92
|
+
once: false, // animate only the first time
|
|
93
|
+
mirror: false, // animate back out when scrolling away
|
|
94
|
+
anchorPlacement: 'top-bottom',
|
|
95
|
+
disable: 'lg', // below the lg breakpoint — see Responsive breakpoints
|
|
96
|
+
breakpoints: { sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400 },
|
|
97
|
+
startEvent: 'DOMContentLoaded',
|
|
98
|
+
initClassName: 'motus-init',
|
|
99
|
+
animatedClassName: 'motus-animate',
|
|
100
|
+
useClassNames: false,
|
|
101
|
+
disableMutationObserver: false,
|
|
102
|
+
debounceDelay: 50, // resize debounce, clamped to 16–500
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Unrecognised keys and out-of-range values produce a single grouped `console.warn`, so a typo shows up immediately instead of silently doing nothing.
|
|
107
|
+
|
|
108
|
+
### Responsive breakpoints
|
|
109
|
+
|
|
110
|
+
**By default motus does not animate below 992px.** Many animations only read well at desktop
|
|
111
|
+
width, so disabling outright is usually better than maintaining a parallel mobile set. Pass
|
|
112
|
+
`disable: false` to animate everywhere.
|
|
113
|
+
|
|
114
|
+
`disable` accepts a Bootstrap-aligned tier name, which means **below that tier** — it is
|
|
115
|
+
inclusive and downward, so `'lg'` covers everything narrower than `lg`:
|
|
116
|
+
|
|
117
|
+
| `disable` | Media query | Disabled on |
|
|
118
|
+
| --------- | ------------------------ | --------------------------------------------------------------- |
|
|
119
|
+
| `'sm'` | `(max-width: 575.98px)` | small phones |
|
|
120
|
+
| `'md'` | `(max-width: 767.98px)` | phones in portrait |
|
|
121
|
+
| `'lg'` | `(max-width: 991.98px)` | the above, plus phone landscape and tablet portrait _(default)_ |
|
|
122
|
+
| `'xl'` | `(max-width: 1199.98px)` | the above, plus tablet landscape |
|
|
123
|
+
| `'xxl'` | `(max-width: 1399.98px)` | the above, plus small laptops |
|
|
124
|
+
|
|
125
|
+
Override any tier individually — the rest keep their defaults:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
Motus.init({ disable: 'lg', breakpoints: { lg: 1024 } });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The `.98` is deliberate: a `max-width` derived from a `min-width` breakpoint must not leave a
|
|
132
|
+
dead zone on fractional viewport widths.
|
|
133
|
+
|
|
134
|
+
`disable` also accepts `true`, `false`, a `() => boolean` predicate, and the older device-class
|
|
135
|
+
keywords `'phone' | 'tablet' | 'mobile'`. Those three detect a touch pointer via `matchMedia`
|
|
136
|
+
rather than width, and are mutually exclusive — `'tablet'` does **not** also cover phones. Prefer
|
|
137
|
+
a tier name unless you specifically want touch detection.
|
|
138
|
+
|
|
139
|
+
The check runs once, at `init()`. A desktop window resized across the breakpoint keeps its
|
|
140
|
+
existing state until something calls [`refreshHard()`](#api).
|
|
141
|
+
|
|
142
|
+
### 2. Mark up your elements
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<div data-motus="fade-up"></div>
|
|
146
|
+
<div data-motus="zoom-in" data-motus-duration="800" data-motus-delay="200"></div>
|
|
147
|
+
<div data-motus="flip-left" data-motus-once="true"></div>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Any global option can be overridden per element:
|
|
151
|
+
|
|
152
|
+
| Attribute | Notes |
|
|
153
|
+
| ----------------------------- | ------------------------------------------------------ |
|
|
154
|
+
| `data-motus` | the animation name (required) |
|
|
155
|
+
| `data-motus-offset` | px |
|
|
156
|
+
| `data-motus-delay` | ms |
|
|
157
|
+
| `data-motus-duration` | ms |
|
|
158
|
+
| `data-motus-easing` | a named easing or a raw `cubic-bezier(...)` |
|
|
159
|
+
| `data-motus-once` | `true` / `false` |
|
|
160
|
+
| `data-motus-mirror` | `true` / `false` |
|
|
161
|
+
| `data-motus-anchor` | CSS selector — trigger on _another_ element's position |
|
|
162
|
+
| `data-motus-anchor-placement` | see below |
|
|
163
|
+
| `data-motus-id` | scopes the `motus:in:<id>` event |
|
|
164
|
+
|
|
165
|
+
Add `data-motus-disabled` to `<html>` to switch everything off in both CSS and JS — useful as a server-rendered kill switch.
|
|
166
|
+
|
|
167
|
+
The library sets a second attribute, `data-motus-inactive`, on `<html>` whenever it is not
|
|
168
|
+
running — disabled by `disable`, torn down by `destroy()`, or in a browser without
|
|
169
|
+
IntersectionObserver. The stylesheet hides `[data-motus]` elements until they animate, so this
|
|
170
|
+
is what reveals them when no JS will ever arrive to do it. Treat it as read-only: it is cleared
|
|
171
|
+
on the next `init()`, and setting it yourself will be overwritten. Use `data-motus-disabled` for
|
|
172
|
+
a kill switch.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## API
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
Motus.init(options); // start; returns the matched elements
|
|
180
|
+
Motus.refresh(); // rebuild the observers against the current DOM
|
|
181
|
+
Motus.refreshHard(); // refresh, re-checking whether the library should be disabled
|
|
182
|
+
Motus.destroy(); // full teardown; safe to init() again afterwards
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The exported object is frozen. `refresh()` and `refreshHard()` are called for you on resize and on DOM mutation respectively, so you rarely need them directly — reach for `destroy()` on SPA route changes.
|
|
186
|
+
|
|
187
|
+
### Events
|
|
188
|
+
|
|
189
|
+
`motus:in` and `motus:out` are dispatched on `document`:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
document.addEventListener('motus:in', ({ detail }) => {
|
|
193
|
+
console.log('animated in', detail.node);
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
With `data-motus-id="hero"` you also get `motus:in:hero`, fired _in addition to_ the base event.
|
|
198
|
+
|
|
199
|
+
> `detail.node` is the live DOM element, not a copy. Treat it as read-only — mutating it from a listener affects the page.
|
|
200
|
+
|
|
201
|
+
### TypeScript
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import Motus, {
|
|
205
|
+
type MotusOptions,
|
|
206
|
+
type AnchorPlacement,
|
|
207
|
+
type BreakpointName,
|
|
208
|
+
} from '@duboseweb/motus';
|
|
209
|
+
|
|
210
|
+
Motus.init({
|
|
211
|
+
anchorPlacement: 'center-center', // union-typed, autocompletes
|
|
212
|
+
disable: 'lg', // BreakpointName | 'phone' | 'tablet' | 'mobile' | boolean | () => boolean
|
|
213
|
+
breakpoints: { lg: 1024 }, // partial — the other tiers keep their defaults
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Animations
|
|
220
|
+
|
|
221
|
+
**Fade** — `fade`, `fade-in`, `fade-up`, `fade-down`, `fade-left`, `fade-right`, `fade-up-left`, `fade-up-right`, `fade-down-left`, `fade-down-right`
|
|
222
|
+
|
|
223
|
+
**Zoom** — `zoom-in`, `zoom-in-up`, `zoom-in-down`, `zoom-in-left`, `zoom-in-right`, `zoom-out`, `zoom-out-up`, `zoom-out-down`, `zoom-out-left`, `zoom-out-right`
|
|
224
|
+
|
|
225
|
+
**Slide** — `slide-up`, `slide-down`, `slide-left`, `slide-right`
|
|
226
|
+
|
|
227
|
+
**Flip** — `flip-up`, `flip-down`, `flip-left`, `flip-right`
|
|
228
|
+
|
|
229
|
+
### Anchor placements
|
|
230
|
+
|
|
231
|
+
`top-bottom` · `top-center` · `top-top` · `center-bottom` · `center-center` · `center-top` · `bottom-bottom` · `bottom-center` · `bottom-top`
|
|
232
|
+
|
|
233
|
+
The first word is the part of the **element**, the second is the part of the **viewport** it must reach.
|
|
234
|
+
|
|
235
|
+
### Easings
|
|
236
|
+
|
|
237
|
+
`linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out` pass straight through to CSS. These resolve to tuned `cubic-bezier()` values:
|
|
238
|
+
|
|
239
|
+
`ease-in-back` · `ease-out-back` · `ease-in-out-back` · `ease-in-sine` · `ease-out-sine` · `ease-in-out-sine` · `ease-in-quad` · `ease-out-quad` · `ease-in-out-quad` · `ease-in-cubic` · `ease-out-cubic` · `ease-in-out-cubic` · `ease-in-quart` · `ease-out-quart` · `ease-in-out-quart`
|
|
240
|
+
|
|
241
|
+
Anything else is passed through untouched, so a raw `cubic-bezier(.25,.1,.25,1)` works.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Accessibility
|
|
246
|
+
|
|
247
|
+
Every animation rule lives inside `@media (prefers-reduced-motion: no-preference)`. Users who ask for reduced motion get the content immediately, fully visible and static — there is no JavaScript branch to get wrong, and no risk of content being stuck at `opacity: 0`.
|
|
248
|
+
|
|
249
|
+
Animations are also wrapped in `html:not(.no-js)`, so if you set `class="no-js"` on `<html>` and remove it from a small inline script, content stays visible when JavaScript fails to load.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Troubleshooting
|
|
254
|
+
|
|
255
|
+
**Nothing animates, but the content is all visible.** This is almost always
|
|
256
|
+
`prefers-reduced-motion`, and it is the library working as intended. Every animation rule is
|
|
257
|
+
gated on it, so if the viewer has asked for reduced motion they get the content immediately,
|
|
258
|
+
static. Check with:
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
On macOS the setting is **System Settings → Accessibility → Display → Reduce motion**, and it
|
|
265
|
+
applies to every browser. Firefox picks a change up immediately; Chrome may need a restart,
|
|
266
|
+
which is why the two can briefly disagree. Firefox also reports `reduce` unconditionally when
|
|
267
|
+
`privacy.resistFingerprinting` is enabled.
|
|
268
|
+
|
|
269
|
+
Note that most browser-automation tools _override_ this: Playwright, for example, defaults to
|
|
270
|
+
`reducedMotion: 'no-preference'`, so an automated check can show animations running on a machine
|
|
271
|
+
where a real browser would correctly suppress them.
|
|
272
|
+
|
|
273
|
+
**Nothing animates and nothing is initialised.** Check that the `no-js` class is actually being
|
|
274
|
+
removed from `<html>`. Every animation is gated behind `html:not(.no-js)`, so if the script that
|
|
275
|
+
removes it is blocked or never runs, the whole library appears dead. Remove it from an inline
|
|
276
|
+
script in `<head>`, not an external file.
|
|
277
|
+
|
|
278
|
+
`demo/diagnose.html` in the repository checks all of the above and names the cause.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Browser support
|
|
283
|
+
|
|
284
|
+
Requires native [IntersectionObserver](https://caniuse.com/intersectionobserver): **Chrome 51+, Firefox 55+, Safari 12.1+, Edge 79+**. No IE11, and no polyfill is bundled. On an unsupported browser `init()` warns and returns, leaving all content visible.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## What ships
|
|
289
|
+
|
|
290
|
+
| | |
|
|
291
|
+
| --------------------------------- | ----------------------------------------- |
|
|
292
|
+
| `dist/motus.js`, `dist/motus.cjs` | **unminified**, with readable identifiers |
|
|
293
|
+
| `dist/motus.umd.js` | minified, for `<script>` tags |
|
|
294
|
+
| `dist/motus.d.ts`, `.d.cts` | types for `import` and `require` |
|
|
295
|
+
| `dist/css/*.css` | minified, one file per animation family |
|
|
296
|
+
| `scss/**` | the Sass sources, for your own build |
|
|
297
|
+
|
|
298
|
+
The bundler entries are deliberately not minified. Your bundler minifies them again on the way
|
|
299
|
+
into your application, so the bytes reaching a browser are identical either way — what you get
|
|
300
|
+
back is a readable stack trace when something goes wrong inside the library. The UMD build is the
|
|
301
|
+
opposite case: it is loaded directly by a `<script>` tag, so it stays minified.
|
|
302
|
+
|
|
303
|
+
Source maps are not published. They would reference `src/*.ts` files the package does not contain,
|
|
304
|
+
so a debugger would report "source not found" while the maps took up nearly half the download.
|
|
305
|
+
The unminified builds serve the same purpose without the indirection.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
npm install
|
|
313
|
+
npm run dev # demo pages at http://localhost:8080 with live reload
|
|
314
|
+
npm test # vitest
|
|
315
|
+
npm run build # dist/
|
|
316
|
+
npm run lint
|
|
317
|
+
npm run test:pack # pack, install into a throwaway project, assert it works
|
|
318
|
+
npm run check:pkg # publint + are-the-types-wrong
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Releasing
|
|
322
|
+
|
|
323
|
+
Publishing runs from CI on a version tag, using npm
|
|
324
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers) — there is no npm token stored
|
|
325
|
+
anywhere, and npm generates a provenance attestation automatically, linking the published tarball
|
|
326
|
+
to the exact commit and workflow that built it.
|
|
327
|
+
|
|
328
|
+
Work lands on `develop`. `main` is protected and takes pull requests only, so the version bump is
|
|
329
|
+
made on `develop` and the tag is cut from `main` after the merge:
|
|
330
|
+
|
|
331
|
+
```sh
|
|
332
|
+
# on develop — bump the version without tagging yet
|
|
333
|
+
npm version patch --no-git-tag-version # or minor / major
|
|
334
|
+
git commit -am "Release v1.0.1"
|
|
335
|
+
git push
|
|
336
|
+
|
|
337
|
+
# open a pull request from develop to main and merge it
|
|
338
|
+
|
|
339
|
+
git checkout main && git pull
|
|
340
|
+
git tag v1.0.1
|
|
341
|
+
git push origin v1.0.1 # this is what triggers the release
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Tags are not covered by the branch protection, so the final push is what starts the publish.
|
|
345
|
+
|
|
346
|
+
The workflow refuses to publish if the tagged commit is not reachable from `main`, or if the tag
|
|
347
|
+
and `package.json` disagree. It fails loudly rather than skipping, so a release that did not happen
|
|
348
|
+
is never mistaken for one that did. `prepublishOnly` then runs lint, typecheck, tests, the build,
|
|
349
|
+
`check:pkg` and the package smoke test before anything reaches the registry.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## License
|
|
354
|
+
|
|
355
|
+
MIT © DuBose Web
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media (prefers-reduced-motion:no-preference){body.motus-ready [data-motus]{transition-delay:0s;transition-duration:var(--motus-duration);transition-timing-function:var(--motus-easing)}body.motus-ready [data-motus].motus-animate{transition-delay:var(--motus-delay)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media screen and (prefers-reduced-motion:no-preference){html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=fade][data-motus^=fade]{opacity:0;transition-property:opacity,transform}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=fade][data-motus^=fade].motus-animate{opacity:1;transform:none}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-in],html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade]{transform:none}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-up]{transform:translate3d(0,100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-down]{transform:translate3d(0,-100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-right]{transform:translate3d(-100px,0,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-left]{transform:translate3d(100px,0,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-up-right]{transform:translate3d(-100px,100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-up-left]{transform:translate3d(100px,100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-down-right]{transform:translate3d(-100px,-100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-down-left]{transform:translate3d(100px,-100px,0)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media screen and (prefers-reduced-motion:no-preference){html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=flip][data-motus^=flip]{-webkit-backface-visibility:hidden;backface-visibility:hidden;transition-property:transform}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-left]{transform:perspective(2500px) rotateY(-100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-left].motus-animate{transform:perspective(2500px) rotateY(0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-right]{transform:perspective(2500px) rotateY(100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-right].motus-animate{transform:perspective(2500px) rotateY(0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-up]{transform:perspective(2500px) rotateX(-100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-up].motus-animate{transform:perspective(2500px) rotateX(0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-down]{transform:perspective(2500px) rotateX(100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-down].motus-animate{transform:perspective(2500px) rotateX(0)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media (prefers-reduced-motion:no-preference){body.motus-ready [data-motus]{transition-delay:0s;transition-duration:var(--motus-duration);transition-timing-function:var(--motus-easing)}body.motus-ready [data-motus].motus-animate{transition-delay:var(--motus-delay)}}@media screen and (prefers-reduced-motion:no-preference){html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=fade][data-motus^=fade]{opacity:0;transition-property:opacity,transform}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=fade][data-motus^=fade].motus-animate{opacity:1;transform:none}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-in],html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade]{transform:none}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-up]{transform:translate3d(0,100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-down]{transform:translate3d(0,-100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-right]{transform:translate3d(-100px,0,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-left]{transform:translate3d(100px,0,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-up-right]{transform:translate3d(-100px,100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-up-left]{transform:translate3d(100px,100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-down-right]{transform:translate3d(-100px,-100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=fade-down-left]{transform:translate3d(100px,-100px,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=zoom][data-motus^=zoom]{opacity:0;transition-property:opacity,transform}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=zoom][data-motus^=zoom].motus-animate{opacity:1;transform:translateZ(0) scale(1)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in]{transform:scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-up]{transform:translate3d(0,100px,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-down]{transform:translate3d(0,-100px,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-right]{transform:translate3d(-100px,0,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-left]{transform:translate3d(100px,0,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out]{transform:scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-up]{transform:translate3d(0,100px,0) scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-down]{transform:translate3d(0,-100px,0) scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-right]{transform:translate3d(-100px,0,0) scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-left]{transform:translate3d(100px,0,0) scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=slide][data-motus^=slide]{transition-property:transform;visibility:hidden}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=slide][data-motus^=slide].motus-animate{transform:translateZ(0);visibility:visible}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-up]{transform:translate3d(0,100%,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-down]{transform:translate3d(0,-100%,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-right]{transform:translate3d(-100%,0,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-left]{transform:translate3d(100%,0,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=flip][data-motus^=flip]{-webkit-backface-visibility:hidden;backface-visibility:hidden;transition-property:transform}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-left]{transform:perspective(2500px) rotateY(-100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-left].motus-animate{transform:perspective(2500px) rotateY(0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-right]{transform:perspective(2500px) rotateY(100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-right].motus-animate{transform:perspective(2500px) rotateY(0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-up]{transform:perspective(2500px) rotateX(-100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-up].motus-animate{transform:perspective(2500px) rotateX(0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-down]{transform:perspective(2500px) rotateX(100deg)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=flip-down].motus-animate{transform:perspective(2500px) rotateX(0)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media screen and (prefers-reduced-motion:no-preference){html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=slide][data-motus^=slide]{transition-property:transform;visibility:hidden}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=slide][data-motus^=slide].motus-animate{transform:translateZ(0);visibility:visible}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-up]{transform:translate3d(0,100%,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-down]{transform:translate3d(0,-100%,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-right]{transform:translate3d(-100%,0,0)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=slide-left]{transform:translate3d(100%,0,0)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media screen and (prefers-reduced-motion:no-preference){html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=zoom][data-motus^=zoom]{opacity:0;transition-property:opacity,transform}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus^=zoom][data-motus^=zoom].motus-animate{opacity:1;transform:translateZ(0) scale(1)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in]{transform:scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-up]{transform:translate3d(0,100px,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-down]{transform:translate3d(0,-100px,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-right]{transform:translate3d(-100px,0,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-in-left]{transform:translate3d(100px,0,0) scale(.6)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out]{transform:scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-up]{transform:translate3d(0,100px,0) scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-down]{transform:translate3d(0,-100px,0) scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-right]{transform:translate3d(-100px,0,0) scale(1.2)}html:not(.no-js):not([data-motus-disabled]):not([data-motus-inactive]) [data-motus=zoom-out-left]{transform:translate3d(100px,0,0) scale(1.2)}}
|