Guide to Slots in Rails' ViewComponent

An abstract, minimalistic 3D-like aerial view of a lush green maze under bright sunlight, creating an inviting atmosphere.

ViewComponent, started at GitHub, is a popular open-source project. The goal for it is to break down complex views and improve performance in Rails applications

Slots are one of the most powerful features. From a simple text-output to rendering (multiple other) ViewComponent’s. Slots can do it! Let’s go over how they work.

What are slots in ViewComponent?

ViewComponent Slots are a powerful feature designed to make components more flexible and reusable by allowing them to accept and render nested content or even other components. Introduced as an improvement in the “Slots V2” API, slots are now a default feature of ViewComponent and one I highly recommend you start to use (if you haven’t already). Slots are used extensively in Rails Designer too.

Let’s go over the various types of slots. Yes, slots are versatile. From rendering just some content, to rendering other components to even polymorphic slots.

Slots can be defined in two ways:

  • renders_one
  • renders_many

And they do exactly what you think they do. renders_one allows you to render (and define) only one slot (think an avatar in a UserProfileComponent). renders_many can render many slots (think related links in an ArticleComponent). All the different kinds of slots, described below, work for both renders-one and -many.

Render content

The most basic version of defining a slot is as follows:

# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
  renders_one :avatar
end
<%# user_profile_component.html.erb %>
<header>
  <%= avatar %>
  <h1><%= @user.name %></h1>
</header>

Then when you render the component in a view you can define the avatar slot as follows:

<%= render UserProfileComponent.new do |component| %>
  <⁠% component.with_avatar do %>
    <%= image_tag("path/to/avatar/of/sorts.jpg", alt: "") %>
  <% end %>
<⁠% end %>

Little aside: it takes any block, so you could also write <⁠% component.with_avatar { image_tag("path/to/avatar/of/sorts.jpg", alt: "") } %>.

Render another component

There are two ways to render another component through a slot: inside of the component class or as another file. The way they work are similar, but are set up slightly different.

Render another component inline

# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
  renders_one :avatar, "AvatarComponent"

  class AvatarComponent < ViewComponent::Base
    def call
      tag.img src: "path/to/avatar/of/sorts.jpg", alt: ""
    end
  end
end

Notice how the quotes around AvatarComponent? Adding the component as a string is how ViewComponent assumes the other component is nested inside the component.

Render another component from another file

# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
  renders_one :avatar, AvatarComponent
end
# avatar_component.rb
class AvatarComponent < ViewComponent::Base
  def call
    tag.img src: "path/to/avatar/of/sorts.jpg", alt: ""
  end
end

Here the class name (AvatarComponent) is referenced, letting ViewComponent to look for the class in another file.

Render lambda slot

Alright, now this sounds scary! But a lambda is nothing more like a mini-function that you can create on-the-fly without giving it a name. Still afraid? An example probably helps:

# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
  renders_one :avatar, ->(src: nil, alt: alt, css: "w-6 h-6 rounded-full") do
    content_tag(:img, src: src, alt: alt, class: css)
  end
end

Then when rendering it:

<%= render UserProfileComponent.new do |component| %>
  <⁠% component.with_avatar(src: "path/to/avatar/of/sorts.jpg", alt: "My Profile picture") %>
<% end %>

For simple elements like this one, this will work just fine. But you can also call another component. Using both inline and external components as described above.

# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
  renders_one :avatar, ->(src: nil, alt: nil, css: "w-6 h-6 rounded-full") do
    AvatarComponent.new(src: src, alt: alt, css: css)
  end
end

This example assumes a AvatarComponent to be defined that takes three attributes src, alt and an optional css.

Render polymorphic components

Polymorphic slots has been a fairly new addition to ViewComponent (since 2.12.0). I personally haven’t found too much use for them, but still grabbed for them a few times. Let’s look at a, bit of contrived, example:

# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
  renders_one :avatar, types: {
    icon: ->(css: "w-4 h-4 rounded-full") do
      tag.span @user.first.name, class: css
    end,
    image: ->(css: "w-4 h-4 rounded-full") do
      image_tag @user.avatar, class: css
    end
  }
end

In your view you can then do this:

<%= render UserProfileComponent.new(user: Current.user) do |component| %>
  <⁠% if Current.user.avatar.attached? %>
    <% component.with_image_avatar %>
  <⁠% else %>
    <% component.with_icon_avatar %>
  <⁠% end %>
<% end %>

The polymorphic slots are called with the type name in them, eg. with_image_avatar and with_icon_avatar.

Predicate slot_name? methods

Say what? Another fancy term, I know. But all it is, is the slot name appended with a question mark. Assume the same user profile component from above.

Let’s also assume you need to wrap the user’s avatar in a parent element. And maybe the avatar slot is optional.

You can then, in your template, wrap the avatar HTML like so to only display it if it’s defined:

<%# user_profile_component.html.erb %>
<header>
   <% if avatar? %>
    <div class="mr-4">
      <%= avatar %>
     </div>
   <% end %>

  <h1><%= @user.name %></h1>
</header>

And that’s all there is to know about slots in ViewComponent. The many options might make it sometimes tricky to know which kind of slot to go for, but experimenting—quickly trying an option and move to the next—is the fastest way to get comfortable with the many options around slots.

Have questions around if, when or how to use for a specific example? Feel free to reach out.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built for Ruby on Rails (inc. Rails 8)

  • Designed with Tailwind CSS and Enhanced with Hotwire

  • Updates for 12 months