Esc

Try “tilt”, “reveal”, “drag”, or “reduced motion”.

Guide

Pointer effects

Parallax, tilt, depth, magnetic, repel, follow, look, point, and spin. One property each.

Each effect is one property. Several on one element add together.

The pointer area

Parallax, tilt, follow, and spin measure the pointer against an area. Its center is rest and its edges are the maximum. When the pointer leaves, these effects return to rest.

By default the area is the nearest [data-kinesis] scope. --k-area changes it:

ValueArea
scope (default)the nearest [data-kinesis] element
selfthe element itself, so each card in a grid reacts only under the pointer
viewportthe window
.product-card {
  --k-tilt: 10deg;
  --k-area: self;
}

Magnetic, repel, look, and point use the distance between the element and the pointer instead. --k-radius sets how far they reach.

Parallax

--k-parallax shifts the element away from the pointer. One value covers both axes, two set x and y. Negative values move with the pointer.

.background { --k-parallax: 12px; }
.midground  { --k-parallax: 30px 10px; }
.foreground { --k-parallax: -20px; }

Different amounts per layer create depth.

Somewhere quiet

Move your pointer over the sceneEdit
.layers {
  --k-motion: soft;
  position: relative;
  height: 100%;
  overflow: hidden;
  background: linear-gradient(#bcd4ff, #f1f5ff);
}

.layers > * {
  position: absolute;
}

.layers__sun {
  --k-parallax: -12px;
  top: 40px;
  right: 18%;
  width: 90px;
  height: 90px;
  border-radius: 50%;
  background: #ffcf5a;
}

.layers__far {
  --k-parallax: 18px 6px;
  inset: auto -10% 40px -10%;
  height: 150px;
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
  background: #8fb0f7;
}

.layers__near {
  --k-parallax: 44px 12px;
  inset: auto -15% -30px -15%;
  height: 130px;
  border-radius: 45% 55% 0 0 / 100% 100% 0 0;
  background: #3d6fe0;
}

.layers__title {
  --k-parallax: -26px -10px;
  inset: 34% 0 auto 0;
  margin: 0;
  text-align: center;
  font-size: 34px;
  letter-spacing: -0.02em;
  color: #0c1324;
}

Tilt

--k-tilt turns the element toward the pointer in 3D. One angle covers both axes, two set each. Negative values tilt away.

.card {
  --k-tilt: 12deg;
}

Tilt needs perspective. If the parent has none, Kinesis gives it perspective: 1000px. --k-perspective sets another distance. Smaller values look more dramatic.

Depth

--k-depth lifts an element toward the viewer. Inside a tilted ancestor, deeper layers stand out as it turns.

.card        { --k-tilt: 14deg; }
.card__badge { --k-depth: 70px; }
.card__title { --k-depth: 40px; }
New

Depth, not decoration

Each layer sits at its own height above the card.

Move your pointer over the stageEdit
.tilt-scene {
  display: grid;
  place-items: center;
  height: 100%;
}

.tilt-card {
  --k-tilt: 14deg;
  --k-motion: soft;
  width: min(300px, 80%);
  padding: 28px;
  border-radius: 22px;
  background: linear-gradient(140deg, #2f6bff, #7c5cff);
  color: #fff;
  box-shadow: 0 40px 60px -30px rgb(47 107 255 / 0.7);
}

.tilt-card__badge {
  --k-depth: 70px;
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  background: #fff;
  color: #2f6bff;
  font-size: 12px;
  font-weight: 700;
}

.tilt-card h3 {
  --k-depth: 45px;
  margin: 16px 0 6px;
  font-size: 26px;
  line-height: 1.1;
}

.tilt-card p {
  --k-depth: 25px;
  margin: 0;
  font-size: 15px;
  color: rgb(255 255 255 / 0.85);
}

Kinesis sets transform-style: preserve-3d between each depth layer and its tilted ancestor. overflow: hidden, filter, or opacity below 1 on an element in between flattens the 3D. If depth looks flat, check those. debug mode names the element.

Magnetic

--k-magnetic leans the element toward a pointer within --k-radius. The value is the share of the distance it travels, so 0.3 moves a third of the way. The pull fades toward the edge of the radius.

.cta {
  --k-magnetic: 0.35;
  --k-radius: 160px;
  --k-motion: bouncy;
}

A smaller value on the label than on the button gives a layered pull.

Bring your pointer close, then clickEdit
.magnet-scene {
  display: grid;
  place-items: center;
  height: 100%;
}

.magnet {
  --k-magnetic: 0.35;
  --k-radius: 160px;
  --k-when: press;
  --k-scale: 0.94;
  --k-motion: bouncy;
  padding: 18px 34px;
  border: 0;
  border-radius: 999px;
  background: #0c1324;
  color: #fff;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
}

.magnet span {
  --k-magnetic: 0.15;
  --k-radius: 160px;
  display: inline-block;
}

Repel

--k-repel pushes the element away from a pointer within --k-radius. The value is the largest distance, reached with the pointer right on top.

.dots i {
  --k-repel: 26px;
  --k-radius: 110px;
}
Sweep your pointer through the dotsEdit
.dots {
  display: grid;
  grid-template-columns: repeat(16, 1fr);
  place-items: center;
  height: 100%;
  padding: 28px;
}

.dots i {
  --k-repel: 26px;
  --k-radius: 110px;
  --k-when: near;
  --k-scale: 1.8;
  --k-background: #2f6bff;
  display: block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #b4bfd6;
}

Follow

--k-follow chases the pointer inside the area. 1 reaches it, 0.5 goes halfway. When the pointer leaves, the element returns. --k-motion sets the chase: instant sticks to the pointer, soft trails it.

.cursor-dot  { --k-follow: 1; --k-motion: instant; }
.cursor-ring { --k-follow: 1; --k-motion: soft; }
Move inside the boxEdit
.cursorbox {
  position: relative;
  height: 100%;
  cursor: none;
}

.cursorbox span {
  --k-follow: 1;
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  border-radius: 50%;
  pointer-events: none;
}

.cursorbox__dot {
  --k-motion: instant;
  width: 10px;
  height: 10px;
  background: #2f6bff;
}

.cursorbox__ring {
  --k-motion: soft;
  width: 46px;
  height: 46px;
  border: 2px solid #2f6bff;
}

--k-limit keeps it on a short leash, like a pupil in an eye:

.pupil {
  --k-follow: 1;
  --k-limit: 22px;
  --k-area: viewport;
}
Move your pointer anywhere on the pageEdit
.eyes {
  display: flex;
  gap: 18px;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.eyes__eye {
  display: grid;
  place-items: center;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  background: #fff;
  box-shadow: inset 0 -6px 0 rgb(0 0 0 / 0.07), 0 12px 30px -12px rgb(12 19 36 / 0.35);
}

.eyes__pupil {
  --k-follow: 1;
  --k-limit: 22px;
  --k-area: viewport;
  --k-motion: snappy;
  display: block;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: #0c1324;
}

Look

--k-look turns the element in 3D to face the pointer, like a head. It reaches the full angle at --k-radius and holds it beyond.

.head {
  --k-look: 35deg;
  --k-radius: 320px;
}
Move your pointer around the headsEdit
.heads {
  display: flex;
  gap: 28px;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.heads__head {
  --k-look: 35deg;
  --k-radius: 320px;
  --k-motion: soft;
  display: flex;
  gap: 16px;
  justify-content: center;
  align-items: center;
  width: 104px;
  height: 104px;
  border-radius: 30px;
  background: linear-gradient(160deg, #ffd166, #ff9f43);
}

.heads__head span {
  --k-depth: 24px;
  width: 12px;
  height: 18px;
  border-radius: 6px;
  background: #0c1324;
}

Tilt reacts to where the pointer is in an area. Look makes each element face the pointer from its own position.

Point

--k-point rotates the element flat to aim at the pointer, for needles, arrows, and compasses. The value is the direction your artwork faces at rest, measured like gradient angles: 0deg up, 90deg right, 180deg down. When the pointer leaves the page, it keeps its last direction.

.needle {
  --k-point: 0deg;
  --k-motion: bouncy;
}
Move your pointer across the gridEdit
.needles {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 18px;
  place-items: center;
  height: 100%;
  padding: 36px;
}

.needles__needle {
  /* The artwork points up. */
  --k-point: 0deg;
  --k-motion: bouncy;
  width: 6px;
  height: 36px;
  border-radius: 3px;
  background: linear-gradient(#2f6bff 50%, #c7d3ee 50%);
}

Spin

--k-spin rotates the element flat in proportion to the pointer's position across the area. The value is the rotation at the right edge, and the left edge turns the other way. --k-axis: y uses vertical position instead. transform-origin sets the pivot.

.wheel { --k-spin: 360deg; }
.arm   { --k-spin: 20deg; transform-origin: 20% 90%; }
Move your pointer left and rightEdit
.gears {
  --k-motion: snappy;
  position: relative;
  height: 100%;
}

/* --r: pitch radius. --n: tooth count, proportional to the radius so the teeth line up. */
.gear {
  position: absolute;
  left: calc(50% + var(--x) - var(--r) - 5px);
  top: calc(50% + var(--y) - var(--r) - 5px);
  width: calc(var(--r) * 2 + 10px);
  height: calc(var(--r) * 2 + 10px);
  border-radius: 50%;
  background:
    radial-gradient(circle closest-side, #fff 0 22%, rgb(0 0 0 / 0.2) 23% 28%, transparent 29%),
    var(--gear);
  /* A body and a ring of teeth. --phase aligns the teeth with the next gear's gaps. */
  mask:
    radial-gradient(circle closest-side, #000 calc(100% - 10px), transparent calc(100% - 9.5px)),
    repeating-conic-gradient(from var(--phase), #000 0 calc(150deg / var(--n)), transparent 0 calc(360deg / var(--n)));
}

/* Spin is inversely proportional to the radius: 180 × 80 / 50 = 288, 288 × 50 / 35 = 411.43. */
.gear--large {
  --k-spin: 180deg;
  --gear: #1f2a44;
  --r: 80px;
  --n: 32;
  --phase: 6.41deg;
  --x: -71.2px;
  --y: 12.5px;
}

.gear--medium {
  --k-spin: -288deg;
  --gear: #2f6bff;
  --r: 50px;
  --n: 20;
  --phase: 16.25deg;
  --x: 46.6px;
  --y: -42.5px;
}

.gear--small {
  --k-spin: 411.43deg;
  --gear: #ff5d6c;
  --r: 35px;
  --n: 14;
  --phase: 25.36deg;
  --x: 116.2px;
  --y: 6.3px;
}

Point aims at the pointer. Spin turns with the pointer's position, which is how a wheel rolls as a rider moves.

Radius, axis, and limit

  • --k-radius (default 200px) is how far the pointer reaches for magnetic, repel, look, and the near reaction.
  • --k-axis: x or y limits effects to one axis. Distance-based effects then measure along that axis only, which is how a dock follows horizontal position.
  • --k-limit caps how far pointer effects move the element.

Touch and device orientation

On touch screens, pointer effects follow a finger while it is down and return to rest when it lifts. Hover is never simulated.

On phones, parallax and tilt can follow how the device is held. iOS asks for permission, so request it from a user gesture:

import { requestOrientation } from "@amineyarman/kinesis"

enableMotionButton.addEventListener("click", async () => {
  const granted = await requestOrientation()
})

Once granted, the device's tilt drives parallax and tilt while no finger is down.

Edit this page on GitHub