rails

Easier Nested Layouts in Rails

Clean up your views with this simple idiom for writing and declaring nested layouts. Now updated for Rails 7.

Updated June 2023 to work with Rails 7.

A layouts recap

tl;dr – Jump straight to the code.

Any non-trivial web application will have multiple HTML layouts. For example, a login page may use a simple layout with no header and footer, whereas authenticated pages will have navigation bars and breadcrumbs. Public pages like documentation and privacy policy may use yet a different layout.

Rails offers a basic solution for this in the form of layout templates. Create a layout file with the boilerplate you need, then yield to the actual dynamic content that is being rendered:

<html>
  <head>
    ...
  </head>
  <body>
    <%= render("shared/header") %>
    <%= render("shared/alerts") %>

    <div class="container">
      <%= yield %>
    </div>

    <%= render("shared/footer") %>
  </body>
</html>
The view gets rendered into the <%= yield %> of the layout.

You can have as many layouts as you’d like, and Rails makes use of convention over configuration to automatically pair up layouts with corresponding controllers of the same name.

Nested layouts, the (awkward) Rails way

So far so good. But what if you want to reuse one layout within another? Here’s my common practice:

  • Every page in the app needs the standard HTML boilerplate with my common stylesheets and JavaScripts. I need a layout that provides these, but makes no assumptions on the actual body of the page. This is my “base” layout.
  • Most – but not all – pages in the app need a common header and footer. This is my “application” (default) layout.
  • I’d like the application layout to reuse the base layout to keep my code DRY.

Some frameworks call this “template inheritance”. In this example, we might say that the application layout “inherits from” or “extends” the base layout. In Rails, this is known as nested layouts, and it is a bit awkward to use. The standard Rails practice for nested layouts is complicated and involves these considerations:

  • There can only be one yield statement for your entire stack of nested layouts.
  • To achieve nesting, you therefore have to replace yield with specially-named content_for calls.
  • Your nested layout templates have to be refactored to put their boilerplate inside content_for blocks, otherwise they won’t work.
  • To ensure layouts work in both nested and non-nested contexts, you have to switch between yield and content_for with a conditional statement.
  • The nested layout has to trigger its parent layout with an explicit render call at the bottom of the file

The official Rails guide provides an explanation with some examples.

A nicer, helper-based approach

If you do some searching (as you may have done to find this article!) you’ll find alternatives to nested layouts ranging from simple hacks all the way up to full blown gems that introduce their own layout DSL.

My approach is to use a parent_layout helper, which is one of the simpler solutions.1 Instead of juggling content_for names and blocks, I use a helper method to make Rails aware of the nesting structure. Here’s how this solution stacks up:

  • You can still use plain yield in each layout
  • No need for content_for blocks
  • No conditional logic
  • At the bottom of the nested layout, just call parent_layout (it’s self-documenting!) rather than render
# Place this in app/helpers/layouts_helper.rb
module LayoutsHelper
  def parent_layout(layout)
    @view_flow.set(:layout, output_buffer)
    output = render(template: "layouts/#{layout}")
    self.output_buffer = ActionView::OutputBuffer.new(output)
  end
end

An example

Here’s how to use parent_layout to implement the “base” and “application” layouts from my earlier example. This code is taken from my rails-template project, which is my starting point for new Rails apps.

This is the base layout, which handles the general boilerplate:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title><%= content_for?(:title) ? strip_tags(yield(:title)) : "MyApp" %></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag("application", "data-turbo-track": "reload") %>
    <%= javascript_importmap_tags %>
    <%= yield(:head) %>
  </head>
  <body>

    <%= yield %>

  </body>
</html>
app/views/layouts/base.html.erb
Note the final yield statement, which is where views or nested layouts will be rendered.

The application layout extends the base layout by use of the parent_layout helper:

<main class="container">
  <%= render("shared/flash") %>
  <%= yield %>
</main>

<% parent_layout "base" %>
app/views/layouts/application.html.erb
Notice that this layout uses the standard yield syntax; no content_for hacks are needed. This code will get injected into the <%= yield %> of the parent layout shown previously.

That’s it! Just make sure you’ve installed the helper as explained above.

Finally, a word of caution…

I’ve tested this solution with Rails 6.0, 6.1, 7.0, and the latest unreleased version as of June 2023. However, keep in mind that since this helper relies on private Rails internals, it is likely that it will break in a future Rails version.

Notice any bugs or want to suggest an improvement? Let me know!


  1. It’s hard to attribute the parent_layout solution to a single developer, as the code seems to be constantly evolving as it bounces around various blogs and Stack Overflow answers. I originally found a version of it on the MyRailsWay blog, and have since enhanced it to work with later versions of Rails. 

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

RSS
View all posts →

Open source projects

mattbrictson/rails-template

App template for Rails 7 projects; best practices for TDD, security, deployment, and developer productivity. Now with optional Vite integration! ⚡️

1,055
Updated 1 month ago

mattbrictson/tomo

A friendly CLI for deploying Rails apps ✨

360
Updated 20 days ago

More on GitHub →