Transitions

transition says how an element gets to its target: a spring, a tween, or keyframes on the way. Press Move and the three take the same trip their own way.

Live demo
transitions/solid2.tsx
import { w } from "@wardic/solid/unstable/v2"
import { createSignal } from "solid-js"

/** One toggle, three ways to get there: a spring, a tween, and keyframes. */
export default function Transitions() {
  const [moved, setMoved] = createSignal(false)
  return (
    <div class="sample-stack">
      <button class="sample-button" onClick={() => setMoved(!moved())}>
        Move
      </button>
      <w.div class="sample-box" animate={{ x: moved() ? 160 : 0 }} transition={{ type: "spring", stiffness: 300, damping: 14 }}>
        spring
      </w.div>
      <w.div class="sample-box" animate={{ x: moved() ? 160 : 0 }} transition={{ duration: 0.6, ease: "easeInOut" }}>
        tween
      </w.div>
      <w.div class="sample-box" initial={false} animate={{ x: moved() ? [0, 200, 160] : [160, -40, 0] }} transition={{ duration: 0.8, times: [0, 0.6, 1] }}>
        keyframes
      </w.div>
    </div>
  )
}
transitions/solid1.tsx
import { w } from "@wardic/solid"
import { createSignal } from "solid-js"

/** One toggle, three ways to get there: a spring, a tween, and keyframes. */
export default function Transitions() {
  const [moved, setMoved] = createSignal(false)
  return (
    <div class="sample-stack">
      <button class="sample-button" onClick={() => setMoved(!moved())}>
        Move
      </button>
      <w.div class="sample-box" animate={{ x: moved() ? 160 : 0 }} transition={{ type: "spring", stiffness: 300, damping: 14 }}>
        spring
      </w.div>
      <w.div class="sample-box" animate={{ x: moved() ? 160 : 0 }} transition={{ duration: 0.6, ease: "easeInOut" }}>
        tween
      </w.div>
      <w.div class="sample-box" initial={false} animate={{ x: moved() ? [0, 200, 160] : [160, -40, 0] }} transition={{ duration: 0.8, times: [0, 0.6, 1] }}>
        keyframes
      </w.div>
    </div>
  )
}
transitions/motion.tsx
import { motion } from "motion/react"
import { useState } from "react"

/** One toggle, three ways to get there: a spring, a tween, and keyframes. */
export default function Transitions() {
  const [moved, setMoved] = useState(false)
  return (
    <div className="sample-stack">
      <button className="sample-button" onClick={() => setMoved(!moved)}>
        Move
      </button>
      <motion.div className="sample-box" animate={{ x: moved ? 160 : 0 }} transition={{ type: "spring", stiffness: 300, damping: 14 }}>
        spring
      </motion.div>
      <motion.div className="sample-box" animate={{ x: moved ? 160 : 0 }} transition={{ duration: 0.6, ease: "easeInOut" }}>
        tween
      </motion.div>
      <motion.div className="sample-box" initial={false} animate={{ x: moved ? [0, 200, 160] : [160, -40, 0] }} transition={{ duration: 0.8, times: [0, 0.6, 1] }}>
        keyframes
      </motion.div>
    </div>
  )
}

Springs

A spring is set by its physics, stiffness, damping and mass, or by time:

  • { type: "spring", duration: 0.5, bounce: 0.25 } lands on its target at 0.5 s. bounce is 0.3 unless you give one.
  • { visualDuration: 0.5, bounce: 0.25 } does most of its move in 0.5 s, then settles. bounce is 0 here.

A spring keeps the velocity it's handed: retarget one mid-flight, or release a drag into it, and it carries on from there. With no type, any spring field (stiffness, damping, mass, bounce, visualDuration) makes a spring.

Tweens

A tween runs for duration seconds along ease: one of Motion's names ("easeOut", "easeInOut", "backOut", "anticipate"…), a cubic Bézier's four numbers ([0.25, 0.1, 0.35, 1]), or a function. The functions are exported by the same names: import { easeOut, cubicBezier, steps } from "@wardic/solid".

Keyframes

A target that's a list plays through its stops: animate={{ x: [0, 200, 160] }}. times places each stop between 0 and 1, and ease can be a list, one per stretch. A first stop of null is wherever the value is now. Keyframes play at mount too, as Motion's do; with initial={false}, the element mounts at the last stop.

Along a curve

transition.path moves x and y together along a path, on x's transition, in place of two straight lines. arc() makes one: a curve from where the element is to its target.

Live demo
arc/solid2.tsx
import { arc, w } from "@wardic/solid/unstable/v2"
import { createSignal } from "solid-js"

/** A ball tossed across along a curve, and back. */
export default function Arc() {
  const [across, setAcross] = createSignal(false)
  // One arc, made once: with no `direction`, each trip keeps to the same side of the screen.
  const toss = arc({ strength: 0.6 })
  return (
    <div class="sample-stack">
      <button class="sample-button" onClick={() => setAcross(!across())}>
        Toss
      </button>
      <div class="sample-track">
        <w.div class="sample-ball" animate={{ x: across() ? 240 : 0 }} transition={{ path: toss, duration: 0.7, ease: "easeInOut" }} />
      </div>
    </div>
  )
}
arc/solid1.tsx
import { arc, w } from "@wardic/solid"
import { createSignal } from "solid-js"

/** A ball tossed across along a curve, and back. */
export default function Arc() {
  const [across, setAcross] = createSignal(false)
  // One arc, made once: with no `direction`, each trip keeps to the same side of the screen.
  const toss = arc({ strength: 0.6 })
  return (
    <div class="sample-stack">
      <button class="sample-button" onClick={() => setAcross(!across())}>
        Toss
      </button>
      <div class="sample-track">
        <w.div class="sample-ball" animate={{ x: across() ? 240 : 0 }} transition={{ path: toss, duration: 0.7, ease: "easeInOut" }} />
      </div>
    </div>
  )
}
arc/motion.tsx
import { arc, motion } from "motion/react"
import { useState } from "react"

// One arc, made once: with no `direction`, each trip keeps to the same side of the screen.
// `y: 0` as well: a toss caught mid-air curves back down to the line, not to where it was caught.
const toss = arc({ strength: 0.6 })

/** A ball tossed across along a curve, and back. */
export default function Arc() {
  const [across, setAcross] = useState(false)
  return (
    <div className="sample-stack">
      <button className="sample-button" onClick={() => setAcross(!across)}>
        Toss
      </button>
      <div className="sample-track">
        <motion.div className="sample-ball" animate={{ x: across ? 240 : 0, y: 0 }} transition={{ path: toss, duration: 0.7, ease: "easeInOut" }} />
      </div>
    </div>
  )
}
  • strength is how far it bows, and peak where along the way it's highest.
  • direction picks the side. Without one, an arc() made once (in the component, as above) keeps to the same side of the screen on every trip.
  • rotate turns the element along the curve, on top of its own rotate.
  • Caught mid-air, it curves back to its line: an axis the target leaves out ends where the path in flight was taking it. Motion's ends where it was caught, so its tab gives y: 0 too.
  • Under reduced motion, x and y snap to their target.

A path is any (from, to) => (t) => ({ x, y, rotate? }), so a curve of your own works too. Layout animations follow a path as well: a move or a layoutId handover of 20 px or more curves its centre. Saved for later flies each photo into the cart along one.

Per value

A value can have a transition of its own, under its name, with default for the rest:

<w.div animate={{ x: 100, opacity: 1 }} transition={{ default: { type: "spring" }, opacity: { duration: 0.2 } }} />

delay waits before it starts, and repeat, repeatType ("loop", "reverse", "mirror") and repeatDelay play it again.

At the end

transitionEnd sets values once the target's own have landed, such as hiding a panel after it fades:

<w.div animate={{ opacity: 0, transitionEnd: { display: "none" } }} />

With no transition

Each value runs on Motion's default for it: x, y and rotate on a spring, scale on a stiffer one, and everything else, opacity and colours among them, on a 0.3 s ease. A WardicConfig sets one for every element below it: <WardicConfig transition={{ type: "spring", bounce: 0.2 }}>.