Guide
JavaScript
The same vocabulary as props, plus live overrides, bindings to signals, drag events, and scopes.
Every CSS property has a JavaScript prop. Use JavaScript when motion depends on application state, or when CSS can't describe it.
Every example on this site comes both ways. Open its code and switch Motion in to JavaScript to see it written with kinesis().
kinesis()
kinesis() takes an element, a selector, or a list of elements, plus props. Props are the CSS names in camelCase. Lengths are numbers in px, angles in degrees, times in ms.
import { kinesis } from "@amineyarman/kinesis"
kinesis(".card", { tilt: 8, area: "self", motion: "soft" })
kinesis(document.querySelector(".cta"), {
magnetic: 0.35,
radius: 160,
when: "press",
scale: 0.94,
})
Arrays are pairs: parallax: [30, 12], y: [24, 0], color: ["#999", "tomato"]. A single value means "from rest to this", as in CSS.
kinesis() works on any element, inside a [data-kinesis] scope or not. Outside a scope, the pointer area is the viewport.
set() and reset()
kinesis() returns a handle. set() changes props, which override CSS for that element. reset() hands them back to CSS.
const card = kinesis(document.querySelector(".card"))
card.set({ tilt: 14 }) // JavaScript owns tilt now
card.reset("tilt") // back to whatever the stylesheet says
card.reset() // back to CSS for everything
set() doesn't touch the DOM or re-read styles, so it's cheap enough for every input event or animation frame. Prefer it to inline --k-* styles for state-driven motion.
bind()
bind() drives an output from a signal, a number that changes over time. Bound values add to CSS effects. Scale and opacity multiply.
import { kinesis, pointer } from "@amineyarman/kinesis"
kinesis(".needle").bind({
rotate: pointer.speed.map([0, 4000], [-90, 90]).spring("bouncy"),
})
.gauge-scene {
display: grid;
place-items: center;
height: 100%;
}
.gauge {
position: relative;
width: 220px;
height: 120px;
overflow: hidden;
border-radius: 120px 120px 0 0;
background: conic-gradient(from -90deg at 50% 100%, #16c784, #ffb020, #ff5d6c 180deg, transparent 0);
}
.gauge__needle {
position: absolute;
bottom: 0;
left: calc(50% - 3px);
width: 6px;
height: 100px;
border-radius: 3px;
background: #0c1324;
transform-origin: 50% 100%;
}Bindable channels: x, y, z, rotate, rotateX, rotateY, scale, opacity, blur, progress, and any CSS custom property starting with --. Binding progress drives the element's reaction outputs in place of its trigger.
kinesis(".meter").bind({ "--level": audio.volume })
unbind("rotate") removes one binding, unbind() removes all of them.
Signals
Signals update once per frame. Kinesis provides these:
| Signal | Value |
|---|---|
pointer.x, pointer.y | position in the viewport, px |
pointer.nx, pointer.ny | position from -1 to 1 across the viewport |
pointer.velocityX, pointer.velocityY, pointer.speed | px per second, smoothed |
pointer.present | 1 while a pointer is over the page |
scroll.y, scroll.progress, scroll.velocity | document scroll |
time | seconds since the page loaded |
handle.progress | an element's reaction progress |
handle.near | how close the pointer is to that element, 1 at its center to 0 at its radius |
handle.pointer.x, handle.pointer.y | the pointer's offset from that element's resting center, px |
Make your own with value() and combine signals with computed():
import { computed, kinesis, value } from "@amineyarman/kinesis"
const volume = value(0) // set it from anywhere: volume.set(0.7)
const loud = computed(() => volume.get() * 2)
kinesis(".bars").bind({ scale: loud.map([0, 1], [1, 1.5]) })
Every signal has:
map([inMin, inMax], [outMin, outMax])maps linearly, clamped to the output range. A third argument{ clamp: false }extrapolates. A function also works:map(v => v * v).clamp(min, max)spring(preset)follows the signal with a spring, from a preset name or{ stiffness, damping }.get()returns the current value.subscribe(fn)callsfnwith each new value, at most once per frame, and returns an unsubscribe function.
A signal is computed at most once per frame, however many elements read it. Signals nothing reads cost nothing.
Per-element signals keep custom motion short. Grass that bends behind a fast pointer, most strongly near it:
for (const el of document.querySelectorAll(".blade")) {
const blade = kinesis(el, { radius: 220 })
blade.bind({
rotate: computed(() => (pointer.velocityX.get() / 45) * blade.near.get()).spring("bouncy"),
})
}
The robot arm on the home page aims two joints the same way, from shoulder.pointer.x and shoulder.pointer.y.
Events
kinesis(".sheet").on("dragend", ({ x, y, velocityX, velocityY }) => { /* … */ })
Events: dragstart, drag, dragend. See Drag.
Cleaning up
destroy() removes the props and bindings a handle added. Motion declared in CSS keeps running. Elements removed from the page are released automatically.
Scopes
initKinesis() sets up every [data-kinesis] element and watches the page for more. For a scope you control, for example in a component, use createKinesis():
import { createKinesis } from "@amineyarman/kinesis"
const scope = createKinesis(element) // adds data-kinesis if missing
scope.pause() // freeze
scope.resume()
scope.refresh() // re-read CSS after changes Kinesis cannot observe
scope.destroy() // remove everything and restore styles
Options
import { configure, initKinesis } from "@amineyarman/kinesis"
initKinesis({
reducedMotion: "user", // "user" (default), "always", or "never"
debug: false, // console warnings for common mistakes
})
configure({ reducedMotion: "always" }) // change later
The full list is in the JavaScript API reference.