---
title: Don't Steal a Penguin -- A Guide to Rails Flashes
teaser: If someone tries to steal a penguin, how should you warn them?
tags: ruby,rails,ruby on rails,development
author: Louis Antonopoulos
published_on: 2025-04-25
---

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](https://edgeapi.rubyonrails.org/classes/ActionDispatch/Flash.html)

> 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.

![A screenshot of a portion of a sample profile page with a circular Penguin avatar and a name of Beak Fisher.
A green-tinted flash rectangle at the top says "Success" and "Penguin was successfully updated."
](https://images.thoughtbot.com/dctnb859timgf22vs255zad9tk04_Success%20flash.png)

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`:

```rb
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`:

```rb
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`.

```rb
# 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.

```rb
# 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:

```erb
<!-- 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:

```erb
<!-- app/views/layouts/application.html.erb -->

<body>
  <%= render "application/flashes" %>

  <%= yield %>
</body>
```

```erb
<!-- app/views/application/_flashes.html.erb -->

<% flash.each do |type, messages| %>
  <% Array(messages).each do |message| %>
    <%= render "application/flash", type:, message: %>
  <% end %>
<% end %>
```

```erb
<!-- 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:

```rb
# 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](https://www.w3.org/WAI/ARIA/apg/patterns/alert/examples/alert/)
- It allows [Capybara accessible selectors](https://github.com/citizensadvice/capybara_accessible_selectors?tab=readme-ov-file#role-string-symbol-nil) 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):

```javascript
// 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:

```erb
<!-- 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>
```

![A screenshot of a green-tinted flash rectangle that says "Success" and "Penguin was successfully updated.",
and all the way on the right it has a bold capital letter X as the button to dismiss the flash.
](https://images.thoughtbot.com/jvyl14b8zw5cvtmaixq1lk55xdol_X%20to%20close.png)

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:

```ruby
# 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](https://github.com/hotwired/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:

```rb
# 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](https://github.com/citizensadvice/capybara_accessible_selectors):

```rb
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:

1. The expected _kind_ of flash was set
2. 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:

```rb
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 an `id` of `flash`
- each flash is enclosed in a `<div>` tag with an `id` of `flash-{type}-{index}`
- you include two gems in your `Gemfile`:
  - [Capybara accessible selectors](https://github.com/citizensadvice/capybara_accessible_selectors)
  - [ActionDispatch::Testing::Integration::Capybara](https://github.com/thoughtbot/action_dispatch-testing-integration-capybara?tab=readme-ov-file)

```rb
# 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"

  [...]
```

```rb
# 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:

```rb
# 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](https://github.com/heartcombo/devise/blob/main/lib/devise/models/timeoutable.rb).

In
[Configuring Controllers](https://github.com/heartcombo/devise?tab=readme-ov-file#configuring-controllers),

> 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.

```erb
<% all_flashes = flash.delete(:timedout) %>

<% if all_flashes.count.positive? %>
  [...]
<% end %>
```

### Overriding with content_for

Our friend
[content_for](https://thoughtbot.com/blog/content-for-what-is-it-good-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.

```erb
<!-- app/views/layouts/application.html.erb -->

<body>
  <% unless content_for?(:hide_flashes) %>
    <%= render "application/flashes" %>
  <% end %>

  <%= yield %>

  [...]
```

```erb
<!-- 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:

```erb
<!-- app/views/layouts/application.html.erb -->

<body>
  <% unless content_for?(:hide_flashes) %>
    <%= render "application/flashes", classes: "some default class values" %>
  <% end %>

  <%= yield %>

  [...]
```

```erb
<!-- 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 %>
```

```erb
<!-- 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>
```

```javascript
// app/javascript/controllers/hide_on_click_controller.js

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  hide() {
    this.element.remove();
  }
}
```

```rb
# 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"

  [...]
```

```rb
# 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](https://github.com/heartcombo/devise/blob/main/lib/devise/models/timeoutable.rb)
- Test that flashes are set _and_ visible
- And never, ever, ever try to steal a penguin. 🐧
