Flash messages are one of the most convenient ways to share transient messages with a user. They might be informative (“You have landed in Antarctica”) or they might offer a warning (“Hands off the penguins!”).
If you are not familiar with them, an ActionDispatch::Flash
provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action that sets flash[:notice] = “Post successfully created” before redirecting to a display action that can then expose the flash to its template.
It’s always surprised me that Rails leaves the rendering of flash messages completely up to you. If you don’t set up the rendering of flash messages, your users will never see them. It’s one of the first things I find myself doing on new projects.
Read on to learn about best practices, gotchas, and code examples that you can use to make your site…flashier! ✨
Ways to set flashes
Here are three different ways of setting an alert
:
flash.alert = "Don't steal a penguin!"
flash[:alert] = "Penguins are for remote appreciation only!"
redirect_to :penguins_path, alert: "I said no penguin stealing!"
And three different ways of setting a notice
:
flash.notice = "You see a majestic penguin"
flash[:notice] = "The majestic penguin sees you"
redirect_to :penguins_path, notice: "You and the majestic penguins see each other"
Can I use an arbitrary flash type?
Sure! But you have to set it by key. You cannot use an arbitrary =
setter nor pass it
as a parameter to redirect_to
.
# Will raise `undefined method 'penguin=' for an instance of ActionDispatch::Flash::FlashHash`.
flash.penguin = "This will raise"
# Will not raise or be shown to the user.
redirect_to :penguins_path, penguin: "This will not be set or shown to the user."
# This works!
flash[:penguin] = "This will be shown to the user"
flash vs. flash.now
Before we get into the rendering of flashes, let’s quickly cover the difference between flash
and flash.now
.
When you set flash
, the value is stored in session and persisted until the next request. This is useful for
showing a message after a redirect.
When you set flash.now
, the value is only persisted for the duration of the current request. This is useful
for showing a message before rendering.
# app/controllers/penguins_controller.rb
def create
@penguin = Penguin.new(penguin_params)
if @penguin.save
flash.alert = "Penguin saved from wanton theft."
redirect_to penguins_path
else
flash.now.alert = "Somebody stole a penguin!"
render :new
end
end
Where to render flashes
Flashes are typically rendered in your Application layout. As an absolute minimum, you could do this:
<!-- app/views/layouts/application.html.erb -->
<body>
<% flash.each do |_type, message| %>
<div role="alert"><%= message %></div>
<% end %>
<%= yield %>
</body>
Note that while the code seems to refer to a singular flash
, it is an ActionDispatch::Flash::FlashHash
that can have many different keys, which is why we enumerate over those keys with each
.
If this is as far as you get, you will at least see all your flash messages.
But they will be terrible. Really, keep reading.
Let’s render some partials!
A flexible structure that keeps your Application layout from becoming too busy is to render
a _flashes
partial that then renders an individual _flash
partial:
<!-- app/views/layouts/application.html.erb -->
<body>
<%= render "application/flashes" %>
<%= yield %>
</body>
<!-- app/views/application/_flashes.html.erb -->
<% flash.each do |type, messages| %>
<% Array(messages).each do |message| %>
<%= render "application/flash", type:, message: %>
<% end %>
<% end %>
<!-- app/views/application/_flash.html.erb -->
<div role="alert">
<%= message %>
</div>
You might be asking, “Why should I have two partials here? Why not just move the code in _flash.html.erb
into the loop in _flashes.html.erb
?”
The answer is that we are going to expand the functionality of each partial: _flashes
is going to
handle some high-level decision-making about each flash, and _flash
is going to handle the styling
of each individual flash. You’ll appreciate the separation by the time we’re done!
You might also be asking, “What’s that Array(messages)
wrapper doing in _flashes.html.erb
?”
Well, it allows you to set multiple flashes of the same type by passing in an array while also
allowing you to continue to pass a string:
# Both of these will work:
flash.notice = ["Penguin 1 is majestic", "Penguin 2 is majestic"]
flash.notice = "All penguins are majestic"
You might also also be asking, “What does the role="alert"
do?”
Two things:
- It is important as an accessibility marker for each message
- It allows Capybara accessible selectors to test for flashes with
assert_selector :alert
Dismissing flashes with Stimulus
It can be helpful to users to be able to “dismiss” individual flashes without reloading a page. For example, a message might be so long that it hides or moves the main view’s content in a way that makes it hard to read.
Fortunately, making flashes dismissible with a click is very easy!
We add a simple Stimulus controller (that has strong reuse energy):
// app/javascript/controllers/hide_on_click_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
hide() {
this.element.remove();
}
}
And then give each flash a button to do the dismissing:
<!-- app/views/application/_flash.html.erb -->
<div role="alert" data-controller="hide-on-click">
<span><%= message %></span>
<button data-action="hide-on-click#hide">X</button>
</div>
Now the user can click on the X to dismiss the flash.
Pro tip: Developing the styling of your flashes
An easy way to iterate on the visual appeal of your flashes is to pick a controller and an action and set a flash there:
# app/controllers/penguin_controller
class PenguinController < ApplicationController
def index
flash.alert = "Do not try to steal a majestic penguin!"
flash.notice = "You see a majestic penguin."
end
end
You can then make changes and manually reload your browser to see how your flashes are rendered.
Even more pro tip: Use Hotwire Spark for rapid iterations
Add
Hotwire Spark
to your Gemfile
in the group :development
section to get hot reloading for free! This will allow
you to make changes to your flash rendering code and see the results immediately without
having to remember to reload your browser.
Testing for flashes in tests
Are the flashes set?
In Minitest, you can query the flash
object directly in a controller test to see if a particular
flash has been set:
# tests/controllers/penguins_controller_test.rb
class PenguinsControllerTest < ActionDispatch::IntegrationTest
test "GET /penguins shows a notice when a penguin is sighted" do
survey_the_icy_landscape
get penguins_path
assert_response :ok
assert_equal "You see a majestic penguin.", flash.notice
end
test "GET /penguins shows an alert when you try to steal a penguin" do
stealthily_approach_a_majestic_penguin
get penguins_path
assert_response :ok
assert_equal "Do not try to steal a majestic penguin!", flash.alert
end
end
…but if your flash rendering code has a flaw, you might be setting the flashes while at the same time, no one is actually seeing them.
Are the flashes visible?
We could update our tests to use Capybara accessible selectors:
test "GET /penguins shows an alert when you try to steal a penguin" do
stealthily_approach_a_majestic_penguin
get penguins_path
assert_response :ok
assert_equal "Do not try to steal a majestic penguin!", flash.alert
assert_selector :alert, text: "Do not try to steal a majestic penguin!"
end
But this gets trickier the more precise you want your tests to be. For one, assert_selector :alert
can’t tell the difference between an alert and a notice unless you do something like also pass in
an ID or a class, so we end up having to add two assertions per flash to confirm that:
- The expected kind of flash was set
- The flash message is visible
We also probably want to be more specific about the number of flashes as well as their content
so we don’t have passing tests that should be failing. This means changing the assert_selector
to use count:
to match our expected count, exact_text:
so we don’t have unexpected values
in our flashes, and normalize_ws:
so we don’t have to also specify line breaks in tests:
assert_equal "Do not try to steal a majestic penguin!", flash.alert
assert_selector :alert, text: "Do not try to steal a majestic penguin!", count: 1, normalize_ws: true, exact_text: true
This is a lot of noise.
I’ve found that adding a few test helpers can make testing for flashes much easier. These will keep your tests tidy while also asserting that the flashes were set correctly and are visible.
These helpers have a few requirements:
- flashes are enclosed in a single
<div>
tag with anid
offlash
- each flash is enclosed in a
<div>
tag with anid
offlash-{type}-{index}
- you include two gems in your
Gemfile
:
# Gemfile
group :test do
gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors", tag: "v0.12.0"
gem "action_dispatch-testing-integration-capybara",
github: "thoughtbot/action_dispatch-testing-integration-capybara",
require: "action_dispatch/testing/integration/capybara/minitest"
[...]
# test/test_helper.rb
module ActiveSupport
class TestCase
[...]
def assert_flashes(messages, type:)
messages = Array(messages)
assert_equal messages, Array(flash[type])
assert_element "div", id: "flash", count: 1 do |parent|
messages.each_with_index do |text, index|
parent.assert_selector :alert, text:, id: "flash-#{type}-#{index}", normalize_ws: true, exact_text: true
end
end
end
def assert_no_flash(type:)
assert_nil flash[type]
assert_element "div", id: "flash-#{type}-0", count: 0
end
def assert_no_flashes
assert_empty flash
assert_element "div", id: "flash", count: 0
assert_no_flash(type: :alert)
assert_no_flash(type: :notice)
end
end
end
Here are some examples of these test helpers in action:
# after trying to access a page that requires authentication
assert_flashes "You need to sign in or sign up before continuing.", type: :alert
# after posting to the login endpoint
assert_flashes "Signed in successfully.", type: :notice
# after your session has timed out
assert_flashes "Your session expired. Please sign in again to continue.", type: :alert
assert_no_flash type: :timedout
# after some action that sets a series of custom flashes
assert_flashes ["Penguins are majestic", "Peguins are also cuddly"], type: :penguin
Edgier cases
A quirk with Devise and :timeoutable
As discussed above, you can assign a message to an arbitrary key, which is what Devise does with :timeoutable.
Devise uses flash messages to let users know if sign in was successful or unsuccessful. Devise expects your application to call flash[:notice] and flash[:alert] as appropriate. Do not print the entire flash hash, print only specific keys. In some circumstances, Devise adds a :timedout key to the flash hash, which is not meant for display. Remove this key from the hash if you intend to print the entire hash.
If you are using Devise’s :timeoutable
feature and you don’t remove the timedout
key,
users will see a flash message that reads true
when they come back to the site and their session has timed out.
To address this situation, delete it in your _flashes
partial.
<% all_flashes = flash.delete(:timedout) %>
<% if all_flashes.count.positive? %>
[...]
<% end %>
Overriding with content_for
Our friend content_for can tell our layout that a view wants to handle flashes in a custom way (or disable them).
For example, say a view wants flash messages for that view to not be at the top of the page, but nestled somewhere inside the content.
<!-- app/views/layouts/application.html.erb -->
<body>
<% unless content_for?(:hide_flashes) %>
<%= render "application/flashes" %>
<% end %>
<%= yield %>
[...]
<!-- app/views/penguin/index.html.erb -->
<% content_for :hide_flashes, true %>
<div>
<div>Somewhere else on the page</div>
<%= render "application/flashes" %>
</div>
All the code in one place
Finally, here are all the suggested code changes to support flashes, with a little extra support for styling:
<!-- app/views/layouts/application.html.erb -->
<body>
<% unless content_for?(:hide_flashes) %>
<%= render "application/flashes", classes: "some default class values" %>
<% end %>
<%= yield %>
[...]
<!-- app/views/application/_flashes.html.erb -->
<%
all_flashes = flash.delete(:timedout)
classes ||= ""
%>
<% if all_flashes.count.positive? %>
<div id="flash" class="<%= classes %>">
<% all_flashes.each do |type, messages| %>
<% Array(messages).each_with_index do |message, index| %>
<%= render "application/flash", type:, message:, index: %>
<% end %>
<% end %>
</div>
<% end %>
<!-- app/views/application/_flash.html.erb -->
<%
# Style different flash types to be visually distinct.
if type == "alert"
background_color = "angry-penguin-red"
text_color = "antarctic-white"
elsif type == "notice"
background_color = "muddy-penguin-beige"
text_color = "volcanic-brown"
else
background_color = "emperor-penguin-black"
text_color = "antarctic-white"
end
flash_class = "#{text_color} #{background_color} some more class styles"
%>
<div
id="flash-<%= type %>-<%= index %>"
role="alert"
class="<%= flash_class %>"
data-controller="hide-on-click"
>
<div>
<span><%= message %></span>
<button data-action="hide-on-click#hide">X</button>
</div>
</div>
// app/javascript/controllers/hide_on_click_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
hide() {
this.element.remove();
}
}
# Gemfile
group :test do
gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors", tag: "v0.12.0"
gem "action_dispatch-testing-integration-capybara",
github: "thoughtbot/action_dispatch-testing-integration-capybara",
require: "action_dispatch/testing/integration/capybara/minitest"
[...]
# test/test_helper.rb
module ActiveSupport
class TestCase
[...]
def assert_flashes(messages, type:)
messages = Array(messages)
assert_equal messages, Array(flash[type])
assert_element "div", id: "flash", count: 1 do |parent|
messages.each_with_index do |text, index|
parent.assert_selector :alert, text:, id: "flash-#{type}-#{index}", normalize_ws: true, exact_text: true
end
end
end
def assert_no_flash(type:)
assert_nil flash[type]
assert_element "div", id: "flash-#{type}-0", count: 0
end
def assert_no_flashes
assert_empty flash
assert_element "div", id: "flash", count: 0
assert_no_flash(type: :alert)
assert_no_flash(type: :notice)
end
end
end
Bringing it all together
Here are the key steps you should take to render flash messages in Rails:
- Create partials for flashes and render the main one in your Application Layout
- Use an
each
loop to cycle through all the flashes that have been set - Allow arrays of messages
- Use Stimulus to allow the user to dismiss flashes
- Don’t show
:timedout
flashes if you’re using Devise’s :timeoutable - Test that flashes are set and visible
- And never, ever, ever try to steal a penguin. 🐧