Building Component-Based Front Ends with Rails

Our latest Ruby on Rails front-end project strongly emphasizes a component-based approach. In this post, we briefly explain how a tiny helper not only helped us render UI components, but also resulted in better components thanks to well-defined contracts and effortless composition.

These days, most front-end developers agree that UIs should be built in a component-oriented fashion. If you’re curious about that larger conversation, there are plenty of good resources on that out there: We’d highly recommend the excellent Style Guide Podcast — yes, all twelve episodes are well worth listening to — as well as Atomic Design to get a sense of why this matters.

Implementing a component-based approach can be a challenge. Generally speaking, it’s a good idea to maintain an application-independent style guide / component library, not least because it tends to result in more well-crafted components (e.g. regarding documentation, reusability and robustness). These days, we can choose from numerous tools to set up a library like that (e.g. Pattern Lab or Fractal).

Even so, you probably still want some sort of abstraction to generate components' server-side markup1 within your application, both for convenience and encapsulation:

        <li class="is-active">
            <img src="…" alt="…">
            <img src="…" alt="…">

(<image-gallery> here is a custom element, which unobtrusively takes care of client-side augmentation.)

Whenever we need to generate such complex HTML structures within our templates, we just want to invoke something like a function with the respective parameters:

component("image-gallery", images=[], selected_index=0)

Well, that’s pretty much exactly what we’ve done in our latest project — which happens to use Haml (“HTML Abstraction Markup Language”), though this approach should work just the same with ERB or whatever templating language you prefer2:

  #parameters are passed into the component's template explicitly
  images = data.fetch(:images) # required
  selected_index = data.fetch(:selected_index, 0)

    - images.each_with_index do |image, index|
      %li{ class: index == selected_index ? "is-active" : nil }
        = image_tag image.src, alt: image.alt

Note that any parameters are passed in explicitly, which ensures that each component has a well-defined contract.

  %h1 Portfolio
  %p Here's a selection of my favorite images:
  = component :image_gallery, data: { images: @images }

  %p © 2017 Unsigned Artist

Components may also support blocks to allow for composition:

= component :order_form, data: { logo: logo } do
    %p All photos half price until solstice. Terms and conditions apply.
    = component :discount_voucher

Behind the scenes, component is just a tiny wrapper around the built-in render Rails helper:

module ComponentHelper
  def component(name, data: {}, &block)
    render_component("#{name}/#{name}", { data: data }, &block)


  def render_component(name, locals, &block)
    if block_given?
      # using `layout` is a trick to allow passing blocks to partials
      # (cf.
      render layout: name, locals: locals, &block
      render partial: name, locals: locals

Whenever component is invoked, it renders the respective markup partial from the corresponding directory in app/components3 (e.g. app/components/image_gallery/_image_gallery.html.haml). That directory typically also contains whatever else the component might require, such as CSS and JavaScript assets as well as i18n translation files:

├── app
│   ├── components
│   │   ├── …
│   │   ├── image_gallery
│   │   │   ├── _image_gallery.html.haml
│   │   │   ├── _image_gallery.scss
│   │   │   ├── image_gallery.js
│   │   │   └── image_gallery.yml
│   │   └── …

We’d be happy to elaborate on that aspect some other time — let us know in the comments.

  1. Distinguishing between server–side base markup and augmented client–side DOM structures improves performance, robustness and generally makes us a good citizen of the web — but that's a topic for another day.  ↩

  2. At least one of the authors is not particularly, err, partial to Haml.  ↩

  3. For this to work, we've extended ApplicationController to include append_view_path Rails.root.join("app", "components").  ↩



Please accept our cookie agreement to see full comments functionality. Read more

Find us on