The Transition API at a Glance

Svelte’s transition directive gives components a first-class way to animate into and out of the document. The default path uses CSS animations, which keep the browser’s main thread free. Basic usage is <element transition:transitionFunction />, with in: and out: variants available when you only need one direction.

Out of the box, the svelte/transition runtime package ships with seven prepackaged transition functions, all of which support parameter tuning. Combined with the svelte/easing package, these cover a wide range of interactions without writing any animation logic yourself.

The Shape of a Custom Transition

For finer control, Svelte lets you supply your own transition function–so long as it follows a few conventions:

transition = (node: HTMLElement, params: any) => {
  delay?: number,
  duration?: number,
  easing?: (t: number) => number,
  css?: (t: number, u: number) => string,
  tick?: (t: number, u: number) => void
} 

A transition function receives the DOM node where the directive is used and must return an object describing the animation. That object must include either a css function (which returns a CSS string for the animation) or a tick function (which uses JavaScript for full control, at the cost of performance since it bypasses CSS animations).

Both functions are conventionally written with parameters (t, u). The value t moves from 0.00 to 1.00 as an element enters the DOM, and from 1.00 back to 0.00 as it leaves. The u parameter is simply 1 - t. For instance, returning transform: scale(${t}) would animate your element from 0 to 1 on entry, and reverse on exit.

A Working Example

To see this in action, we start with toggleable boilerplate: an #if block that conditionally mounts and unmounts an element. Transitions only fire when an element actually enters or leaves the DOM, so this is a fitting testbed:

<script>
  let showing = true
</script>

<label for="showing">
  Showing
</label>
<input id="showing" type="checkbox" bind:checked={showing} />

{#if showing}
  <h1>Hello custom transition!</h1>
{/if}

Initially, toggling the checkbox makes the element appear and vanish abruptly. To intercept that behavior, we wire up our custom transition:

<script>
  let showing = true
  // Custom transition function
  function whoosh(node) {
    console.log(node)
  }
</script>

<label for="showing">
  Showing
</label>
<input id="showing" type="checkbox" bind:checked={showing} />

{#if showing}
  <h1 transition:whoosh>Hello custom transition!</h1>
{/if}

Toggling now logs the <h1> element to the console, confirming the transition is connected. While we won't use the node in this example, it's commonly valuable for referencing live styles or dimensions.

Without a css or tick function, nothing animates. Adding a css function that returns a scale transform, plus a duration property, gives us movement–though it jumps straight to 0.5 scale instead of easing into it:

<script>
  function swoop() {
    return {
      duration: 1000,
      css: () => `transform: scale(.5)`
    }
  }
  let showing = true
</script>

<!-- markup -->

That jump is where (t, u) matter. Using t inside the returned CSS lets the scale move smoothly from 0.00 to 1.00:

<script>
  function swoop() {
    return {
      duration: 1000,
      css: (t) => `transform: scale(${t})`
    }
  }
  let showing = true
</script>

<!-- markup -->

This smoothed version is effectively a manual recreation of the built-in scale transition.

Building a “Swoop” Transition

To add a bit more flair, we can extend the transform with translateX, so the element zooms in from the side on entry and back out on exit. The challenge: translate to 100% when leaving, back to 0% on entering.

One valid implementation looks like this:

css: (t, u) => `transform: scale(${t}) translateX(${u * 100}%);`

The key is using the second parameter for the translation. On entry, the element should end at scale(1) translateX(0%), so using unaltered t for both transforms won't work. Since u is 1 - t, it hits 0 exactly when t is 1–perfect for the target state. Multiplying u by 100 converts it to a percentage string.

Understanding the interplay between t and u unlocks a great deal of dynamism. The pair can be combined, divided, or otherwise manipulated to produce the easing behavior you need. Finally, an easing function from svelte/easing can be applied to polish the motion:

<script>
  import { elasticOut } from 'svelte/easing'
  function swoop() {
    return {
      duration: 1000,
      easing: elasticOut,
      css: (t, u) => `transform: scale(${t}) translateX(${u * 100}%)`
    }
  }
  let showing = true
</script>

<label for="showing">
  Showing
</label>
<input id="showing" type="checkbox" bind:checked={showing} />

{#if showing}
  <h1 transition:swoop>Hello custom transition!</h1>
{/if}

That's the core of authoring custom transitions in Svelte. With the pattern and parameters in hand, it's worth exploring the official docs and the Svelte tutorial to see how far they can be pushed.