Motion · GSAP-style

Transforms

Move with x/y, spin with rotate, grow with scale, lean with skewX — each independently, composed into one GPU-friendly transform.

Writing style.transform by hand means every animation must know every other: set a rotation and you lose the translation. jsKjs keeps each component per element and rebuilds translate() rotate() scale() skew() on every frame, so you can tween y on hover while a separate tween spins the same element.

Transforms don't trigger layout, which is why they stay smooth on phones. Prefer them to left, top, width and height whenever you can.

Live · Transform playground

View the source for this demo
HTML
<div class="tf-grid">
  <div class="tf-stage" aria-hidden="true"><div id="tf-card">$jsKjs</div></div>
  <div class="tf-ctrls" id="tf-ctrls">
    <div class="field"><label for="tf-x">x: <span>0</span></label><input id="tf-x" data-prop="x" type="range" min="-140" max="140" value="0"></div>
    <div class="field"><label for="tf-y">y: <span>0</span></label><input id="tf-y" data-prop="y" type="range" min="-80" max="80" value="0"></div>
    <div class="field"><label for="tf-rotate">rotate: <span>0</span></label><input id="tf-rotate" data-prop="rotate" type="range" min="-180" max="180" value="0"></div>
    <div class="field"><label for="tf-scale">scale: <span>1</span></label><input id="tf-scale" data-prop="scale" type="range" min="0.4" max="1.8" step="0.05" value="1"></div>
    <div class="field"><label for="tf-skewX">skewX: <span>0</span></label><input id="tf-skewX" data-prop="skewX" type="range" min="-30" max="30" value="0"></div>
    <div class="row"><button type="button" id="tf-spin">Spin 360° (keeps the rest)</button><button type="button" id="tf-reset">Reset</button></div>
  </div>
</div>
<pre class="out" id="tf-out"></pre>
JavaScript
const state = { x: 0, y: 0, rotate: 0, scale: 1, skewX: 0 };
const show = () => $('#tf-out').text("style.transform → '" + $('#tf-card')[0].style.transform + "'");

$('#tf-ctrls').on('input', 'input[type=range]', async function () {
  state[this.dataset.prop] = +this.value;
  $(this).prev().find('span').text(this.value);
  await $.to('#tf-card', { ...state, duration: .25, ease: 'power2.out' });
  show();
});

$('#tf-spin').on('click', async () => {
  await $.to('#tf-card', { rotate: state.rotate + 360, duration: .8, ease: 'power2.inOut' });   // only rotate changes
  $.to('#tf-card', { rotate: state.rotate, duration: 0 });
  show();
});

$('#tf-reset').on('click', async () => {
  Object.assign(state, { x: 0, y: 0, rotate: 0, scale: 1, skewX: 0 });
  $('#tf-ctrls input[type=range]').each(function () {
    $(this).val(state[this.dataset.prop]).prev().find('span').text(state[this.dataset.prop]);
  });
  await $.to('#tf-card', { ...state, duration: .5, ease: 'back.out' });
  show();
});
show();

More examples

Hover lift that does not fight a spin

JavaScript
$('.card').hover(
  function () { $(this).to({ y: -6, scale: 1.03, duration: .15 }); },
  function () { $(this).to({ y: 0, scale: 1, duration: .2 }); });
$.to('.card .icon', { rotate: 360, duration: 2, ease: 'linear' });

API

$.to(target, { …props, duration, delay, ease, stagger, onComplete }) → Promise

Tween to a state. stagger offsets each element’s start; onComplete fires after the last finishes. Also chainable on sets: $(sel).to(props).

Reference: $.to() →
$.fromTo(target, fromProps, toProps) → Promise

Explicit start and end states.

Reference: $.fromTo() →

Migrating

Coming from GSAPSame shorthand names (rotation is accepted as an alias of rotate). xPercent, yPercent, z, 3D rotations and transformOrigin are not included — set transform-origin in CSS.

Related features