Turbo Frames and the Extra DOM Node – How to Handle It?

… and check why 5600+ Rails engineers read also this

Turbo Frames and the Extra DOM Node – How to Handle It?

I come from the React world, I’ve spent years working with it, and I’ve been a fan of it ever since. Recently, I started working with Hotwire, and I quickly fell in love with it – the simplicity and efficiency of this approach are impressive. However, along the way, I encountered a few surprises.

One of those surprises was how Turbo Frames work in the DOM. In React, we can use <Fragment> or a shorthand <> to avoid adding an extra node to the DOM structure. In contrast, when using <turbo-frame> tag, it’s being physically embedded in the DOM, which can sometimes lead to unexpected display and styling issues.

Let’s start with a simple example.

We have a record collection browser and we want to display a list of records, but first element is an add new record button.

That’s the basic markup:

  <ul class="grid grid-cols-4 gap-8">
    <li>
      <div class="w-full bg-black aspect-square">
        <%= link_to new_record_path, :class => "bg-blue-500 text-white px-2 py-1 rounded-md size-full flex items-center justify-center" do %>
          <span class="text-2xl text-center"><span class="text-4xl">+</span><br/> New Record</span>
        <% end %>
      </div>
    </li>
    <% @records.each do |record| %>
      <li class="relative">
        <%= link_to  record_path(record) do %>
          <div class="w-full bg-black aspect-square">
            <%= image_tag record.cover, class: "size-full object-contain" if record.cover.present? %>
          </div>
        <% end %>
      </li>
    <% end %>
  </ul>

and it should look like this:

Now we want to wrap the records in a turbo frame, without including the add new record button in it.

  <ul class="grid grid-cols-4 gap-8">
    <li>
      <div class="w-full bg-black aspect-square">
        <%= link_to  new_record_path, :class => "bg-blue-500 text-white px-2 py-1 rounded-md size-full flex items-center justify-center" do %>
          <span class="text-2xl text-center"><span class="text-4xl">+</span><br/> New Record</span>
        <% end %>
      </div>
    </li>
    <%= turbo_frame_tag "records" do %>
      <% @records.each do |record| %>
        <li class="relative">
          <%= link_to  record_path(record) do %>
            <div class="w-full bg-black aspect-square">
              <%= image_tag record.cover, class: "size-full object-contain" if record.cover.present? %>
            </div>
          <% end %>
        </li>
      <% end %>
    <% end %>
  </ul>

But the result is not what we expected:

So what’s the problem?

Turbo Frames are inserted into the DOM as a new element, which can break the layout. Frame is inserted into the second column of the grid, and so are all the records.

Can we fix it?

Yes! One of the solutions is to use good old CSS to make the frame “transparent”. The property we need is display, and we need to set it to contents.

turbo-frame {
  display: contents;
}

This will make the frame transparent and the records will be displayed correctly 🎉

Webinar: My Journey from React to Hotwire

If you’re interested in similar topics, join my webinar - From React to Hotwire, where I’ll talk about my experiences I had along the way!

The webinar will take place on January 30th at 17:00 Warsaw time.

See you there!

You might also like