<body>
<%= render 'shared/navbar' %>
<main class="mx-auto max-w-5xl p-6">
<%= yield %>
</main>
<%= turbo_frame_tag 'modal' %>
</body>
class ApplicationController < ActionController::Base
layout -> { turbo_frame_request? ? false : 'application' }
end
<%= turbo_frame_tag 'modal' do %>
<div class="fixed inset-0 bg-black/40"></div>
<div class="fixed inset-0 flex items-center justify-center p-4">
<div class="w-full max-w-xl rounded bg-white p-6 shadow">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">New post</h2>
<%= link_to '✕', '#', data: { turbo_frame: 'modal' }, class: 'text-gray-500 hover:text-gray-900' %>
</div>
<%= render 'form', post: @post %>
</div>
</div>
<% end %>
When I need a modal (new/edit/show), I avoid client-side templating by using a dedicated turbo_frame_tag as the modal container (often id='modal'). Links target that frame, so the response only replaces the modal content. Closing the modal is just swapping it back to an empty shell. The key trick is to ensure the modal response renders without the full layout when it’s a frame request (use turbo_frame_request?). This keeps DOM diffing small and avoids nesting <html> inside a frame. I also like placing the modal frame once in application.html.erb so every page can open modals without duplicated markup.