Esc

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

Guide

Reactions

Animate between two states when the pointer comes near, an element is hovered or pressed, it scrolls into view, or the page scrolls.

A reaction has a trigger and outputs. --k-when sets the trigger, and outputs set what changes. Kinesis turns the trigger into a progress value from 0 to 1, springs it, and moves each output with it.

.card {
  --k-when: hover;   /* trigger */
  --k-y: -6px;       /* outputs */
  --k-scale: 1.02;
}

A dock is near with --k-scale. A pressable button is press with --k-scale. A reveal is view with --k-opacity.

Outputs

One value goes from rest to that value. Two values go from the first to the second.

OutputRestExample
--k-x, --k-y0--k-y: 24px 0 rises into place
--k-rotate0deg--k-rotate: -8deg 8deg
--k-scale1--k-scale: 1.1 grows a little
--k-opacity1--k-opacity: 0 1 fades in
--k-blur0--k-blur: 8px 0 sharpens
--k-colorcurrent color--k-color: tomato
--k-backgroundcurrent background--k-background: #0c1324 #2f6bff

Colors accept any CSS color. The browser mixes them in Oklab, which keeps transitions from passing through gray.

Transform outputs add to pointer effects on the same element and respect transform-origin. A dock icon with transform-origin: bottom grows upward.

near

The pointer's closeness: 0 at --k-radius, 1 at the element's center.

.dock__icon {
  --k-when: near;
  --k-radius: 120px;
  --k-axis: x;
  --k-scale: 1.65;
  --k-y: -14px;
  transform-origin: 50% 100%;
}
Slide your pointer along the dockEdit
.dock-scene {
  display: grid;
  place-items: end center;
  height: 100%;
  padding-bottom: 36px;
}

.dock {
  display: flex;
  gap: 8px;
  padding: 8px;
  border-radius: 22px;
  background: rgb(255 255 255 / 0.7);
  box-shadow: 0 20px 40px -20px rgb(12 19 36 / 0.35);
}

.dock__icon {
  --k-when: near;
  --k-axis: x;
  --k-radius: 120px;
  --k-scale: 1.65;
  --k-y: -14px;
  --k-motion: snappy;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: linear-gradient(145deg, hsl(var(--hue) 90% 66%), hsl(calc(var(--hue) + 40) 85% 52%));
  transform-origin: 50% 100%;
}

--k-axis: x measures horizontal distance only, so the icons follow the pointer along the dock.

hover

1 while the pointer is over the element. Keyboard focus also turns it on.

Kinesis tests hover against the element's resting position. An element that shrinks or tilts away from the pointer doesn't flicker at its edges, as it would with CSS :hover.

.card {
  --k-when: hover;
  --k-y: -6px;
}

Touch never turns hover on.

press

1 while the element is pressed, by mouse, finger, or pen, or by Enter or Space when it has focus.

.button {
  --k-when: press;
  --k-scale: 0.9;
  --k-motion: bouncy;
}
Press the buttons, or Tab to one and press EnterEdit
.presses {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.press {
  --k-when: press;
  --k-motion: bouncy;
  padding: 14px 26px;
  border: 0;
  border-radius: 14px;
  background: #0c1324;
  color: #fff;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.press--squish {
  --k-scale: 0.88;
}

.press--sink {
  --k-y: 5px;
  box-shadow: 0 calc((1 - var(--k-progress)) * 5px) 0 #2f6bff;
}

.press--glow {
  --k-scale: 0.97;
  --k-background: #2f6bff;
}

view

1 once the element is a tenth of the way into the viewport, 0 when it leaves. view once stays at 1 after the first time.

.feature {
  --k-when: view once;
  --k-opacity: 0 1;
  --k-y: 28px 0;
}
Edit
.reveal {
  display: grid;
  gap: 10px;
  align-content: center;
  height: 100%;
  padding: 28px;
}

.reveal__item {
  --k-when: view once;
  --k-opacity: 0 1;
  --k-y: 28px 0;
  --k-stagger: 90ms;
  --k-motion: soft;
  display: flex;
  gap: 14px;
  align-items: center;
  padding: 14px 16px;
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 8px 24px -16px rgb(12 19 36 / 0.4);
}

.reveal__item b {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--c);
}

.reveal__item span {
  flex: 1;
  height: 10px;
  border-radius: 5px;
  background: #e3e8f2;
}

scroll and page

These follow scrolling continuously.

  • scroll is the element's trip through the viewport: 0 when its top enters at the bottom, 1 when its bottom leaves at the top.
  • page is the whole document: 0 at the top, 1 at the bottom.
.tile {
  --k-when: scroll;
  --k-rotate: -120deg 120deg;
}

.reading-progress {
  --k-when: page;
  --k-motion: instant;
  scale: var(--k-progress) 1;
  transform-origin: left;
}
Scroll the pageEdit
.scrolly {
  display: grid;
  place-items: center;
  height: 100%;
}

.scrolly__bar {
  --k-when: page;
  --k-motion: instant;
  position: absolute;
  inset: 0 0 auto 0;
  height: 6px;
  background: #2f6bff;
  scale: var(--k-progress) 1;
  transform-origin: left;
}

.scrolly__tile {
  --k-when: scroll;
  --k-rotate: -120deg 120deg;
  --k-scale: 0.7 1.15;
  width: 120px;
  height: 120px;
  border-radius: 30px;
  background: conic-gradient(from 45deg, #2f6bff, #7c5cff, #ff5d6c, #2f6bff);
}

The progress bar reads --k-progress in its own CSS. See Reaction progress.

Stagger and delay

--k-delay waits before a hover, press, or view reaction starts. --k-stagger adds that delay once per sibling, so a list reacts in order.

.list > li {
  --k-when: view once;
  --k-opacity: 0 1;
  --k-y: 20px 0;
  --k-stagger: 60ms;
}

The order is the element's position among its Kinesis siblings. --k-index sets it yourself. Text splitting sets it on every character and word.

Motion

Reactions use the element's --k-motion. Springs can overshoot. With bouncy, --k-scale: 1.2 passes 1.2 briefly and settles back. Opacity stays between 0 and 1.

Edit this page on GitHub