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>
<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.
#message_2
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.
| action | What is preserved | Where the template ends up | Typical meaning |
|---|---|---|---|
| append | target and current children | at the end of the target | add an entry to a list |
| prepend | target and current children | at the start of the target | show the newest item first |
| before | the whole target | before the target | insert a neighbouring row |
| after | the whole target | after the target | add an explanation or a continuation |
| replace | nothing from the old target | in place of the target | re-render a component |
| update | the target's shell | inside the emptied target | change the content of a counter or a panel |
| remove | the target's neighbours | no template | remove an entry |
| refresh | depends on the page refresh strategy | no template | refresh 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.
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>
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
append or prepend. Pick the side where the user expects the new entry.replace if you need a new shell; update if the shell is a stable contract.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
- Is the target's ID stable between renders?
- Do you need to keep the element itself or only its place? That is the choice between
updateandreplace. - Is there unsaved browser state inside the target?
- Do you need one operation for a set of elements, or different operations in different places?
- 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.