
We recently built a quiz feature that needed to celebrate correct answers with confetti animations. The challenge wasn’t the confetti itself but coordinating between our Rails controller and the frontend JavaScript without mixing concerns or adding complexity.

The solution led us to discover how Turbo Stream’s extensibility can solve coordination problems in surprisingly elegant ways.
Starting Simple: A Custom Confetti Action
For our quiz, we started with something specific: a custom action that triggers confetti directly. Our Rails controller becomes remarkably clean:
class QuizController < ApplicationController
def create
if correct_answer?
render turbo_stream: turbo_stream.action(:confetti)
else
# Handle incorrect answer
end
end
end
The controller focuses purely on business logic, validating answers and deciding outcomes. When the answer is correct, it tells Turbo to trigger confetti, and that’s it.
To make this work, we extend Turbo.StreamActions
with custom behavior:
Turbo.StreamActions.confetti = function () {
launchConfetti();
};
Making It Generic: The Trigger Action
The real insight came when we needed similar coordination for other features. Instead of creating specific actions like confetti
, hideSpinner
, closeDialog
or playSound
, we could create one generic action that triggers JavaScript events.
Now our quiz controller can trigger any event by specifying it in the target:
class QuizController < ApplicationController
def create
if correct_answer?
render turbo_stream: turbo_stream.action(:trigger, "launch-confetti")
else
...
end
end
end
This works with one simple JavaScript function that transforms any Turbo Stream response into a custom DOM event:
Turbo.StreamActions.trigger = function () {
document.dispatchEvent(new Event(this.target));
};
Frontend Components as Event Listeners
With events, our frontend components become listeners that can react independently:
export default class extends Controller {
connect() {
document.addEventListener("launch-confetti", this.launchConfetti);
}
launchConfetti() {
🎉🎉🎉
}
}
When a correct answer triggers the “launch-confetti” event, our Stimulus controller responds with the celebration animation.
This pattern opens up many possibilities. Your Rails controllers can trigger events like:
hide-loading-spinner
after data loads successfullyplay-notification-sound
for important alertsupdate-progress-bar
during long-running processestrigger-analytics-event
for user behavior tracking
Each event can have multiple listeners, and listeners can be added or removed without affecting server code.
Why This Approach Wins
This pattern keeps Rails controllers focused on data and business logic while enabling rich frontend interactions. Testing becomes easier because you verify events fire without testing implementation details. Multiple components can coordinate without coupling to each other.
Custom Turbo Stream actions remind us that extending existing tools often beats adding new dependencies. Starting with something specific like confetti and then generalizing to events shows how the best abstractions often emerge from real use cases.