Flash messages with Turbo Streams (Bootstrap Alert)

The goal: show flash messages from turbo_stream rendering with animation, fadeout and remove it after the animation has finished

Rails controller: set the flash message

/app/controllers/tasks_controller.rb
def create
  @task = @list.tasks.build(task_params)

  respond_with(@task, location: location) do |format|
    if @task.save
      format.turbo_stream { flash.now[:notice] = "Task successfully created" }
    end
  end
end
We use flash.now here since there is no redirect with turbo_stream.
respond_with since we uses the responders gem from https://github.com/heartcombo/responders. Your mileage may vary. Plain rails 7 generators don’t use respond_to (nor respond_with). See app/controllers/categories_controller.rb.

Layout

Very simple: just a <div> with an id where to show flash messages from turbo_stream rendering. Otherwise render existing flashes as usual. In case of turbo_stream rendering the content of the <div> will be replaced by turbo_stream content.

/app/views/layout/application.html.erb
<div class="container-fluid">
  <div id="flash"> (1)
    <%= render "shared/flash_alert" %>
  </div>
  <%= yield %>
</div>
1 id for turbo_stream replacing

A Helper to render Turbo Alerts

A helper to call from each create|update|destroy.turbo_stream.erb view: simply add the following snippet to each view:

create|update|destroy.turbo_stream.erb
<%= render_turbo_flash %>

The helper itself

app/helpers/flash_alert_helper.rb
module FlashAlertHelper
  def render_turbo_flash
    turbo_stream.prepend "flash", partial: "shared/flash_alert"
  end
end

A partial to go through each flash key

partial app/views/shared/_flash_alert.html.erb
<% flash.each do |severity,message| %>
  <%= render AlertComponent.new(severity: severity, message: message) %>
<% end %>

A view component for alerts

Since flashes goes with keys like :alert, :error, :notice, we need those translated in Bootstrap jargon like 'danger', 'info' or 'success'.

/app/components/alert_component.rb
# frozen_string_literal: true

class AlertComponent < ViewComponent::Base
  def initialize(severity:, message:)
    @level = update_severity(severity)
    @message = message
  end

private

  # bootstrapify names
  def update_severity(severity)
    case severity.to_sym
    when :alert, :error
      "danger"
    when :notice
      "info"
    else
      severity.to_s
    end
  end

end
/app/components/alert_component.html.erb
<div class="alert alert-<%= @level %> alert-dismissable fadeout" role="alert" 
     data-controller="remove"
     data-action="animationend->remove#remove"
>
  <span><%= @message %></span>
  <button type="button" class="btn-close float-end" data-bs-dismiss="alert" aria-label="Close"></button>
</div>

CSS for animating flash messages

app/assets/stylesheets/flash-animation.scss
// -- animation: fadeout for flash messages
.fadeout {
  animation: 4s fadeout;
}
@keyframes fadeout {
  0%, 100% { opacity: 0; }
  10%, 50% { opacity: 1; }
}
More information on css animations can be found on https://developer.mozilla.org/de/docs/Web/CSS/animation