<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>The Hotwire Club</title>
    <description></description>
    <link>https://hotwire.club/</link>
    <atom:link href="https://hotwire.club/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Tue, 19 May 2026 13:07:46 +0000</pubDate>
    <lastBuildDate>Tue, 19 May 2026 13:07:46 +0000</lastBuildDate>
    <generator>Jekyll v4.3.3</generator>
    
      <item>
        <title>Turbo Drive - Shared Element View Transitions</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;This is the third installment in our View Transitions series. In &lt;a href=&quot;../2024-11-19-turbo-drive-swiper-view-transitions/&quot;&gt;Challenge 35&lt;/a&gt; we built swiper-like page transitions with Turbo Drive, and in &lt;a href=&quot;../2025-06-10-turbo-streams-list-animation-view-transitions/&quot;&gt;Challenge 38&lt;/a&gt; we animated list appends with Turbo Streams. What we haven’t covered yet is the &lt;em&gt;shared element transition&lt;/em&gt;, the one you know from native apps where a thumbnail in a grid smoothly morphs into the hero image on the detail page.&lt;/p&gt;

&lt;p&gt;The View Transitions API makes this surprisingly straightforward. When two elements on different pages share the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name&lt;/code&gt;, the browser automatically interpolates between their position, size, and appearance during a navigation. You don’t need a JavaScript animation library or manual coordinate math - you assign a name, and the browser handles the rest.&lt;/p&gt;

&lt;p&gt;There’s one important constraint to keep in mind, though: that name must be unique per page. If two elements share the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name&lt;/code&gt; at the same time, the entire transition aborts silently. In a gallery grid with dozens of thumbnails, that’s something you’ll need to think about.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We have a simple photo gallery. The &lt;a href=&quot;https://stackblitz.com/edit/turbo-drive-shared-element-transitions?file=index.html&quot;&gt;index page&lt;/a&gt; shows a grid of cards, each with a thumbnail image and a title. Clicking a card navigates to a &lt;a href=&quot;https://stackblitz.com/edit/turbo-drive-shared-element-transitions?file=photo1.html&quot;&gt;detail page&lt;/a&gt; that shows the same image enlarged as a hero, plus a body of text.&lt;/p&gt;

&lt;p&gt;View Transitions are already enabled via the meta tag:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-drive-shared-element-transitions?file=index.html%3AL6&quot;&gt;https://stackblitz.com/edit/turbo-drive-shared-element-transitions?file=index.html%3AL6&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right now, navigating between the index and a detail page triggers the default cross-fade, because no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name&lt;/code&gt; is set on any element yet. The &lt;a href=&quot;https://stackblitz.com/edit/turbo-drive-shared-element-transitions?file=view-transitions.css&quot;&gt;stylesheet&lt;/a&gt; already contains some basic transition configuration, but it’s waiting for the names to be wired up.&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;Make the clicked card’s image smoothly morph into the hero image on the detail page, and back again when the user navigates with the browser back button.&lt;/p&gt;

&lt;p&gt;Two things need to happen:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Assign a unique &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name&lt;/code&gt; to each card image on the index page.&lt;/strong&gt; Each image needs its own name, because names must be unique per page. If two images share the same name, the transition breaks silently. Hint: inline styles are the cleanest way to bind a dynamic name to a DOM node.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Give the detail page’s hero image the same name as the card it came from.&lt;/strong&gt; When the browser sees the same name disappearing on the old page and appearing on the new one, it creates the morph animation automatically.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-drive-shared-element-transitions?file=index.html&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s what the result looks like - the thumbnail lifts out of the grid, scales up, and settles into the hero position on the detail page. Navigating back reverses the animation.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20260512_Turbo_Drive_Shared_Element_View_Transitions.gif&quot; alt=&quot;Gallery grid where clicking a thumbnail morphs it into the hero image on the detail page, and browser back reverses the animation&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reminder:&lt;/strong&gt; the Node server simulates a Rails backend. If you make changes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.js&lt;/code&gt;, restart with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm start&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;teaser&quot;&gt;Teaser&lt;/h2&gt;

&lt;p&gt;What happens when the user navigates backward? The morph plays in reverse by default, which already looks good. But you can go further: Turbo sets a &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive#custom-rendering&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-visit-direction&lt;/code&gt;&lt;/a&gt; attribute on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element during rendering. Try using it to apply a different easing curve or duration for the reverse transition, so that returning to the grid feels distinct from opening a detail page.&lt;/p&gt;

&lt;h2 id=&quot;caveat&quot;&gt;Caveat&lt;/h2&gt;

&lt;p&gt;The uniqueness constraint on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name&lt;/code&gt; is strict. If the same name appears on more than one element at any point during the transition, the browser aborts the entire animation and falls back to a hard cut. This is easy to trip over in a gallery - if your index page renders 20 thumbnails and you give all of them &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name: hero&lt;/code&gt;, nothing will animate. Each card image needs its own scoped name.&lt;/p&gt;

&lt;p&gt;In a Rails context, this maps naturally to the model ID: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;style=&quot;view-transition-name: photo-&amp;lt;%= photo.id %&amp;gt;&quot;&lt;/code&gt;. On the detail page only one hero image exists, so there’s no collision risk - just make sure the name matches the one from the index.&lt;/p&gt;

&lt;p&gt;One more thing to watch for is Turbo’s page cache. If a cached version of the index page has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name&lt;/code&gt; set on elements that are also present in the incoming page, you might get duplicate names during the snapshot restoration. Test with caching enabled to make sure your transitions survive the round trip.&lt;/p&gt;
</description>
        <pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2026-05-12-turbo-drive-shared-element-view-transitions/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2026-05-12-turbo-drive-shared-element-view-transitions/</guid>
        
        <category>View Transitions</category>
        
        
        <category>Turbo Drive</category>
        
      </item>
    
      <item>
        <title>Turbo Frames - Error Boundaries</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;You’ve seen it: a Turbo Frame request hits a 500, and instead of anything useful, the user gets “Content Missing.” Or worse - a network blip leaves a lazy-loaded frame silently empty. No feedback, no way to recover.&lt;/p&gt;

&lt;p&gt;React solved this years ago with &lt;a href=&quot;https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary&quot;&gt;Error Boundaries&lt;/a&gt; - a wrapper component that catches failures in its children and renders fallback UI. We can do the same thing in Hotwire with a Stimulus controller that listens for &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-missing&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:fetch-request-error&lt;/code&gt;&lt;/a&gt;, shows a clean error state, and offers a retry button.&lt;/p&gt;

&lt;p&gt;Building on the frame lifecycle work from &lt;a href=&quot;../2026-01-20-turbo-frames-loading-spinner/&quot;&gt;Challenge 44&lt;/a&gt; (loading spinners) and &lt;a href=&quot;../2026-03-10-turbo-frames-form-loading-states/&quot;&gt;Challenge 46&lt;/a&gt; (form loading states) - this challenge handles the failure path.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We have a dashboard with three lazy-loaded Turbo Frames: &lt;strong&gt;Recent Activity&lt;/strong&gt;, &lt;strong&gt;Notifications&lt;/strong&gt;, and &lt;strong&gt;Billing&lt;/strong&gt;. Each frame has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute and loads independently on page load.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-error-boundaries?file=index.js&quot;&gt;server&lt;/a&gt; is a small Express app standing in for a Rails backend. It has three routes - but not all of them are healthy:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/frames/activity&lt;/code&gt; returns valid frame content.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/frames/notifications&lt;/code&gt; returns a &lt;strong&gt;500 error&lt;/strong&gt; (after a short delay) - a generic error page with no matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt;, which triggers &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-missing&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/frames/billing&lt;/code&gt; &lt;strong&gt;drops the connection&lt;/strong&gt;, simulating a network failure that triggers &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:fetch-request-error&lt;/code&gt;. Note: this one takes a while to show — the browser needs to time out the request before the error fires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Right now, the notifications frame shows “Content Missing” and the billing frame stays stuck on its loading state. The &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-error-boundaries?file=controllers%2Ferror_boundary_controller.js&quot;&gt;error_boundary_controller.js&lt;/a&gt; has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;connect()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;showError()&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;retry()&lt;/code&gt; pre-filled. Your job is the two event handlers.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;error-boundary&lt;/code&gt; controller lives on a wrapper &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;div&amp;gt;&lt;/code&gt; around each frame, with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;frame&lt;/code&gt; target and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt; as an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;errorTemplate&lt;/code&gt; target. The action descriptors for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-missing&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:fetch-request-error&lt;/code&gt; are wired on the frame element, bubbling up to the controller.&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;One file needs work - &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-error-boundaries?file=controllers%2Ferror_boundary_controller.js&quot;&gt;error_boundary_controller.js&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;The controller skeleton already handles &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;connect()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;showError(message)&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;retry()&lt;/code&gt; for you. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;showError&lt;/code&gt; method clones the error template, fills in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[data-role=&quot;message&quot;]&lt;/code&gt; element with the message you pass, and swaps it into the frame. Your job is the two event handlers — each one deals with a different type of failure:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Handle &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-missing&lt;/code&gt;.&lt;/strong&gt; In &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-error-boundaries?file=controllers%2Ferror_boundary_controller.js%3AL13&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handleFrameMissing&lt;/code&gt;&lt;/a&gt;, call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.preventDefault()&lt;/code&gt; to suppress the “Content Missing” message. Then read &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.detail.response&lt;/code&gt; — it’s a standard &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response&quot;&gt;Response&lt;/a&gt; object with the server’s error response. Extract the status code and pass a user-friendly message to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;showError()&lt;/code&gt;, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Something went wrong. Please try again later.&quot;&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Handle &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:fetch-request-error&lt;/code&gt;.&lt;/strong&gt; In &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-error-boundaries?file=controllers%2Ferror_boundary_controller.js%3AL19&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handleFetchError&lt;/code&gt;&lt;/a&gt;, call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.preventDefault()&lt;/code&gt; and pass a network-specific message to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;showError()&lt;/code&gt;. This time there’s no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.detail.response&lt;/code&gt; — the request never got a response. Instead, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.detail.error&lt;/code&gt; has the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error&quot;&gt;Error&lt;/a&gt; object describing the failure.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-frames-error-boundaries?file=controllers%2Ferror_boundary_controller.js&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s the end result - activity loads normally, notifications shows an error state instead of “Content Missing”, billing eventually shows a network error, and clicking Retry re-fetches:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20260428_Turbo_Frames_Error_Boundaries.gif&quot; alt=&quot;Dashboard with three frames: activity loads successfully, notifications shows HTTP error state, billing shows network error state, clicking retry re-fetches&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reminder:&lt;/strong&gt; the Node server simulates a Rails backend. If you make changes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.js&lt;/code&gt;, restart with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm start&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;teaser&quot;&gt;Teaser&lt;/h2&gt;

&lt;p&gt;What if you added exponential backoff - automatically retrying 3 times with increasing delays before showing the error UI? And could you announce failures to screen readers using an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aria-live&lt;/code&gt; region on the error template?&lt;/p&gt;

&lt;h2 id=&quot;caveat&quot;&gt;Caveat&lt;/h2&gt;

&lt;p&gt;The two events cover different failure modes. &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-missing&lt;/code&gt;&lt;/a&gt; fires when the HTTP request succeeds (you get a response) but the HTML doesn’t contain a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; with a matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; - the typical case for 4xx/5xx error pages. &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:fetch-request-error&lt;/code&gt;&lt;/a&gt; fires when the request itself fails at the network level - no response at all. You need both for a robust error boundary.&lt;/p&gt;

&lt;p&gt;Also worth knowing: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-missing&lt;/code&gt; provides &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.detail.visit&lt;/code&gt; - a function that replaces the entire page with the error response. That’s Turbo’s built-in escape hatch, useful for auth redirects (e.g., 401 → login page). The error boundary pattern is better for partial failures where the rest of the page should keep working.&lt;/p&gt;
</description>
        <pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2026-04-28-turbo-frames-error-boundaries/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2026-04-28-turbo-frames-error-boundaries/</guid>
        
        <category>Error Handling</category>
        
        <category>Error Boundaries</category>
        
        <category>Lazy Loading</category>
        
        
        <category>Turbo Frames</category>
        
        <category>Stimulus</category>
        
      </item>
    
      <item>
        <title>Turbo Streams - Custom Stream Actions - pushState</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;../2024-01-30-turbo-streams-custom-stream-actions-localstorage/&quot;&gt;Challenge 19&lt;/a&gt; — building on &lt;a href=&quot;../2023-08-01-turbo-streams-inline-stream-tags/&quot;&gt;Challenge 8&lt;/a&gt; (inline stream tags) and &lt;a href=&quot;../2023-08-15-turbo-streams-custom-stream-actions/&quot;&gt;Challenge 9&lt;/a&gt; (custom actions) — we stored ephemeral state in localStorage. That worked, but the browser’s Back and Forward buttons don’t know about it. Hit Back after switching versions three times? You leave the page entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/pushState&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history.pushState()&lt;/code&gt;&lt;/a&gt; fixes this — but the part most people overlook is the first argument: the &lt;strong&gt;state object&lt;/strong&gt;. It stashes data directly in the history entry. When the user hits Back, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popstate&lt;/code&gt;&lt;/a&gt; hands that object back in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.state&lt;/code&gt;. No URL parsing, no guessing — your Stimulus controller reads the state and knows exactly what to restore. The &lt;strong&gt;URL&lt;/strong&gt; is for the user and for refresh; the &lt;strong&gt;state object&lt;/strong&gt; is for your JavaScript. One human-facing, the other machine-facing.&lt;/p&gt;

&lt;p&gt;Today we’re building a custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_state&lt;/code&gt; stream action (the server pushes URL &lt;em&gt;and&lt;/em&gt; state) and a Stimulus controller that reads it back on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popstate&lt;/code&gt;. &lt;a href=&quot;https://github.com/marcoroth/turbo_power&quot;&gt;TurboPower&lt;/a&gt; ships this out of the box — here we’re building it from scratch.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We have a documentation page with a &lt;strong&gt;version selector&lt;/strong&gt; — a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;select&amp;gt;&lt;/code&gt; dropdown that lets you switch between v1.0, v2.0, and v3.0 of an API. The selector lives inside a Stimulus controller (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version-selector&lt;/code&gt;), and the whole thing is wrapped in a form with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt; — which tells Turbo to send an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Accept: text/vnd.turbo-stream.html&lt;/code&gt; header, so the server knows to respond with Turbo Stream actions instead of a full HTML page.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=index.js&quot;&gt;server&lt;/a&gt; is a small Express app standing in for a Rails backend. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /versions/:version&lt;/code&gt; &lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=index.js%3AL107&quot;&gt;route&lt;/a&gt; does what a Rails controller with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;respond_to { |f| f.turbo_stream }&lt;/code&gt; would do: it checks for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;switch&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore&lt;/code&gt; params, sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Content-Type: text/vnd.turbo-stream.html&lt;/code&gt;, and returns a string of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; tags. If you make changes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.js&lt;/code&gt;, restart with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server is ready to respond with Turbo Streams that replace the content area and the selector itself — but nothing works yet. That’s your job.&lt;/p&gt;

&lt;p&gt;The server already handles a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?version=&lt;/code&gt; query parameter on full page load — that part is done. What’s missing is the pushState/popstate wiring.&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;Two files need work — &lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=app.js&quot;&gt;app.js&lt;/a&gt; for the custom stream action, and the &lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=controllers%2Fversion_selector_controller.js&quot;&gt;version_selector_controller.js&lt;/a&gt; for the Stimulus side:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Register a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_state&lt;/code&gt; custom stream action&lt;/strong&gt; in &lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=app.js%3AL5&quot;&gt;app.js&lt;/a&gt;. The server already includes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream action=&quot;push_state&quot;&amp;gt;&lt;/code&gt; tag in its response, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state&lt;/code&gt; attributes. Your action should read both and call &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/pushState&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history.pushState(state, &quot;&quot;, url)&lt;/code&gt;&lt;/a&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state&lt;/code&gt; attribute is JSON — it carries the restore URL that the Stimulus controller will need later on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popstate&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt; attribute is what the user sees in the address bar.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;navigate&lt;/code&gt;&lt;/strong&gt; in the &lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=controllers%2Fversion_selector_controller.js%3AL11&quot;&gt;Stimulus controller&lt;/a&gt;. When the user picks a version from the dropdown, update the form’s action to the selected value and submit it. The form already has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt;, so this triggers a Turbo Stream response. Disable the select while the request is in flight to prevent double-clicks.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore&lt;/code&gt;&lt;/strong&gt; in the &lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=controllers%2Fversion_selector_controller.js%3AL20&quot;&gt;Stimulus controller&lt;/a&gt;. This method is wired to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popstate@window&lt;/code&gt; in the controller’s action descriptor. When the user hits Back or Forward, the browser hands you the state object you stashed earlier — it’s sitting right there in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.state.restoreUrl&lt;/code&gt;. Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get()&lt;/code&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@rails/request.js&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;responseKind: &quot;turbo-stream&quot;&lt;/code&gt; to fetch that URL. Notice: the restore URL already has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore=1&lt;/code&gt; baked in, which tells the server to skip the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_state&lt;/code&gt; stream tag — otherwise you’d push a new history entry on every Back press, creating an infinite loop.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-streams-custom-actions-push-state?file=controllers%2Fversion_selector_controller.js&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s the end result — note the URL changing with each version switch, and Back/Forward restoring the correct version:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20260414_Turbo_Streams_Custom_Stream_Actions_PushState.gif&quot; alt=&quot;Version selector switching content, URL updating, browser Back and Forward restoring previous versions, and refresh loading the correct version&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;teaser&quot;&gt;Teaser&lt;/h2&gt;

&lt;p&gt;Now that you’ve built &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_state&lt;/code&gt; from scratch, try dropping in &lt;a href=&quot;https://github.com/marcoroth/turbo_power&quot;&gt;TurboPower&lt;/a&gt; and replacing your hand-rolled action with theirs. What would it take to combine this pattern with Turbo’s morphing and page refreshes?&lt;/p&gt;

&lt;h2 id=&quot;caveat&quot;&gt;Caveat&lt;/h2&gt;

&lt;p&gt;A common stumbling point: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popstate&lt;/code&gt;&lt;/a&gt; only fires on actual Back/Forward navigation, never when you call &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/pushState&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pushState()&lt;/code&gt;&lt;/a&gt;. The forward direction (user picks a version) is handled by the stream action; the backward direction (user hits Back) is handled by the Stimulus controller’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore&lt;/code&gt; method. Two different mechanisms, one unified UX.&lt;/p&gt;
</description>
        <pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2026-04-14-turbo-streams-custom-stream-actions-push-state/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2026-04-14-turbo-streams-custom-stream-actions-push-state/</guid>
        
        <category>Custom Stream Actions</category>
        
        <category>History API</category>
        
        <category>pushState</category>
        
        
        <category>Turbo Streams</category>
        
        <category>Stimulus</category>
        
      </item>
    
      <item>
        <title>Turbo Frames - Chained Selects</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;Pick a country, then pick a city — you’ve seen this pattern a thousand times. The second dropdown depends on what you chose in the first. In jQuery-land this meant fetching JSON and manually rebuilding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;option&amp;gt;&lt;/code&gt; elements. In React, you’d manage loading states, store the fetched options in component state, and re-render. With Turbo Frames, the server just re-renders the HTML you already have.&lt;/p&gt;

&lt;p&gt;The trick: wrap the dependent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;select&amp;gt;&lt;/code&gt; in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt;, and when the first one changes, reload that frame with the selected value as a query parameter. The server filters the options and returns the updated frame. No JSON APIs, no manual DOM manipulation.&lt;/p&gt;

&lt;p&gt;The Stimulus controller that ties it together is surprisingly small.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We have a simple form with a &lt;strong&gt;Country&lt;/strong&gt; select. The city select doesn’t exist yet — it should only appear after a country has been chosen.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-chained-selects?file=index.js&quot;&gt;server&lt;/a&gt; has a small dataset of countries and their cities, and accepts a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;country_id&lt;/code&gt; query parameter to return the matching city list. But nothing in the frontend triggers that yet.&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;There’s an empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame id=&quot;city_select&quot;&amp;gt;&lt;/code&gt; in the page. Your job is to wire up a &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-chained-selects?file=controllers%2Fchained_select_controller.js&quot;&gt;Stimulus controller&lt;/a&gt; that loads the city select into that frame when a country is chosen:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;When the country select changes, build a URL with the selected &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;country_id&lt;/code&gt; as a query parameter and set it as the city frame’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt;. The server already knows how to return the right cities — it just needs the parameter.&lt;/li&gt;
  &lt;li&gt;When the country select is reset to the placeholder, the city frame should go back to being empty.&lt;/li&gt;
&lt;/ol&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-frames-chained-selects?file=controllers%2Fchained_select_controller.js%3AL16&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s a preview of the result:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20260324_Turbo_Frames_Chained_Selects.gif&quot; alt=&quot;City select appearing after choosing a country, updating when the country changes&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;teaser&quot;&gt;Teaser&lt;/h2&gt;

&lt;p&gt;What if you had a third level — say, &lt;strong&gt;Country → City → Neighborhood&lt;/strong&gt;? Can you extend the pattern so picking a city loads a neighborhood select into yet another frame?&lt;/p&gt;
</description>
        <pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2026-03-24-turbo-frames-chained-selects/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2026-03-24-turbo-frames-chained-selects/</guid>
        
        <category>select</category>
        
        <category>forms</category>
        
        <category>chained selects</category>
        
        <category>dependent dropdowns</category>
        
        
        <category>Turbo Frames</category>
        
        <category>Stimulus</category>
        
      </item>
    
      <item>
        <title>Turbo Frames - Form Submission Loading States</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;In the &lt;a href=&quot;/blog/2026-01-20-turbo-frames-loading-spinner/&quot;&gt;Loading Spinner&lt;/a&gt; challenge, we added busy indicators for Turbo Frame navigations — links that swap frame content asynchronously.&lt;/p&gt;

&lt;p&gt;But what about forms? When a user submits a form inside a Turbo Frame, Turbo handles the request and swaps the response, but gives &lt;strong&gt;zero visual feedback&lt;/strong&gt; while it’s in flight. The button stays clickable, nothing changes on screen, and the user is left wondering if their click registered — especially on slow connections.&lt;/p&gt;

&lt;p&gt;Back in the Rails-UJS days, we had &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-disable-with&lt;/code&gt; to handle this. Turbo 8 has its own answer, but it’s not automatic.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We have a simple guest book inside a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt;. The &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-form-loading-states?file=index.js&quot;&gt;server&lt;/a&gt; stores entries in memory and redirects back to the frame after a 1.5 second delay — simulating a slow save.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-form-loading-states?file=index.js%3AL26&quot;&gt;form&lt;/a&gt; lives inside the frame, so Turbo intercepts the submission and swaps the response automatically.&lt;/p&gt;

&lt;p&gt;Try it: submit an entry and notice the dead silence while the request is in flight. No spinner, no disabled button, no feedback at all.&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;Make the form feel responsive during submission:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The submit button should change its text and become disabled while the request is in flight. Look up Turbo 8’s successor to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-disable-with&lt;/code&gt; in the &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive#form-submissions&quot;&gt;Turbo Handbook&lt;/a&gt;. You’ll need to edit the button markup in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;renderFrame()&lt;/code&gt; inside &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-form-loading-states?file=index.js&quot;&gt;index.js&lt;/a&gt; — remember to restart the server with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm start&lt;/code&gt; after making changes there.&lt;/li&gt;
  &lt;li&gt;The frame itself should show some visual indication that it’s busy. Remember: Turbo sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[busy]&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[aria-busy]&lt;/code&gt; on frames during requests — how can you use that in CSS?&lt;/li&gt;
&lt;/ol&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-frames-form-loading-states?file=index.js,styles.css%3AL46&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s a preview of the end result:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20260310_Turbo_Frames_Form_Loading_States.gif&quot; alt=&quot;Guest book form with loading feedback during submission&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;teaser&quot;&gt;Teaser&lt;/h2&gt;

&lt;p&gt;Can you combine this with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt;-based spinner approach from the &lt;a href=&quot;/blog/2026-01-20-turbo-frames-loading-spinner/&quot;&gt;Loading Spinner&lt;/a&gt; challenge?&lt;/p&gt;
</description>
        <pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2026-03-10-turbo-frames-form-loading-states/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2026-03-10-turbo-frames-form-loading-states/</guid>
        
        <category>Form Submission</category>
        
        <category>forms</category>
        
        
        <category>Turbo Frames</category>
        
      </item>
    
      <item>
        <title>Turbo Frames - Using External Forms</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;Turbo Frames are a great way to compartmentalize your user interface, but sometimes they require workarounds when it comes to state management.&lt;/p&gt;

&lt;p&gt;One example of such a requirement is when you have to handle a form on your page that refers to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;input&amp;gt;&lt;/code&gt;’s in separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt;s.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We’re building upon the &lt;a href=&quot;/blog/2024-12-10-stimulus-turbo-frames-faceted-search/&quot;&gt;Faceted Search with Stimulus and Turbo Frames&lt;/a&gt; challenge here, but add a new requirement: Search results should also be sortable by publication year, author name (first name in this case) and book title.&lt;/p&gt;

&lt;p&gt;Remember that this uses a single &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-external-forms?file=controllers%2Ffaceted_search_controller.js&quot;&gt;Stimulus controller&lt;/a&gt; to wrap a form and swap a Turbo Frame’s contents to filter the results.&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;The goal is not to alter the Stimulus controller at all. We want to just append a new query parameter, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This query parameter could for example be set to&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name_asc&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name_desc&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title_asc&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title_desc&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I’m going to require a bit fantasy from you, because we are going to look at the NodeJS part of our example here on Stackblitz. I’ve implemented the actual sorting in pure JavaScript here - you have to imagine that in fact we are looking at a Rails controller:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-external-forms?file=index.js%3AL101&quot;&gt;https://stackblitz.com/edit/turbo-frames-external-forms?file=index.js%3AL101&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is sorted list is wrapped in a Turbo Frame inside the server response. What’s missing is the configuration of a select box that should&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;hold an option for every sort parameter and direction&lt;/li&gt;
  &lt;li&gt;somehow refer to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; that’s &lt;strong&gt;outside&lt;/strong&gt; the Turbo Frame, &lt;a href=&quot;https://stackblitz.com/edit/turbo-frames-external-forms?file=index.html%3AL46&quot;&gt;here&lt;/a&gt; (?! 🤯)&lt;/li&gt;
&lt;/ul&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-frames-external-forms?file=index.js%3AL51&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;To try this out you’ll have to kill the node server in Stackblitz and restart it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a preview of the result:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20260203_Turbo_Frames_External_Forms.gif&quot; alt=&quot;User clicking through a faceted search form&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Granted, this is a bit of a contrived example (we could just hoist the sort dropdown to the form), but there will be other, more complex views where you’ll be glad you know about this approach. For example, you might want to reuse the turbo frame with sorting in other contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hint:&lt;/strong&gt; Look up a form input’s possible attributes on MDN!&lt;/p&gt;
</description>
        <pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2026-02-03-turbo-frames-external-form/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2026-02-03-turbo-frames-external-form/</guid>
        
        <category>Form Submission</category>
        
        <category>forms</category>
        
        
        <category>Turbo Frames</category>
        
      </item>
    
      <item>
        <title>Turbo Frames - Loading Spinner</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;Asynchronously loading content via Turbo Frames sometimes comes with a user experience degradation because the frame doesn’t advertise its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;busy&lt;/code&gt; status, and/or becomes unresponsive.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We start with a quite simplistic page that has a bullet point navigation and a Turbo Frame for the contents:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/page1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-turbo-frame=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;async-loader&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Load Page 1&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/page2&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-turbo-frame=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;async-loader&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Load Page 2&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;async-loader&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/page1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &amp;gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Observe that the links in the navigation point to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;async-loader&lt;/code&gt; Turbo Frame, i.e. its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute is targeted.&lt;/p&gt;

&lt;p&gt;The NodeJS backend is set up so that it returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page1.html&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page2.html&lt;/code&gt; respectively:&lt;/p&gt;

&lt;p&gt;https://stackblitz.com/edit/turbo-frames-loading-spinner?file=index.js%3AL12&lt;/p&gt;

&lt;p&gt;These two pages contain just the Turbo frame to exchange:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- page1.html --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;async-loader&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;span&amp;gt;&lt;/span&gt;Page 1 took 1 sec to load&lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- page2.html --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;async-loader&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Page 2 took 2 sec to load&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;To implement loading/busy feedback, use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt; fragment supplied in the index page:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;nt&quot;&gt;&amp;lt;template&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-frame-spinner-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;placeholder&quot;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;↻ Spinner, &lt;span class=&quot;nt&quot;&gt;&amp;lt;em&amp;gt;&lt;/span&gt;can be any&lt;span class=&quot;nt&quot;&gt;&amp;lt;/em&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;HTML&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&amp;lt;/template&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;https://stackblitz.com/edit/turbo-frames-loading-spinner?file=index.html%3AL46&lt;/p&gt;

&lt;p&gt;The crucial point is to clone this fragment and insert it when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;busy&lt;/code&gt; attribute appears, and remove it afterwards. For this we make use of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MutationObserver&lt;/code&gt; in a Stimulus controller. Thankfully &lt;a href=&quot;https://stimulus-use.github.io/stimulus-use/#/use-mutation&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMutation&lt;/code&gt;&lt;/a&gt; from the Stimulus Use project makes this trivial. Insert the HTML swapping logic here:&lt;/p&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-frames-loading-spinner?file=controllers%2Fframe_spinner_controller.js%3AL20&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s a preview of the end result:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20260120_Turbo_Frames_Loading_Spinner.gif&quot; alt=&quot;Tabbed Turbo Frame navigation with loading display&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;teaser&quot;&gt;Teaser&lt;/h2&gt;
&lt;p&gt;There’s a way to make this work only with CSS if you don’t need any HTML in the “loading” display!&lt;/p&gt;
</description>
        <pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2026-01-20-turbo-frames-loading-spinner/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2026-01-20-turbo-frames-loading-spinner/</guid>
        
        <category>Lazy Loading</category>
        
        
        <category>Turbo Frames</category>
        
      </item>
    
      <item>
        <title>Stimulus - Web Share API</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;
&lt;p&gt;Among the more esoteric native browser APIs there’s one that is mostly overlooked (well, tbh most of them are) and thus lead to a whole host of third-party solutions when in fact the browser already includes this capability out of the box: The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API&quot;&gt;Web Share API&lt;/a&gt;, which allows a site to share text, files, and URLs in a simple, standardized manner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Of course, there’s a caveat - Firefox does NOT support this functionality: &lt;a href=&quot;https://caniuse.com/web-share&quot;&gt;https://caniuse.com/web-share&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We start with a grid of four images from &lt;a href=&quot;https://picsum.photos/&quot;&gt;https://picsum.photos/&lt;/a&gt;. In the caption of each image, next to the title there’s a link that shows a share button. This link already contains the binding to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;share&lt;/code&gt; Stimulus controller, along with several values and an action defined for each:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;an &lt;em&gt;action&lt;/em&gt; that points to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;share&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt; value that contains the image name (unoriginally just Picsum’s ID)&lt;/li&gt;
  &lt;li&gt;a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text&lt;/code&gt; value for sharing a description (in our case alt text) of the photo&lt;/li&gt;
  &lt;li&gt;a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt; value containing the image URL, and&lt;/li&gt;
  &lt;li&gt;a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file&lt;/code&gt; value pointing to a downloadable resource to share (for demonstration purposes, again just the image URL).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/stimulus-web-share-api?file=index.html%3AL60&quot;&gt;https://stackblitz.com/edit/stimulus-web-share-api?file=index.html%3AL60&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;Your challenge is to use the Web Share API - most notably &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;navigator.share()&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Navigator/canShare&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;navigator.canShare&lt;/code&gt;&lt;/a&gt; - to complete the sharing solution using the provided share data values:&lt;/p&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/stimulus-web-share-api?file=controllers%2Fshare_controller.js%3AL6&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s a preview of the result on Mac OS:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20251125_Stimulus_Web_Share_API.gif&quot; alt=&quot;Share dialogs being opened for four images&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;caveat&quot;&gt;Caveat&lt;/h2&gt;

&lt;p&gt;You will have to use feature detection using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;canShare&lt;/code&gt; for certain share data values!&lt;/p&gt;

</description>
        <pubDate>Tue, 25 Nov 2025 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2025-11-25-stimulus-web-share-api/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2025-11-25-stimulus-web-share-api/</guid>
        
        <category>web apis</category>
        
        
        <category>Stimulus</category>
        
      </item>
    
      <item>
        <title>Turbo Frames - Typeahead Validation</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;Sometimes your application requires validating user input in real time. In order to maximize user experience, for example, you might want to report whether a certain email address, time slot etc. has been taken.&lt;/p&gt;

&lt;p&gt;We are going to explore how this can be achieved just with native Turbo Frames form submission handling and a few client-side event hooks.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;Let’s explore the server side code first. I’ve again replicated a basic Rails controller in Node.js again. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/subdomain/edit&lt;/code&gt; route uses the &lt;a href=&quot;https://stackblitz.com/edit/turbo-typeahead-validation?file=form.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form.html&lt;/code&gt;&lt;/a&gt; template to return, you guessed it, the form. It’s populated with the current value, whether that value is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;invalid&lt;/code&gt;, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;helper&lt;/code&gt; text:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-typeahead-validation?file=index.js%3AL21&quot;&gt;https://stackblitz.com/edit/turbo-typeahead-validation?file=index.js%3AL21&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Emulating an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; action, we respond to a PATCH request on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/subdomain&lt;/code&gt; with either 200 (OK) or 422 (Unprocessable Content) depending on whether the subdomain we entered is contained in a list of taken subdomains:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-typeahead-validation?file=index.js%3AL42&quot;&gt;https://stackblitz.com/edit/turbo-typeahead-validation?file=index.js%3AL42&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Continuing to the client, our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html&lt;/code&gt; only contains a reference to an eager loaded Turbo Frame pointing to said &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/subdomain/edit&lt;/code&gt; route:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-typeahead-validation?file=index.html%3AL41&quot;&gt;https://stackblitz.com/edit/turbo-typeahead-validation?file=index.html%3AL41&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve also included an event handler that listens for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; event of the text field and sets off a form submission:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-typeahead-validation?file=app.js%3AL10&quot;&gt;https://stackblitz.com/edit/turbo-typeahead-validation?file=app.js%3AL10&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In reality you’d likely want to debounce this to save bandwidth and not overload your server.&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;Currently the form is practically unusable because the text field loses focus and we have to manually click into it again. We could use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;autofocus&lt;/code&gt; but that leads to other issues down the road (trust me on this one).&lt;/p&gt;

&lt;p&gt;Your task is to&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;somehow capture and store the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;activeElement&lt;/code&gt; (the one with focus, i.e. the text field),&lt;/li&gt;
  &lt;li&gt;and the current caret positition on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:submit-start&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;and restore both on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-render&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-typeahead-validation?file=app.js%3AL5&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s a preview of the result:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20251020_Turbo_Frames_Typeahead_Validation.gif&quot; alt=&quot;User input being validated in real time&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;caveat&quot;&gt;Caveat&lt;/h2&gt;

&lt;p&gt;You will learn why morphing libraries such as &lt;a href=&quot;https://github.com/bigskysoftware/idiomorph&quot;&gt;Idiomorph&lt;/a&gt; or &lt;a href=&quot;https://github.com/patrick-steele-idem/morphdom&quot;&gt;morphdom&lt;/a&gt; require IDs being set on HTML elements 😉.&lt;/p&gt;

&lt;h2 id=&quot;teaser&quot;&gt;Teaser&lt;/h2&gt;

&lt;p&gt;What would it take to make this solution portable, for example in a Stimulus controller?&lt;/p&gt;
</description>
        <pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2025-10-20-turbo-frames-typeahead-validation/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2025-10-20-turbo-frames-typeahead-validation/</guid>
        
        <category>turbo:frame-render</category>
        
        <category>turbo:submit-start</category>
        
        
        <category>Turbo Frames</category>
        
      </item>
    
      <item>
        <title>Turbo Streams - List Animations Using the View Transitions API</title>
        <description>&lt;h2 id=&quot;premise&quot;&gt;Premise&lt;/h2&gt;

&lt;p&gt;Since I always try to trace out a (web) technology from multiple angles, I wanted to test whether there’d be a use case for View Transitions with &lt;em&gt;Turbo Streams&lt;/em&gt;, too.&lt;/p&gt;

&lt;p&gt;That use case arrived when I had to implement infinite loading using a “Load more” button for a client project. I had a list of items to which more were added using a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream action=&quot;append&quot;&amp;gt;&lt;/code&gt; action. What was asked for was a smooth animation for this event.&lt;/p&gt;

&lt;p&gt;I could’ve used any CSS animation using a class and a timed JavaScript callback to remove that class again, but I wanted to see if View Transitions could do the job, and perhaps more elegantly.&lt;/p&gt;

&lt;h2 id=&quot;starting-point&quot;&gt;Starting Point&lt;/h2&gt;

&lt;p&gt;We start with a simple list of tickets, with an ID of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tickets&lt;/code&gt;. Beneath it there’s a “Load More” button wrapped in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-view-transitions?file=index.html%3AL42&quot;&gt;https://stackblitz.com/edit/turbo-streams-view-transitions?file=index.html%3AL42&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The form’s action points to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; route which responds with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; to mimic the typical behavior of a Ruby on Rails controller:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-view-transitions?file=index.js%3AL12&quot;&gt;https://stackblitz.com/edit/turbo-streams-view-transitions?file=index.js%3AL12&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the stylesheet I’ve prepared a view transition named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new-ticket&lt;/code&gt; which uses an animation to slide in the respective new element from the right:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackblitz.com/edit/turbo-streams-view-transitions?file=styles.css&quot;&gt;https://stackblitz.com/edit/turbo-streams-view-transitions?file=styles.css&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;challenge&quot;&gt;Challenge&lt;/h2&gt;

&lt;p&gt;Akin to Turbo Frames, Turbo Streams also fire a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:before-stream-render&lt;/code&gt; &lt;a href=&quot;https://turbo.hotwired.dev/reference/events#turbo%3Abefore-stream-render&quot;&gt;event&lt;/a&gt; that allows to customize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Your task is, similar to the preceding challenges, to overwrite the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; method. Note that you have to manually invoke a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#a_javascript-powered_custom_same-document_spa_transition&quot;&gt;same document transition&lt;/a&gt; again!&lt;/p&gt;

&lt;iframe src=&quot;https://stackblitz.com/edit/turbo-streams-view-transitions?file=app.js%3AL7&amp;amp;embed=1&quot; style=&quot;width: 100%; height: 480px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Here’s a look at the result:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/blog/20250610_Turbo_Streams_List_Animations.gif&quot; alt=&quot;List items being appended with a View Transition animation&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;caveat&quot;&gt;Caveat&lt;/h2&gt;

&lt;p&gt;Keep in mind that a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view-transition-name&lt;/code&gt; must be unique across the elements of a page!&lt;/p&gt;
</description>
        <pubDate>Tue, 10 Jun 2025 00:00:00 +0000</pubDate>
        <link>https://hotwire.club/blog/2025-06-10-turbo-streams-list-animation-view-transitions/</link>
        <guid isPermaLink="true">https://hotwire.club/blog/2025-06-10-turbo-streams-list-animation-view-transitions/</guid>
        
        <category>View Transitions</category>
        
        <category>turbo:before-stream-render</category>
        
        
        <category>Turbo Streams</category>
        
      </item>
    
  </channel>
</rss>