Sign in to use Study Buddy
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-animateto animate enter/exit/change, -
data-turbo-refresh-versionfor 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:
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:
-
animation hooks for enter/exit/change,
-
reliable change detection with version tracking,
-
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 |
|
0 |
| Time taken | 0.0 |
|
1 |
| Totals | 0 | 0% | 1 |