Ever wondered how to add more joy to components and partials injected or removed from the DOM? Something like this:
Previously I used a solution that relied on event callbacks from turbo. It did its job, but I was never really happy with the solution nor with its usage.
So I am introducing: Turbo Transition: A “minion” for Turbo-Frames and Streams that transitions elements as they enter or leave the DOM. ✨ A way simpler, but equally powerful solution than what I had before.
Since I’ve been working actively on Rails Designers I have been exploring all kinds of interesting techniques. Turbo Transition, just like turbo-frame
and turbo-stream
is nothing more than a custom element that adds and removes defined CSS classes.
Check it out for your nextcurrent Turbo-powered app.
Interested how Turbo Transition and custom elements work? Continue reading. 👇
How Turbo Transitions works
Turbo Transition is nothing more than a custom element. They let you create your own HTML tags with built-in behavior (like extending HTML’s vocabulary). Here’s the minimal setup:
class MyElement extends HTMLElement {
// Called when element is added to the page
connectedCallback() {
// Element is now in the DOM
}
// Called when element is removed
disconnectedCallback() {
// Clean up time
}
}
// Register so browsers know about the new element
customElements.define("my-element", MyElement);
Turbo Transition builds on this foundation to handle animations when elements enter or leave the page:
class TurboTransition extends HTMLElement {
connectedCallback() {
// Handle enter animations
}
remove() {
// Handle leave animations
}
}
customElements.define("turbo-transition", TurboTransition);
From here, it manages transition classes, timing, and cleanup.
class TurboTransition extends HTMLElement {
connectedCallback() {
if (this.#config.hasEnterTransition()) {
this.#enter();
}
}
remove() {
if (this.#config.hasLeaveTransition()) {
this.#leave();
return this;
}
return super.remove();
}
}
The transition process is straightforward:
async #transition({ to, element = null }) {
const target = element || this.firstElementChild;
if (!target) return;
const classes = this.#config.getClasses({ for: to });
await this.#utilities.run(target, classes);
}
- Get the target element (Turbo Transition requires one child element inside it);
- Fetch the relevant classes from attributes (eg.
enter-from-class="fade-enter-from"
); - Run the transition sequence.
The remove()
method uses cloning to keep the animation visible while the original element is removed:
#leave() {
const clone = this.cloneNode(true);
const parent = this.parentNode;
parent.replaceChild(clone, this);
// Run transition on the clone's content
this.#transition({ to: "leave", element: clone.firstElementChild })
.then(() => clone.parentNode?.removeChild(clone));
}
Classes are applied in a specific sequence to create smooth transitions:
// utilities.js
async run(element, classes) {
this.#applyInitialState(element, classes); // Add 'from' + 'active'
await this.#nextFrame(); // Wait for browser
this.#applyFinalState(element, classes); // Switch to 'to' state
await this.#waitForCompletion(element); // Wait for animation
this.#cleanup(element, classes); // Remove classes
}
This creates a reliable way to add smooth transitions to your components and elements that works seamlessly with Turbo Frames and Turbo Streams.
Check it out and give it that star! ⭐