Sign in to use Study Buddy

Turbo Refresh Animations

The rough edge after morphing

In the previous lesson, we enabled Turbo Drive and morphing. That got us surprisingly far with almost no extra code.

But one rough edge remains in single-user flows: changes often happen with no visual cue. Items pop in, pop out, or update in place, and users can lose track of what just changed.

This lesson is about fixing that with small, targeted animation hooks.

The tool

We’ll use turbo-refresh-animations, an NPM package that layers animation hooks on top of Turbo refresh morphing.

For this lesson, we’ll stay strictly in single-user use cases:

  • data-turbo-refresh-animate to animate enter/exit/change,

  • data-turbo-refresh-version for more reliable change detection.

We’ll handle streamed refresh protection and multi-user concerns in the next lesson.

Demo app

We’ll use a simple To-Do List demo app:

This project is not graded

After you fork the project, run rake sample_data, then bin/server and have a look around at the app and the codebase.

For this lesson, at any time you can review all changes I’ve made here.

Step 1 — Make sure morphing is enabled

If you completed the previous lesson, these tags are already in place in the <head>:

app/views/layouts/application.html.erb
1
2
<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">

That enables same-URL morphing and preserves scroll position.

Step 2 — Add the package

For this demo, we’ll use importmaps + jsDelivr.

Add this line to the bottom of config/importmap.rb:

config/importmap.rb
1
pin "turbo-refresh-animations", to: "https://cdn.jsdelivr.net/npm/turbo-refresh-animations@0.0.1/turbo-refresh-animations.js"

And add this line to app/javascript/application.js:

app/javascript/application.js
1
import "turbo-refresh-animations"

Step 3 — Define animation CSS

The package adds CSS classes during morph operations. You define what those classes do, by adding a new app/assets/stylesheets/turbo-refresh-animations.css stylesheet:

app/assets/stylesheets/turbo-refresh-animations.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.turbo-refresh-enter {
  animation: turbo-refresh-enter 300ms ease-out;
}

@keyframes turbo-refresh-enter {
  from { opacity: 0; }
  to { opacity: 1; }
}

.turbo-refresh-exit {
  animation: turbo-refresh-exit 300ms ease-out forwards;
}

@keyframes turbo-refresh-exit {
  from { opacity: 1; }
  to { opacity: 0; }
}

.turbo-refresh-change {
  animation: turbo-refresh-change 800ms ease-out;
}

@keyframes turbo-refresh-change {
  from { background-color: #FFF3CD; }
  to { background-color: inherit; }
}

@media (prefers-reduced-motion: reduce) {
  .turbo-refresh-enter,
  .turbo-refresh-change,
  .turbo-refresh-exit {
    animation-duration: 1ms;
    animation-iteration-count: 1;
  }
}

Don’t forget to import this stylesheet using your app’s normal stylesheet pipeline:

app/assets/stylesheets/application.css
1
2
@import "custom-image.css";
@import "turbo-refresh-animations.css";

Step 4 — Opt elements into animation

Add data-turbo-refresh-animate to elements you want animated.

Important: each animated element needs a stable id so Turbo/idiomorph can track it across morphs.

app/views/list_templates/show.html.erb
1
2
3
4
5
6
7
<div
  id="<%= dom_id(an_item) %>"
  data-turbo-refresh-animate
  style="margin-bottom: 20px;"
>
  <%# ... item content ... %>
</div>

Each item <div> already has a stable id (via Rails’ dom_id helper). Just add the data-turbo-refresh-animate attribute. Now new items animate in as enter transitions, and deleted items animate out as exit transitions.

Step 5 — Improve change detection

You may notice that toggling a checkbox does not trigger a change animation.

By default, turbo-refresh-animations compares textContent. When you toggle a checkbox, the item’s completed attribute changes, but the visible text stays the same — so the element is treated as unchanged.

To fix that, provide explicit version tracking:

app/views/list_templates/show.html.erb
1
2
3
4
5
6
7
8
<div
  id="<%= dom_id(an_item) %>"
  data-turbo-refresh-animate
  data-turbo-refresh-version="<%= an_item.cache_key_with_version %>"
  style="margin-bottom: 20px;"
>
  <%# ... item content ... %>
</div>

Now any update to an item — even if the visible text doesn’t change — will trigger the change animation.

Step 6 — Customize behavior

Disable specific animation types

All three animation types are enabled by default. You can disable specific ones:

app/views/list_templates/show.html.erb
1
2
3
4
5
6
7
8
9
<div
  id="<%= dom_id(an_item) %>"
  data-turbo-refresh-animate
  data-turbo-refresh-version="<%= an_item.cache_key_with_version %>"
  data-turbo-refresh-enter="false"
  style="margin-bottom: 20px;"
>
  <%# ... item content ... %>
</div>

Use a custom class name

If the default background flash doesn’t fit your design, define your own class:

app/assets/stylesheets/turbo-refresh-animations.css
1
2
3
4
5
6
7
8
.turbo-refresh-change-outline {
  animation: turbo-refresh-change-outline 800ms ease-out;
}

@keyframes turbo-refresh-change-outline {
  from { box-shadow: inset 0 0 0 3px #FFC107; }
  to { box-shadow: inset 0 0 0 3px transparent; }
}

Then select it with data-turbo-refresh-change:

app/views/list_templates/show.html.erb
1
2
3
4
5
6
7
8
9
10
<div
  id="<%= dom_id(an_item) %>"
  data-turbo-refresh-animate
  data-turbo-refresh-version="<%= an_item.cache_key_with_version %>"
  data-turbo-refresh-enter="false"
  data-turbo-refresh-change="turbo-refresh-change-outline"
  style="margin-bottom: 20px;"
>
  <%# ... item content ... %>
</div>

You can do the same for enter/exit with data-turbo-refresh-enter and data-turbo-refresh-exit.

Summary

For single-user flows, turbo-refresh-animations adds three practical upgrades with very little code:

  1. animation hooks for enter/exit/change,

  2. reliable change detection with version tracking,

  3. easy per-element customization.

You keep the same server-rendered Rails workflow, but users can now track changes much more easily.

Next lesson

Next we’ll move to multi-user flows: broadcast refreshes, form protection during streamed updates, and stale-data warnings.


Assessment Details
Review your overall progress for this lesson
Assessment Title Earned Points Current Progress Assessment Points
ToDo List Project 0.0
100%
0
Time taken 0.0
0%
1
Totals 0 0% 1