Progressive enhancement philosophy and WHEN to add Hotwired?

I’ve tried to search the forum to find something that speaks to this question, but I didn’t really find a “foundations” kind of answer. I’m new to Hotwire - still just reading the docs and watching YouTube videos. But, one thing that isn’t quite gelling in my head is when to actually add Hotwire to a project? I understand that Hotwire takes a progressive enhancement approach to web development. But, does that mean that the entire suite of features in my app should work without Hotwire? And, that I should only start adding Hotwire and decomposing into frames once all the “standalone page” versions of interactions are complete?

I’m sure the answer is (as always), it depends; but, if anyone could share their general approach, I’d greatly appreciate it!

My biggest concern of going the full “progressive enhancement”, “after app is working” approach is that it might take much more refactoring to get a good UX once I start to add Hotwire. As opposed to working with Hotwire from the beginning and being able to plan for it as I go.



Welcome! And thank you for verifying your Senior Dev Cred™ by invoking the important qualifier “it depends”. You’re right, there are many ways to look at this.

The hard-line progressive enhancement approach would be to always ensure that nothing is required besides HTML and HTTP(S) requests to access the app and use it effectively. That’s not always practical, but that’s the hard line approach. I don’t recommend you go all the way with this, because if you try to make a Rails app work without JavaScript, you’re building quite a lot of work for yourself. But do bear with me here.

This ethos brings along some tremendous benefits to you as the application designer, because it literally forces you to consider the relationships between actions and data changes, and can often expose you to some hard problems that you may be papering over using a higher-level approach (or even just old-school Ajax). It literally forces you to think in REST, which is not bad at all.

When you are first designing an app, I find it is often fastest to build out entire pages, however complex, as single templates, then start going through them with tweezers and pulling out individual features as partials (still just working with a single page render here, not moving into rendering those partials separately for a sub-page update).

Once you’ve done that, and ensured that everything still works, you can move on to making the appropriate additions of sub-page renders. Those can be nothing more than a modern equivalent of UJS, where a form request only updates the relevant parts of the page. (Think wrapping a form in a Turbo Frame.) But the same set of partials can also be used to make multiple changes to a page using Stimulus controllers or Turbo Streams.

And the beauty of this approach is that because you’ve decomposed the page at the template level, you can make any of those templates live in any context. If you want to use Action Cable to broadcast the changes to all users on the same page, that can literally be as simple as adding an after_save callback in the model or an after_action to the controller.

And if someone comes along with an older browser or a tin-foil hat, they won’t have a completely awful day.



I agree that building out full-pages first seems like it would lay-down all (or a lot of) the ground work for future optimizations and AJAX’ifying UIs and doing frames. It’s just tough because I’m so used to buiding out SPAs using Angular that I think in terms of complex UIs. Now, the struggle if figuring out how to think in terms of simple UIs that can become more complex with some “sprinkles.”

I think one of my big mental blocks right now is figuring out how/when to using a Stimulus Controller vs. when to try and use a Turbo-Frame with an embedded Form. To paraphrase DHH in other matters, my gut says to over-index on Frames first, until it “hurts”; and then maybe start to dial it back and use Controllers.

Clearly I’m just trying to find a path without having any experience yet :laughing: Not sure learning ever works that way. Anyway, I appreciate your insights, thanks a lot.

Remember that all this stuff (e-commerce, interactive Web sites, etc) was all being done with stone knives and bear skins (CGI-BIN anyone? PERL?) in the mid '90s. We didn’t even have JavaScript yet. It’s all possible without a heavy-weight front-end framework.


Ha ha, I remember when jQuery was revolutionary :rofl: Actually, from what I’ve been seeing, this feels a little bit like returning to jQuery where the DOM often held the state / source of truth. Seems like Hotwire uses the DOM for a lot of the config. So, to that end, feels like coming home.

Many developers incorrectly assume Everyone has JavaScript, right?.

I’ve dealt with bugs reported by customers where a JavaScript heavy application was bricked because of a hard-to-reproduce state of the application or deployed bug in the code.

I’ve also dealt with bugs reported by customers where the application did not work at all because their firewall incorrectly flagged one line of code from a 3rd party library as potentially malicious and blocked the entire JavaScript, which crippled the entire application.

However, even if you don’t subscribe to the basic definition of progressive enhancement, there are many more benefits to the hotwire approach of progressive than running on browsers where JavaScript is not currently working.

The resilient approach (or progressive enhancement) from the hotwire/stimulus handbook:

This resilient approach, commonly known as progressive enhancement, is the practice of delivering web interfaces such that the basic functionality is implemented in HTML and CSS, and tiered upgrades to that base experience are layered on top with CSS and JavaScript, progressively, when their underlying technologies are supported by the browser.

DHH, Hotwire creator/contributor

When is a Hi-Fi experience necessary?

Part of the ethos of Hotwire is…“just because you can, doesn’t mean you should…”. You are hurting the web when you are bathing it in unnecessary JavaScript. It’s a progressive enhancement, it’s a ladder…Most pages do not need all of that. A bunch of applications would be totally fine if the only thing you did was Turbo Drive. They’d already feel fast enough, they’d already be great

What you want to do is identify the 5 or 10% of your app where you really want the Hi-Fi experience, there you pay 100%, you build it and it takes more time. For the other 90 to 95%, you get it on sale.

Sam Stephenson, Hotwire creator/contributor

If the web offers us guidance, take it. The web gives us <a> to change pages and <form> to change server-side state, so that’s what we’ll use in our app. That way we have fewer decisions to make and we’re responsible for less code.

If we want to do something that can’t be done with HTML and CSS alone, we’ll still try to do as much as we can with HTML and CSS, and use a layer of JavaScript on top for the rest.

Hotwire Progress Enhancements

A great example of progressive enhancement in Hotwire is Thinking in Hotwire: Progressive Enhancement | Boring Rails

Personal Opinion

Until Hotwire, it’s been a long time since feeling I had the library of tools to build a performant and stable application in the velocity we are building. For me, the benefits of progressive enhancement are:

  • Development Velocity
  • Application performance
  • Stability (with all business logic on backend)
  • Cross Platform (web and mobile app)
  • And last as a side benefit, much (not all) of your application still works without JavaScript.



Thank you very much for the comprehensive reply and the links - I just took a look through the Boring Rails post and that was quite insightful. I think seeing the “New comment” form as a stand-alone page then loaded inline via a Frame was something I wasn’t connecting with before. I’ll continue to try add layer-in functionality in my P.O.C exploratory app, trying to get a better sense of how it all fits together. One thing that still feels very foreign is not relying on a “page load” to initialize some JavaScript (since the pages don’t “reset” on each navigation). But, I assume if I start using Stimulus controllers, that all just magically works.

I think having system tests that target a js-off state for all important pathways is probably the practical way into making it happen on an app.

I’m not seeing anybody talk about a system testing suite for the js-off scenario!

Maybe that should happen.

We would have to fake the js-off without turning it off in the browser, because capybara probably needs it for issuing the user interactions. But maybe just omit the js from loading while in this scenario.

It would have to be a different path in the system test (because an altogether separate system test would hurt). In the system test, there could be light branching for those times when the interaction with js-off is different (say, it changes pages and goes to the edit endpoint for that resource instead of having the turbo-frame inline editing). Short if js_off; else; end branches throughout.

That’d be solid.

Even without automated testing, I can see it becoming harder to manually test interfaces once they get progressively enhanced. I’m still pretty far away from even having to do that - still just trying to wrap my head around the whole frames-based approach.

We’ve considered adding no-js tests, but decided against it. The ROI in spending the time building and maintaining tests for a fraction of our audience is not worth it. Was not worth it for us. The page still loads and has some functionality (but not necessarily 100% functionality). With SPA, if javascript access is temporarily unavailable, you typically get a white screen. We targeted somewhere in between.

But again, we see progressive enhancement as more than just supporting no-js.


I can respect the ROI calculation.

This is a good thread. I take progressive enhancement very seriously, and I avoided Turbo Frames for a long time because I found it hard to design an app to work well both with and without javascript. Turbo Drive can just be turned off entirely with no real issues, but when you’re using Turbo Frames it’s suddenly part of the design and architecture of the app itself.

I have a Rails app with event registrations. The models are Event [main event page] → EventDate [event recurrences] → AudienceMember [registrations] using has many/belongs_to, and the audience member registration form should ideally be on the event page itself (the show action). This lends itself extremely well to Turbo Frames. In the olden days, I’m sure this could be done with a super-complex nested form workflow that would make my head spin or use some weird ajax, but even then it would be hard since the audience_members/new.html.erb is supposed to be open while everything else about events is locked down and secure.

With Turbo Frames I can keep the audience_members/new.html.erb separate (with the form getting the event_date_ids into a dropdown through params passed from the event), and use a frame to show this view in events/show.html.erb. Like this:

<%# app/views/events/show.html.erb %>
<h1>Event main page</h1>
<p>Bla bla bla description</p>

<h2>Register for event</h2>
<turbo-frame id="new_audience_member" src="/audience_members/new?event_date_ids=1,2,3">
  <%# Show spinner if there's a delay showing the frame %>
  <div class="spinner"><svg><!-- spinner markup --></svg></div>
  <%# Fallback content - link to the standalone form %>
    <p><a href="/audience_members/new">Register</a></p>

Note the noscript tag inside the turbo-frame to provide a fallback link to the frame itself. I hide the spinner when the html element has a no-js class.

.no-js .spinner {
  display: none;

See here for more details on the no-js class approach.

Now, if you actually visit that link without the event_date_ids param, it’s not the same. The event date dropdown will in my case have all available dates instead of only the ones scoped to the parent event. But the form will still work. You can register for events without javascript. The website will definitely be better with it, it will be more intuitive and the form will be on a page where it makes more sense than it does on its own, but it will work without javascript.

I believe that in most cases, if we’re careful with the business logic and when writing views, we really can have it both ways - working no-js websites and intuitive javascript-enhanced interfaces.

1 Like

This is great, I’m actually trying to do something with a similar model. As a study context for this, I’m creating a “Tips” app that allows you to remember tips from holiday to holiday (ex, I gave the postal worker $20 last year). To that effect, a “Tip” is:

  • A person (tippee)
  • An event (optional, ex “Christmas 2022”)
  • An amount (ex, $20)

When I go to create the “Tip”, I would love to have a “Create new tippee” (like your “new audience member”) in the same form. So, you either select an existing person from a drop-down or create a new person on-the-fly.

At first, I was going to try doing it like a <turbo-frame>; but then I realized that I would end up having a <form> element (the inline Tippee form) nested inside another <form> element (the overall Tip page form). That said, I’m super new to this, so maybe that’s not a problem - or maybe the inner form would somehow be stripped-out? I’m not sure.

My next thought is to open the “new tippee (person)” form in a Modal window, and then emit some sort of event when it is done that the other form (“tip”) can listen for and use to update the Person drop-down menu.

Anyway, forgive me thinking out-load here - I think I really gotta go read the Hotwire docs front-to-back to get a better sense of what is possible.

Even does not follow the hard line of progressive ehancement. A few months ago I tried navigating through the app with JS turned off and I believe you can’t even send an email because the SEND button is inside a JS toggled dropdown.

That is not to say that they did a poor job at it, but to show how hard it is to be fully progressivily enhanced.

100% I think I am having a hard time trying to decide if this is a “progressive enhancement framework” or simply a “framework for writing less JavaScript”. Clearly, there can be a huge overlap in that Venn diagram. But, I’m trying to figure out how much I actually need to worry about biasing one way or the other.

1 Like

I’m really struggling to wrap my head around this stuff. Please allow me to just “stream of conscience” for a minute.

In Angular, triggering a modal window would be relatively straightforward because I would have a Layout for modals and I would have a View that was specifically designed to be in a modal. In a Hotwire context, from what I understand, there is no sense of being “designed to be in a modal” since the modal layout itself would be a progressive enhancement. Which means, I need to have a View that works as a standalone page; but, then can be progressively enhanced to be in a modal window.

The problem with that (from what I’ve been reading), is that the “modalized” version of the view would have to have a <turbo-frame> with the “modal” ID and a modal controller that handles all the modally bits (such as using Esc key to close the modal). But, this should not be there in the non-modal version since having that Controller in place wouldn’t make sense.

Now, one solution to this might be to create a modal-ready version of the View (ie, one that is only ever intended to be a modal and has the <turbo-frame> embedded and ready to go. But, the problem I see with this is that I am now moving away from the concept of progressive enhancement and I’m moving towards a “SPA framework”. Which, feels like it’s defeating the whole point of Hotwire.

Anyway, I don’t have a solid question in this rambling - just expressing my frustration in adjusting my brain to work in a Hotwire context.

This post over here starts to address some of my random thoughts – Turbo stream with modal and reusable views - #6 by james-em – still, even reading about it, there are many moving parts to keep in my head.

We created BREAD views for a part of our application a few weeks ago (Browse, Read, Edit, Add, Delete). Each of the views were separate views and worked with (or without) JavaScript. Last week our UX team asked to change the Add view to a modal when on the Browse view. In changing just a few lines of code in 2 existing files our app now renders the Add view as a model for JS browsers, and a separate view for non-js browsers. It took just a few minutes to make this change.

Granted, our application already includes a modal framework where a turbo-frame styled with CSS to look like a modal wrapped in elements to allow close with click or ESC on every page. CSS :empty controls the visibility of the modal. To show the modal we changed the link in the Browse view to target the modal turbo frame. We the wrapped the Add view with the modal turbo frame. To close the modal, JavaScript simply clears the content of the turbo frame and its hidden again.

When the UX team asked us to change the Add view to a modal, one of the developers rolled his eyes and said “this is going to take some time”. Because of past experience in other frameworks. Afterward, he was laughing at the simplicity of the solution. I was also pleasantly surprised.

We did not create any new routes or views. It works the traditional way without JS and more like an SPA with JavaScript. A perfect example of progressive enhancement. Not every situation is like this experience, but in my experience they often are when using Hotwire.

1 Like

That sounds really great. I’d love to hear just a little bit more about what you had to change to get the Add view to work a a modal - just I can start to build-up a better mental model of how things can be rewired like that.

- <%= link_to 'New Comment', new_comments_path %>
+ <%= link_to 'New Comment', new_comments_path, data: { turbo_frame: 'modal' } %>


<!-- already part of layout -->
<div id="modal-wrapper" data-controller="turbo-frame">
  <turbo-frame data-turbo-frame-target="element" id="modal"></turbo-frame>
  <div id="modal-background" class="styles-to-render-and-hide-background-when-previous-sibling-empty" data-action="click->turbo-frame#clear:stop turbo:before-cache@window->turbo-frame#clear:stop">
<h1>New Comment</>

-  <%= form_with url: comments_path, method: 'post', model: @comment do |form| %>
+  <%= render( "3xl")) do %>
+    <%= form_with url: comments_path, method: 'post', model: @comment do |form| 
+      data: { turbo_frame: 'modal' } do |form| %>
      <%= submit_tag t('') %>
    <% end %>
+ <% end %>

The TurboModalComponent view component adds a turbo-frame element and common modal elements (e.g. close button, ESC keyboard shortcut, etc).