Deft
Ask AI

Search

Search skills, authors and topics

Jakubantalik / transitions.dev

transitions-dev

Production-ready CSS transitions for web apps. Use when implementing notification badges, dropdowns, modals, panel reveals, page transitions, card resizes, number pop-ins, text swaps, icon swaps, success checks, avatar group hovers, error state shakes, search/input clear, skeleton loaders, shimmer text, sliding tabs, tooltips, staggered text reveals, card hover tilt, plus-to-menu morph, accordions, toasts, like buttons, learn-more hovers, checkbox checks, spinning counters, toggles, AI thinking states, reasoning streams, streaming text, matrix dot loaders, or banner stacking. Triggers on "add a transition", "animate the dropdown", "make the modal open smoothly", "swap icon", "page slide", "stagger animation", "open / close transition", "make it animate", "fade between", "success animation", "form error", "shake on invalid", "hover lift", "avatar stack hover", "clear the search", "skeleton loader", "loading shimmer", "shimmer text", "sliding tabs", "segmented control", "tooltip", "reveal text", "tilt card", "3D hover tilt", "cursor glare", "plus to menu", "FAB morph", "accordion", "collapsible", "expand / collapse", "disclosure", "toast", "snackbar", "like button", "heart animation", "learn more arrow", "checkbox", "check animation", "spinning counter", "odometer", "slot machine digits", "toggle", "switch", "thinking states", "AI status line", "agent reasoning", "reasoning stream", "streaming text", "stream words in", "matrix loader", "dot loader", "banner stack", "stacked toasts". Also "motion tokens", "scan for ad-hoc transitions", "replace hardcoded durations with motion tokens", "tokenize my animations", and the commands transitions reveal, transitions review, transitions apply, transitions refine.

Install

npx skills add https://github.com/Jakubantalik/transitions.dev --skill transitions-dev

Card hover tilt

When to use

A card / tile / media surface that tilts in 3D toward the pointer while hovered, with a soft light "glare" tracking the cursor across it. Use for product cards, credit / membership cards, feature tiles, cover art — anything that should feel physical and reactive on hover. Pointer-only (skips touch) and flattens under reduced motion.

The pointer is tracked on an outer flat wrapper (.t-tilt) that never transforms, so the tilting card can't rotate its own edges out from under the cursor (which causes hover flicker). The inner .t-tilt-card is the element that actually rotates.

HTML usage

html
<div class="t-tilt">                <!-- flat hit area -->  <div class="t-tilt-card">       <!-- the element that tilts -->    … card content …    <div class="t-tilt-glare"></div>  </div></div>

Track the pointer on the OUTER .t-tilt (it never transforms) and write four custom properties from JS: el.style.setProperty('--tilt-rx', rxDeg + 'deg'); el.style.setProperty('--tilt-ry', ryDeg + 'deg'); el.style.setProperty('--tilt-gx', gxPct + '%'); el.style.setProperty('--tilt-gy', gyPct + '%'); Add .is-tilting while moving (fast follow) and .is-hover to fade the glare in; remove both on leave.

Tunable variables

VariableDefaultNotes
--tilt-perspective1000pxsourced from --p19-perspective
--tilt-return1000mssourced from --p19-return-dur
--tilt-return-easecubic-bezier(0.22, 1, 0.36, 1)sourced from --p19-return-ease
--tilt-follow400mssourced from --p19-follow-dur
--tilt-follow-easecubic-bezier(0.22, 1, 0.36, 1)sourced from --p19-follow-ease
--tilt-glare-opacity0.32sourced from --p19-glare-opacity
--tilt-glare-fade300mssourced from --p19-glare-fade
--tilt-glare-easecubic-bezier(0.22, 1, 0.36, 1)sourced from --p19-glare-ease

The :root defaults below match the live tuning on transitions.dev. Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single :root block.

css
:root {  --tilt-perspective: 1000px;  --tilt-return: 1000ms;  --tilt-return-ease: cubic-bezier(0.22, 1, 0.36, 1);  --tilt-follow: 400ms;  --tilt-follow-ease: cubic-bezier(0.22, 1, 0.36, 1);  --tilt-glare-opacity: 0.32;  --tilt-glare-fade: 300ms;  --tilt-glare-ease: cubic-bezier(0.22, 1, 0.36, 1);}

CSS

css
/* The outer wrapper is the flat hit area; touch-action:none   lets a finger drag tilt the card instead of scrolling the   page, so tap-hold-drag works on mobile. */.t-tilt { touch-action: none; }/* The card tilts toward the pointer via rotateX/rotateY fed   from JS; on leave it eases back to flat. A separate   .is-tilting class swaps in a short linear follow while the   pointer moves so the tilt tracks the cursor 1:1. */.t-tilt-card {  position: relative;  border-radius: 12px;  overflow: hidden;  transform:    perspective(var(--tilt-perspective))    rotateX(var(--tilt-rx, 0deg))    rotateY(var(--tilt-ry, 0deg));  transform-style: preserve-3d;  transition: transform var(--tilt-return) var(--tilt-return-ease);  will-change: transform;}.t-tilt-card.is-tilting {  transition: transform var(--tilt-follow) var(--tilt-follow-ease);}/* Cursor-tracked glare: layered soft circles that add like   light (screen blend) at the pointer position. */.t-tilt-glare {  position: absolute;  inset: 0;  pointer-events: none;  opacity: 0;  mix-blend-mode: screen;  background:    radial-gradient(circle 95px at var(--tilt-gx, 50%) var(--tilt-gy, 50%),      rgba(255,255,255,0.48), rgba(255,255,255,0.06) 52%, rgba(255,255,255,0) 84%),    radial-gradient(circle 200px at var(--tilt-gx, 50%) var(--tilt-gy, 50%),      rgba(255,255,255,0.22), rgba(255,255,255,0.04) 58%, rgba(255,255,255,0) 78%),    radial-gradient(circle 360px at var(--tilt-gx, 50%) var(--tilt-gy, 50%),      rgba(255,255,255,0.10), rgba(255,255,255,0) 88%);  transition: opacity var(--tilt-glare-fade) var(--tilt-glare-ease);}.t-tilt.is-hover .t-tilt-glare { opacity: var(--tilt-glare-opacity); }
@media (prefers-reduced-motion: reduce) {  .t-tilt-card { transform: none !important; transition: none !important; }}

The @media (prefers-reduced-motion: reduce) guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.

JavaScript orchestration

js
// Track the pointer on the OUTER .t-tilt (never transforms) and write// rotation + glare position onto the inner card. Works for mouse// (hover) and touch / pen (tap-hold-drag) — a touch pointermove only// fires while a finger is down, so the press naturally drives the tilt.const tilt = document.querySelector(".t-tilt");const card = tilt.querySelector(".t-tilt-card");const reduce = matchMedia("(prefers-reduced-motion: reduce)");
const MAX = 14; // peak tilt in degrees at the card edges (raise for a stronger lean)
function reset() {  tilt.classList.remove("is-hover");  card.classList.remove("is-tilting");  card.style.setProperty("--tilt-rx", "0deg");  card.style.setProperty("--tilt-ry", "0deg");}
function track(e) {  if (reduce.matches) return;  const r = tilt.getBoundingClientRect();  const px = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));  const py = Math.min(1, Math.max(0, (e.clientY - r.top) / r.height));  tilt.classList.add("is-hover");  card.classList.add("is-tilting");  card.style.setProperty("--tilt-ry", ((px - 0.5) * MAX).toFixed(2) + "deg");  card.style.setProperty("--tilt-rx", ((0.5 - py) * MAX).toFixed(2) + "deg");  card.style.setProperty("--tilt-gx", (px * 100).toFixed(1) + "%");  card.style.setProperty("--tilt-gy", (py * 100).toFixed(1) + "%");}
tilt.addEventListener("pointerdown", (e) => {  // Touch / pen: capture so the drag keeps targeting the card even if  // the finger drifts past its edge. Pair with touch-action: none on  // .t-tilt so the drag tilts instead of scrolling the page.  if (e.pointerType !== "mouse") {    try { tilt.setPointerCapture(e.pointerId); } catch (_) {}  }});tilt.addEventListener("pointermove", track);tilt.addEventListener("pointerup", reset);tilt.addEventListener("pointercancel", reset);tilt.addEventListener("pointerleave", (e) => {  // Mouse: leaving the card flattens it. Touch already reset on up.  if (e.pointerType === "mouse") reset();});

Peak tilt angle

The rotation magnitude is a JS constant (MAX, in degrees), not a CSS variable — the orchestration writes --tilt-rx / --tilt-ry from the pointer position scaled by MAX. Raise it for a stronger lean (the live demo goes up to ~40°); 10–16° reads as a subtle, tasteful tilt.

Why the pointer is tracked on the flat wrapper

Bind pointermove to the outer .t-tilt (which never transforms), not the .t-tilt-card that rotates. If you track the tilting element, its rotating edges slip out from under the cursor near the borders and the hover flickers on and off.

Touch / mobile

Because it uses Pointer Events, the tilt also works on touch: tap-hold-drag on the card and it follows your finger (a touch pointermove only fires while pressed). Two pieces make this reliable — touch-action: none on .t-tilt so the drag tilts instead of scrolling the page, and setPointerCapture on pointerdown so the gesture keeps targeting the card even if the finger drifts past its edge.