Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

April 8, 2024

•

Last updated October 20, 2026

All the ways to render partials in Ruby on Rails

A Rails partial is a reusable piece of a view. Give its filename an underscore, pass it the data it needs, and render it wherever that piece of UI belongs.

The everyday syntax is pleasantly small:

<%= render "projects/project", project: @project %>

That renders app/views/projects/_project.html.erb with a local variable named project.

But there's more to partials than extracting a long chunk of HTML. You can render collections, rename locals, add separators, wrap content in a layout, and define exactly which inputs a partial accepts. I'll work through those options with a small Project example.

This guide targets Rails 8.x. Strict locals require Rails 7.1 or newer. The original video and example repository remain available as older companion material; their Rails UI setup is not required for the examples below.

Rails partial rendering: quick reference

These examples belong in an ERB view. Assume @project is one Project and @projects is a collection of projects.

<%# A named partial with an explicit local %>
<%= render "projects/project", project: @project %>

<%# The longer equivalent %>
<%= render partial: "projects/project", locals: { project: @project } %>

<%# Let the model determine its conventional partial %>
<%= render @project %>

<%# Render a collection using the same convention %>
<%= render @projects %>

<%# Choose the collection's partial explicitly %>
<%= render partial: "projects/project", collection: @projects %>

These are alternatives, not a block to paste into one page unless you want the same content several times. A minimal app setup appears near the end if you want somewhere to try them.

Start with a partial and a local variable

Create the following file:

<%# app/views/projects/_project.html.erb %>
<article>
  <h2><%= link_to project.title, project %></h2>
  <p><%= project.description %></p>
  <p><%= project.active? ? "Active" : "Inactive" %></p>
</article>

From app/views/projects/show.html.erb, render it with:

<%= render "project", project: @project %>

The underscore belongs in the filename, not in the render call. You also leave off .html.erb.

For a shared partial or one outside the current view directory, use its path relative to app/views:

<%= render "projects/project", project: @project %>

I prefer an explicit local such as project over reaching directly for @project inside a reusable partial. The caller makes the dependency visible, and another page can reuse the same markup with a different object.

Short syntax versus explicit options

These two calls pass the same local:

<%= render "projects/project", project: @project %>

<%= render partial: "projects/project", locals: { project: @project } %>

Use whichever reads best in context. When adding options such as collection:, as:, or spacer_template:, the explicit partial: form keeps rendering options separate from the locals you're passing.

Avoid casually mixing the two forms. In the shorthand call, the second hash is the locals hash; locals: isn't a wrapper you need to add there.

Render a model with its conventional partial

For a normal Project model, this is enough:

<%= render @project %>

Rails uses the object's to_partial_path convention to find projects/_project.html.erb and makes the object available as project.

That's convenient when the partial name matches the resource. If you want a deliberately different presentation, name it explicitly instead:

<%= render "projects/summary", item: @project %>

Here's the corresponding partial:

<%# app/views/projects/_summary.html.erb %>
<p><strong><%= item.title %></strong></p>

A custom object can implement to_partial_path, too. The useful idea is that object rendering follows a convention supplied by the object; Rails isn't guessing a filename from arbitrary data.

Render a collection without an each loop

Given @projects, you could write:

<% @projects.each do |project| %>
  <%= render "projects/project", project: project %>
<% end %>

Collection rendering expresses the same intent more directly:

<%= render partial: "projects/project", collection: @projects %>

Or, when you want the models' conventional partials:

<%= render @projects %>

Each item becomes the project local inside _project.html.erb. The collection can be an array or an Active Record relation; you don't need to convert a relation to an array just to render it.

The object-based shorthand can also handle a collection of different model types, provided each object resolves to an available partial. Use that deliberately, not as a substitute for knowing what the page is rendering.

Rename the local with as:

The default local name comes from the partial's filename. When that isn't the name you want, use as::

<%= render partial: "projects/summary",
           collection: @projects,
           as: :item %>

Now each object is available as item inside the _summary.html.erb shown above.

For one object, the equivalent option is object::

<%= render partial: "projects/summary",
           object: @project,
           as: :item %>

This changes the local variable name, not the filename or the model.

Handle an empty collection

Rendering an empty collection returns nil, so a small fallback can be useful:

<%= render(@projects) || tag.p("No projects yet.") %>

For a richer empty state, use a conditional and a dedicated partial:

<% if @projects.any? %>
  <%= render @projects %>
<% else %>
  <%= render "projects/empty_state" %>
<% end %>
<%# app/views/projects/_empty_state.html.erb %>
<p>You haven't added a project yet.</p>
<%= link_to "Create a project", new_project_path %>

An empty collection is different from attempting to render a missing object. If @project might be nil, handle that case explicitly rather than expecting render @project to behave like an empty list.

Add a separator between items

Create a small separator partial:

<%# app/views/projects/_separator.html.erb %>
<hr>

Then use:

<%= render partial: "projects/project",
           collection: @projects,
           spacer_template: "projects/separator" %>

The spacer goes between items, not after every item. For purely visual spacing, CSS may be all you need; use a spacer partial when you actually want markup between records.

Get the item's position

Collection rendering provides a zero-based counter and an iteration object. With a partial named _numbered_project.html.erb, the default names are numbered_project_counter and numbered_project_iteration.

Using as: :project makes the names easier to read:

<%= render partial: "projects/numbered_project",
           collection: @projects,
           as: :project %>
<%# app/views/projects/_numbered_project.html.erb %>
<p>
  <%= project_counter + 1 %>. <%= project.title %>
  <% if project_iteration.last? %>
    <span>Last project</span>
  <% end %>
</p>

The iteration object also exposes index, size, and first?. This partial expects collection rendering; don't assume those automatic collection locals exist when rendering a single object.

Define a partial's inputs with strict locals

Once a partial accepts several options, it's useful to make its interface explicit. Rails 7.1 added strict locals through an ERB comment.

Replace _project.html.erb with this version:

<%# locals: (project:, show_description: true) %>
<article>
  <h2><%= link_to project.title, project %></h2>

  <% if show_description %>
    <p><%= project.description %></p>
  <% end %>

  <p><%= project.active? ? "Active" : "Inactive" %></p>
</article>

project: is required. show_description: is optional and defaults to true.

The original render calls still work. To hide the description:

<%= render "projects/project",
           project: @project,
           show_description: false %>

For a collection, pass the option through locals::

<%= render partial: "projects/project",
           collection: @projects,
           locals: { show_description: false } %>

A missing required local or an undeclared extra local produces an error. That's useful feedback when a call site and its partial disagree. You can intentionally accept additional keyword arguments with a double-splat parameter, but don't add one merely to hide misspelled inputs.

In older Rails apps without strict locals, an optional local can use local_assigns:

<% show_description = local_assigns.fetch(:show_description, true) %>

Using fetch preserves an explicitly passed false. An expression such as local_assigns[:show_description] || true would accidentally turn it back on.

The Action View guide has more detail on strict locals and their method-like signatures.

Wrap content in a partial layout

Sometimes the reusable part is the wrapper: a panel, dialog body, or authentication shell. A partial layout lets the caller supply the content inside it.

Create a panel:

<%# app/views/shared/_panel.html.erb %>
<%# locals: (title:) %>
<section class="panel">
  <h2><%= title %></h2>
  <div class="panel-body">
    <%= yield %>
  </div>
</section>

Use it from a view:

<%= render layout: "shared/panel", locals: { title: "Project overview" } do %>
  <p>Choose a project below to see its details.</p>
  <%= render @projects %>
<% end %>

Notice the colon in layout: and the complete <% end %> tag. The block becomes the content at yield.

This gives you a reusable wrapper without moving the page's entire structure into another abstraction. Named content_for sections can help with page-level layouts, but they are not isolated per-instance slots for repeated components. A simple block is easier to follow when a panel only needs one content area.

Wrap each item in a collection

You can also apply a layout around an ordinary partial:

<%# app/views/projects/_project_frame.html.erb %>
<div class="project-frame">
  <%= yield %>
</div>
<%= render partial: "projects/project",
           collection: @projects,
           layout: "project_frame" %>

Here Rails wraps each rendered project individually. That's different from the previous panel example, which wraps the entire collection once.

Choose a partial variant

Variants let one partial have different presentations. For example:

<%# app/views/projects/_project.html+compact.erb %>
<%# locals: (project:) %>
<p><%= link_to project.title, project %></p>

Request that variant explicitly:

<%= render partial: "projects/project",
           variants: :compact,
           locals: { project: @project } %>

Rails looks for the compact variant and can fall back to the ordinary partial if the variant isn't available. This is server-side template selection, not a CSS breakpoint. For responsive styling alone, you may not need another template.

Rendering in a controller is a different context

One easy source of confusion is that both controllers and views have a render method, but a bare string doesn't mean the same thing in both places.

Inside a view:

<%= render "projects/project", project: @project %>

That means a partial, with an underscore in its filename.

Inside a controller action:

render "projects/show"

That means the full app/views/projects/show.html.erb template, not _show.html.erb. It also doesn't run ProjectsController#show; the current action must prepare whatever data that template needs.

To send a partial as the response body, be explicit:

render partial: "projects/project", locals: { project: @project }

Partial rendering doesn't include the application layout by default. Decide whether the endpoint should return a fragment or a complete page.

Render a string without sending a response

In a controller, render_to_string returns the rendered output rather than assigning it as the response body:

html = render_to_string(
  partial: "projects/project",
  locals: { project: @project },
  formats: [:html]
)

It isn't a special HTML-escaping method. The template and its helpers still determine how content is rendered. Don't assume the resulting string makes arbitrary untrusted HTML safe.

Plain text, HTML, and JSON are response modes, not partials

There is no documented Rails render string: mode for the job described here. Pick the mode that matches the response you intend:

# These are separate controller examples, not one action.
render plain: "Project saved"
render json: { status: "ok" }
render html: helpers.tag.p("Project saved")

For HTML, a tag helper creates the element while escaping its text content. Don't solve an escaping problem by applying html_safe to untrusted input.

render inline: evaluates a template string; render file: reads a file. Neither is a replacement for a normal reusable partial. Never build an inline template from untrusted input or let request parameters choose an arbitrary file path. The controller rendering API documents these less-common modes.

Optional: create a small app to follow along

You don't need Rails UI, authentication, or a custom theme to learn partial rendering. In a Ruby environment with Rails 8.x installed, create a standard app:

rails new flexible_partials
cd flexible_partials
bin/rails generate scaffold Project title:string description:text active:boolean
bin/rails db:migrate

Inside the existing routes block, add the root route and keep the scaffold's resources declaration:

# config/routes.rb
Rails.application.routes.draw do
  resources :projects
  root "projects#index"
end

If you're editing an existing application, preserve its other routes rather than replacing the file wholesale.

Start the server:

bin/rails server

Visit http://localhost:3000/projects/new and create a couple of sample projects. The scaffold gives you a controller and resource routes for the examples above. Its index action supplies @projects, and its show action supplies @project.

Replace the generated project partial with the example from this guide, then try one render call at a time. The older demo repository uses Rails UI for its presentation; treat it as a companion to the original recording, not as a newly tested Rails 8 starter.

Original Rails UI demo showing the projects page

Screenshot from the original Rails UI demo, not the plain Rails setup above.

Common partial errors to check first

  • Missing partial: Check the leading underscore on the file, its view directory, the requested format, and the spelling of the render path.
  • Undefined local: Check what the caller passes. A local called project is not the same thing as an instance variable called @project.
  • Missing or unknown strict local: Compare the call with the partial's locals: signature. Keep collection-specific inputs in mind if the partial uses a counter.
  • Unexpected full page or missing wrapper: Check whether you're calling render in a controller or a view and whether you actually want a template, partial, or layout.
  • Repeated content: Check whether you left both the loop and the collection-rendering version in the page.
  • Queries multiplying with the number of records: Inspect any associations accessed inside the partial and preload them in the controller or query layer when needed. Extracting markup doesn't eliminate N+1 queries.

Which render form should you reach for?

For most views, I would start with a named partial and explicit locals. Use object or collection shorthand when the Rails convention is exactly what you want. Add strict locals when you want the partial's inputs documented and enforced, and use a layout block when the reusable piece is a wrapper.

You don't need to use every rendering option. The goal is a view that makes its data and structure easy to follow, with less duplicated markup to maintain.

Link this article
Est. reading time: 12 minutes
Stats: 10,009 views

Categories

Collection

Part of the Ruby on Rails collection

Products and courses