How Layouts Work in Rails

3D rendering of a bento box, viewed from bird's eye perspective, with bright, summer colors

Layouts in Rails have been around since the earlier versions. They define the surrounding HTML structure shared by multiple views, allowing you to implement a consistent visual design. They encapsulate shared site elements like partials and ViewComponent like: headers, footers, and navigation bars. This allows you have a consistent “chrome” for every view of your app, but also have invisible elements added onto every page, like JavaScript snippets.

Most Rails developers easily grasp the idea of layouts, but not all know the inner workings of layouts in Rails. Let’s go over the most important parts.

How content is rendered in layouts

The central mechanism in layouts for embedding view-specific content is the yield method. When you define a layout, whether it’s app-wide in application.html.erb or specifically for other controllers, yield marks the spot where content from individual view templates (show.html.erb, index.html.erb, etc.) will be inserted into the layout.

A typical layout might look like this:

<!DOCTYPE html>
<html>
<head>
  <title>Rails Designer</title>
</head>
<body>
  <header>
    <h1>Welcome to Rails Designer</h1>
  </header>

  <%= yield %>

  <footer>
    <p>© 2024 Rails Designer</p>
  </footer>
</body>
</html>

In this layout, <⁠%= yield %> is where Rails inserts the content of other templates based on the action being executed.

How Rails looks up layouts

By default Rails looks for layouts in the app/views/layouts folder. The actual layout is checked in the following steps.

In your controller you can specify a layout:

class AuthorsController < ApplicationController
  layout "writers"
  # …
end

Rails will check app/views/layouts for writers.html.erb file.

If no layout declaration was set in above controller, Rails uses the controller name to look for a authors.html.erb layout. If it cannot find a controller-specific layout, it will fallback to the default application.html.erb.

The layout declaration takes more than just a string or symbol though. You can also pass false to skip wrapping the view in a layout. Or you can pass a method with a conditional:

class AuthorsController < ApplicationController
  layout :authors_layout
  # …

  private

  def authors_layout
    Current.user.admin? ? "admins" : "authors"
  end
end

You can also pass the only and except options to the layout declaration: layout :authors_layout, only: %w[index].

Layouts also cascade downward in the hierarchy. So with the following controller:

class Authors::BiographiesController < AuthorsController
  def show
  end
end

The show view will also use the layout from the authors_layout method.

Are you using Devise? If you create a app/views/layouts/devise.html.erb layout, that layout is used for your signin and signup views. 💡

Nested layouts

Rails doesn’t come with nested layouts out-of-the-box. If you are familiar with Static Site Generators you might have used them. I often use a nested layout for my SaaS’ settings page. It often features a dedicated navigation for the many settings.

The cleanest solution, and the one I use in my Rails apps, works like this:

  1. Set the layout in the controller:
# app/controllers/settings_controller.rb
class SettingsController < ApplicationController
  layout "settings"
end

All separate settings screens inherit from this controller, eg. Settings::TeamsController < SettingsController.

  1. Create a settings-layout:
# app/views/layouts/settings.html.erb
<div class="md:h-screen grid grid-cols-12 gap-4 md:gap-2 lg:gap-8">
  <%= component "navigation/settings", account: Current.account %>

  <div class="col-span-12 md:col-span-8 lg:col-span-9">
    <%= yield %>
  </div>
</div>

<⁠% parent_layout "application" %>

You can add whatever component/html you need here. Make sure to include the yield method!

  1. Create a parent_layout helper

This helper is the important part that makes it nested layouts work.

# 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

It saves the current output, renders the specified parent layout, and then sets this rendered output as the new output buffer to encapsulate nested layouts. That’s all there’s needed for nested layouts.

Is there anything else missing around layouts in this article? Let me know!

Rails Designer is a professional UI components library for Rails

Get product updates, insights and latest articles in your inbox

Published at . Have suggestions or improvements on this content? Do reach out. Interested in sharing Rails Designer with the Ruby on Rails community? Become an affiliate.

UI components for Ruby on Rails apps

$ 99 one-time
payment

Get Access
  • One-time payment

  • Access to the entire library

  • Built for Ruby on Rails

  • Designed with Tailwind CSS and enhanced with Hotwire

  • Includes free updates (to any 1.x version)