content_for -- What is it good_for?

Sometimes, you need to include HTML but you can’t figure out where to do it, especially if you’re trying to push something up the view hierarchy.

The answer to your challenge is often #content_for.

Content what?

As described in the Rails guides:

The content_for method allows you to insert content into a named yield block in your layout.

How does it work?

Step 1: A view has a #content_for line or block. This is the “input”.

<!-- app/views/posts/index.html.erb -->

<% content_for :title, "All the Posts" %>

<% content_for :something do %>
  <script>function hello() {}</script>
  <style>h1 {color: blue;}</style>
  I will appear in the :something block!
<% end %>

Step 2: The content in the line or block is picked up by a layout. You can either use content_for again or yield. This is the “output”.

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <title><%= content_for(:title) || "My App Name" %></title>
  </head>

  <body>
    <%= content_for :something %>

    <!-- OR -->

    <%= yield :something %>
  </body>

The content_for(:title) in the default Application layout was added to Rails at the beginning of 2024.

Let’s look at some other examples of how we can use content_for!

Moving content out of a partial

Imagine we have a partial like this:

<!-- app/views/posts/_post.html.erb -->

<%= turbo_stream_from post %>

<li id="<%= dom_id(post) %>">
  <%= link_to post.title, post_path(post) %>
</li>

This will fail an accessibility audit because the parent wraps the partial in a <ul> block, which only allows li, script, or template tags, and this will add a <turbo-cable-stream-source> element inside the <ul>.

We could do this:

<li id="<%= dom_id(post) %>">
  <%= turbo_stream_from post %>

  <%= link_to post.title, post_path(post) %>
</li>

but that feels…icky, because the Turbo stream elements are spread out and intermingled with our with UI content. Why should our <li> tags have extra stuff in them?

Where the Turbo stream elements really should go is in the <body> element of the page. Let’s put them away where they belong!

<!-- app/views/posts/_post.html.erb -->

<% content_for :body do %>
  <%= turbo_stream_from post %>
<% end %>

<li id="<%= dom_id(post) %>">
  <%= link_to post.title, post_path(post) %>
</li>

To support this, we must include the content_for in the layout:

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

<body>
   <%= content_for :body %> <!-- or yield :body -->

   <% yield %>
</body>

Now our <li> elements are focused, and our <body> element has all the Turbo refreshing elements grouped together.

Overriding something at the top of the view hierarchy

Let’s take a different example. Here, imagine our Application layout specifies styling for the <body> element:

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

<html lang="en">
  <head>
    <!-- head stuff -->
  </head>

  <body class="some_style">
    <main>
      <%= yield %>
    </main>
  </body>
</html>

What if we determine that some views need to amend or replace that style, but we don’t want to maintain an entirely different layout (or multiple layouts) for that one customization?

If you guessed #content_for, you win…a new car! No. But you would if I had an HTML game show and we asked that question.

So, what would this approach look like? (The HTML, not the game show.)

First, we update our layout file to accept a new #content_for block:

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

<body class="some_style <%= content_for(:body_class) %>">
  <main>
    <%= yield %>
  </main>
</body>

And now, a view that wants to add extra styling way up there can do this:

<!-- app/views/gangnams/index.html.erb -->

<% content_for :body_class, "gangnam_style flex flex-row" %>

which will result in the <body> element rendering:

<body class="some_style gangnam_style flex flex-row">

If we instead need to be able to overwrite the parent style, we could leverage #presence after the #content_for call:

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

<body class="<%= content_for(:main_classes).presence || 'some_style' %>">
  <main>
    <%= yield %>
  </main>
</body>

so that if a view doesn’t specify content_for :main_classes, it will default to some_style, but if it does specify :main_classes, they will be the only classes on the <body> element.

Multiple <main> elements

In a view that incorporates an iframe of another view or site, it is quite possible to end up with multiple <main> elements.

Like the <li> example, above, this will lead to an accessibility violation: the document has at most one main landmark.

We could set our <main> element to have a title property, but if we’re showing an iframe of our own site, that won’t work, as the view and the iframe will have the same value for the title.

Unless…

That’s right! Unless we call on #content_for again:

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

<body>
  <main title="<%= content_for(:main_title) %>">
    <%= yield %>
  </main>
</body>

Now our views can specify a unique title property to make them more accessible.

Selective Turbo morphing

Here’s one last example before we go our separate ways today.

As I mentioned last week in Hotwire and That Syncing Feeling, you may not want Turbo morphing on every page.

In that case, we can use #content for to apply the morphing and scrolling helpers to the <head> element of a particular view to request the behavior:

<!-- app/views/posts/index.html.erb -->

<% content_for :head do %>
  <%= turbo_refresh_method_tag :morph %>
  <%= turbo_refresh_scroll_tag :preserve %>
<% end %>

But wait, it gets better!

There’s a helper that already uses #content_for :head under the hood: #turbo_refreshes_with. So if we do this instead:

<!-- app/views/posts/index.html.erb -->

<%= turbo_refreshes_with method: :morph, scroll: :preserve %>

it will automatically add the turbo_refresh_method_tag and turbo_refresh_scroll_tag elements to the <head> element of that view, without us needing to explicitly call #content_for.

Bringing it home

#content_for is the decluttering friend that solves HTML problems with elegance and simplicity, leaving you with tidy layouts that spark joy. You may not need it often, but when you do, it’s the perfect tool for the job!