Transitions

Ajax uses the View Transitions API. Each registration can declare one or more transition types — short strings set on html via :active-view-transition-type() while the transition runs. This lets CSS drive the animation entirely.

Declaring transition types

Pass one or more strings in the transitions array on a registration:

ajax.register({
  target: "a[href]",
  transitions: ["slide-left"],
  plugins: [history("push")],
  swaps: [
    { replace: "#main" },
  ],
})

All transition types in a registration are active simultaneously during the startViewTransition call that wraps that registration's swaps.

Dynamic transition types

transitions also accepts a function that receives the completed AjaxContext and returns the types to use. This lets you choose a transition based on the response — for example the HTTP status code, a response header, or anything else available after the fetch:

ajax.register({
  target: "a[href]",
  transitions: (ctx) => {
    if (ctx.response?.status === 404) return ["fade"]
    return ["slide-left"]
  },
  plugins: [history("push")],
  swaps: [{ replace: "#main" }],
})

The function is called after the request completes, so ctx.response and ctx.incomingDocument are both available.

Animating with CSS

Scope both the view-transition-name and the animations inside the transition type block so they only apply during that specific transition:

html:active-view-transition-type(slide-left) {
  #main {
    view-transition-name: main;
  }
  &::view-transition-old(main) {
    animation: 300ms ease both slide-out-left;
  }
  &::view-transition-new(main) {
    animation: 300ms ease both slide-in-right;
  }
}

@keyframes slide-out-left {
  to {
    transform: translateX(-32px);
    opacity: 0;
  }
}
@keyframes slide-in-right {
  from {
    transform: translateX(32px);
    opacity: 0;
  }
}

Direction based on navigation

Use two registrations — one for forward, one for back — each with a different transition type:

<nav>
  <a href="/step-1" id="link-back">Back</a>
  <a href="/step-3" id="link-forward">Next</a>
</nav>

<section id="main">
  <h1>Step 2</h1>
</section>

<script type="module">
  import ajax, { history } from "@erikt/ajax"

  ajax.register({
    target: "#link-forward",
    transitions: ["slide-forward"],
    plugins: [history("push")],
    swaps: [{ replace: "#main" }],
  })

  ajax.register({
    target: "#link-back",
    transitions: ["slide-back"],
    plugins: [history("push")],
    swaps: [{ replace: "#main" }],
  })
</script>
html:active-view-transition-type(slide-forward) {
  #main {
    view-transition-name: main;
  }
  &::view-transition-old(main) {
    animation: 300ms ease both slide-out-left;
  }
  &::view-transition-new(main) {
    animation: 300ms ease both slide-in-right;
  }
}

html:active-view-transition-type(slide-back) {
  #main {
    view-transition-name: main;
  }
  &::view-transition-old(main) {
    animation: 300ms ease both slide-out-right;
  }
  &::view-transition-new(main) {
    animation: 300ms ease both slide-in-left;
  }
}

@keyframes slide-out-left  { to   { transform: translateX(-32px); opacity: 0; } }
@keyframes slide-out-right { to   { transform: translateX(32px);  opacity: 0; } }
@keyframes slide-in-left   { from { transform: translateX(-32px); opacity: 0; } }
@keyframes slide-in-right  { from { transform: translateX(32px);  opacity: 0; } }

Multiple transition types

A registration with multiple transition types has all of them active at once. Use scoped durations to prevent one long animation from holding another open:

ajax.register({
  target: "#filter-form",
  transitions: ["update-list", "fade-count"],
  swaps: [
    { replace: "#results" },
    { replace: "#result-count" },
  ],
})
html:active-view-transition-type(update-list) {
  #results {
    view-transition-name: results;
  }
  &::view-transition-old(results) { animation: 200ms ease both fade-out; }
  &::view-transition-new(results) { animation: 200ms ease both fade-in; }
}

html:active-view-transition-type(fade-count) {
  #result-count {
    view-transition-name: result-count;
  }
  &::view-transition-old(result-count) { animation: 150ms ease both fade-out; }
  &::view-transition-new(result-count) { animation: 150ms ease both fade-in; }
}

@keyframes fade-out { to   { opacity: 0; } }
@keyframes fade-in  { from { opacity: 0; } }

Per-swap transition types

Each swap config accepts its own transition option — a string or a function returning a string. It is concatenated with the registration-level transitions array for that request.

ajax.register({
  target: "#cart-page form",
  swaps: [
    {
      replace: "#cart-list",
      transition: "update-list",
    },
    {
      replace: "#cart-button",
      transition: "update-count",
    },
  ],
})

Both update-list and update-count will be active during the transition.

Conditional inclusion with if

If a swap config has an if callback, its transition is only included when at least one matched element actually swaps. This lets a single registration trigger different animations depending on what changes:

ajax.register({
  target: "#cart-page form",
  swaps: [
    {
      replace: "#cart-list",
      mode: "outerHTML",
      // transition only runs when the list actually changes shape
      transition: "reorder-list",
      if: (current, next) => current.children.length !== next.children.length,
    },
    {
      replace: "#cart-button",
      transition: "update-count",
    },
  ],
})

If if returns false for every matched element, reorder-list is omitted from the startViewTransition call for that request. update-count is always included because its swap has no guard.

Per-element names (FLIP lists)

Give each repeated element a unique view-transition-name so the browser can animate individual items moving, entering, and leaving:

<!-- server renders the style attribute with a stable per-item id -->
<ul id="list">
  <li style="view-transition-name: item-1">Apples</li>
  <li style="view-transition-name: item-2">Bananas</li>
  <li style="view-transition-name: item-3">Cherries</li>
</ul>
ajax.register({
  target: "#sort-form",
  transitions: ["sort"],
  swaps: [{ replace: "#list" }],
})
html:active-view-transition-type(sort) {
  &::view-transition-group(*) {
    animation-duration: 300ms;
    animation-timing-function: ease;
  }
}

The browser automatically FLIP-animates each <li> from its old position to its new one. No keyframes needed — view-transition-group handles the positional interpolation.