Turbo Stream Actions: An Interactive Atlas of DOM Operations

Turbo Stream Actions: an interactive atlas of DOM operations

A Turbo Stream is a small declarative command over an already existing DOM: find the target, take HTML from a <template>, and perform one operation. To understand this mechanism you do not need a Rails controller, a WebSocket, or even a network request. It is enough to see exactly which nodes stay, move, and disappear.

Below is an interactive atlas of all eight standard actions from the Turbo Handbook. The lab simulates the DOM effect right inside the article. It deliberately does not explain message delivery, broadcasting, or server-side rendering: only the element format and the result of embedding it into the page.

One command has four parts

<turbo-stream
  action="append"
  target="messages">
  <template>
    <article>New message</article>
  </template>
</turbo-stream>
actionWhat to do with the target once it is found.
target / targetsA single DOM ID, or a CSS selector for several nodes.
templateThe HTML that will be inserted, if the action needs it.
the stream itselfAfter execution Turbo removes the command element from the DOM.
The target does not have to be a <turbo-frame>. It can be a <div>, a table row, a card, a counter, or any other element in the document that can be found by ID or by CSS selector.

Lab: what each action visually does

Pick an action, then a single or multiple target, and press "Apply". The blue outline shows the nodes Turbo will find before the operation. The command on the left changes together with the settings.

DOM laboratory
Number of targets
Will find #message_2
Stream elementHTML

    
Documentlive DOM
example.test/inbox
Inboxpage render #1
0 nodes Pick an action. The DOM has not been changed yet.

Eight actions, grouped by geometry

The names are easier to remember not as a list, but by where the boundary of the change lies.

append

Adds the template content as the last child nodes inside the target. The target itself stays.

prepend

Adds the content as the first child nodes inside the target. The target itself stays.

before

Inserts the template next to the target, immediately before it. The new node becomes its sibling.

after

Inserts the template next to the target, immediately after it. The new node becomes its sibling.

replace

Removes the target element itself and puts the template in its place. The old shell and its listeners are gone.

update

Keeps the target element but replaces its child nodes. The shell, its attributes, and the listeners bound to it remain.

×remove

Removes the target. No <template> is needed because there is nothing to insert.

refresh

Triggers a page refresh. It has no target and no template: it is an action on the visit, not on an individual node.

actionWhat is preservedWhere the template ends upTypical meaning
appendtarget and current childrenat the end of the targetadd an entry to a list
prependtarget and current childrenat the start of the targetshow the newest item first
beforethe whole targetbefore the targetinsert a neighbouring row
afterthe whole targetafter the targetadd an explanation or a continuation
replacenothing from the old targetin place of the targetre-render a component
updatethe target's shellinside the emptied targetchange the content of a counter or a panel
removethe target's neighboursno templateremove an entry
refreshdepends on the page refresh strategyno templaterefresh the whole page

A subtlety of append and prepend: a matching ID gets replaced

If the first element of the template has an ID that already belongs to a direct child of the target container, Turbo replaces the existing child instead of adding a duplicate. This makes re-adding an entry more idempotent, but the check applies specifically to direct children.

morph is not a ninth action. It is an execution method for replace or update: method="morph". The plain variant without the attribute swaps the node or its children outright; morph tries to reconcile the old and new trees and keep the DOM nodes that fit. So its effect depends on the structure and on stable IDs.

Actions With Multiple Targets

target="message_2" means looking up a single element by DOM ID. targets=".pending" means querySelectorAll(".pending"): the same operation is applied to every element found, and the template content is cloned for each target.

CSS selector playground

Try the selectors .pending, .priority, [data-owner="me"], or .task:not(.done). The selector is evaluated at the moment the command runs. If there are no matches, the DOM simply does not change. If the CSS selector is syntactically invalid, the command cannot resolve its targets.

Multiple targets do not mean multiple different changes. One element with targets repeats one action over a set of nodes. To prepend in one place, update in another, and remove in a third, you need several <turbo-stream> elements.

Several commands run in order

One stream message can contain several adjacent <turbo-stream> elements. Turbo executes them in the order they appear. This is a separate axis of composition: targets multiplies one operation, while several stream elements describe a scenario made of different operations.

Two operations, one local scenario

<turbo-stream action="prepend"
  target="wishlist_items">
  <template>...Work...</template>
</turbo-stream>

<turbo-stream action="update"
  target="quick_create">
  <template>List created</template>
</turbo-stream>
Wishlists2 lists
Toggle any existing checkbox, then run both operations. Its local state will be preserved.

The first command only adds a new row. The second only changes the content of the quick-create form. The rows that already exist are not re-rendered, so an unsaved checkbox choice stays in the browser. This is the advantage of small DOM operations: the boundary of the change is visible right in the message markup.

How to choose an action

Is the list growing?append or prepend. Pick the side where the user expects the new entry.
Is a component changing?replace if you need a new shell; update if the shell is a stable contract.
The same edit everywhere?targets with a narrow CSS selector. For different edits use several stream elements.

The practical rule is the same as for any partial update: choose the smallest area that has actually gone stale. The larger the target, the higher the chance of losing focus, a field value, a text selection, scroll position, or the state of a third-party widget.

A short checklist before use

  1. Is the target's ID stable between renders?
  2. Do you need to keep the element itself or only its place? That is the choice between update and replace.
  3. Is there unsaved browser state inside the target?
  4. Do you need one operation for a set of elements, or different operations in different places?
  5. Will the new markup remain reachable for the next command by the same ID or selector?

For the bigger picture of navigation regions, go back to "Turbo Frames from Scratch". There, a Frame is treated as a navigation context and a Stream as a targeted DOM command. Here we deliberately covered only the second half.