# TurboPower: the missing Turbo Stream actions, from morph to localStorage

> TurboPower is a small TypeScript library that registers a large set of custom actions on Turbo's StreamActions object, so a server can update a CSS class, write to localStorage, focus an element, push browser history state or show a notification without hand-rolling a custom action each time. It also bundles the morph action from Turbo Morph.

**marcoroth/turbo_power** — Custom Actions power-pack for Turbo Streams

- Repository: https://github.com/marcoroth/turbo_power
- Stars: 518 · Forks: 20
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/marcoroth-turbo-power

## What TurboPower actually is: a plugin for the stream action registry

TurboPower is a power-pack for Turbo Streams. It provides Turbo Streams with a bunch of new actions and additionally adds the morph action from Turbo Morph.

That description is short but the mechanism matters. Turbo does not have a fixed set of stream actions; it has a StreamActions object that the library looks up when a stream arrives from the server. A custom action is something you register on that object, and then the server can emit it by name. TurboPower is a package of about sixty such actions, registered in one call, so you do not have to write and register each one by hand.

The installation is two steps. Add the package:

```bash
yarn add turbo_power
```

Then import it and initialise it against Turbo's action registry in your application.js:

```diff
// application.js
import * as Turbo from '@hotwired/turbo'

+import TurboPower from 'turbo_power'
+TurboPower.initialize(Turbo.StreamActions)
```

The one-line initialise is the whole integration. It walks its own action list and adds each one to the registry you pass in, so after that a server-side stream can say morph or set_local_storage_item and the browser will know what to do.

The note in the README is that this requires Turbo 7.2 or newer. The package.json peer dependency is stricter still, @hotwired/turbo at 8.0.0 or newer, so the two statements in the documentation are not the same and the peer dependency is the one your package manager will enforce.

For TypeScript users there is a specific instruction. The type declarations of turbo_power re-export types from @hotwired/turbo, which since Turbo 8 ships its types via the community-maintained @types/hotwired__turbo package. It is not a runtime dependency of turbo_power, so if you consume the types you will want to install it yourself:

```bash
> yarn add --dev @types/hotwired__turbo
```

That is a good example of a package being explicit about what it does not bundle, and it is also a real decision point: on a Turbo 8 project you end up with type definitions that come from DefinitelyTyped rather than from the framework itself, and TurboPower makes sure the two agree with each other.

## The action catalogue, and how it is grouped

The bulk of the README is a catalogue of the actions, grouped by what they touch, and the grouping is the best guide to what the library actually covers.

The DOM actions are the ones Turbo itself does not have. There is graft for moving a node into another parent, morph for replacing an element's content while preserving its state, inner_html, insert_adjacent_html with a position of beforeend by default, insert_adjacent_text, outer_html, text_content, and set_meta for writing a meta tag.

The attribute group is the largest one. add_css_class, remove_attribute, remove_css_class, set_attribute, set_dataset_attribute, set_property, set_style, set_styles for setting several at once, set_value for form inputs, toggle_attribute with a force argument, toggle_css_class, and replace_css_class for swapping one class for another. The naming convention is consistent across all of them, which is what makes the catalogue usable as a reference rather than a set of trivia.

Then there is a small number of singletons. The event group has dispatch_event with a detail object. The form group has reset_form. The browser group has reload, scroll_into_view, set_focus and set_title.

Storage is the group with the most entries, and the reason is that the Web Storage API has three scopes and each supports several operations. There is clear_storage with a type, and then clear_local_storage and clear_session_storage as the specific cases. There is remove_storage_item with a type, and remove_local_storage_item and remove_session_storage_item. And there is set_storage_item with a key, a value and a type, with set_local_storage_item and set_session_storage_item as the specific cases.

The pattern is clear: a generic action taking a type parameter, and a specialised action for each of the two common scopes. That is a sensible API because the generic form is what you need for a configurable target and the specific forms are what you want in a template where the scope is fixed.

The document group has set_cookie and set_cookie_item, the browser history group has history_back, history_forward, history_go with a delta, push_state and replace_state, both taking a url, a title and a state.

The debug group has console_log with a level and console_table, the notification group has notification, and the Turbo-specific group has redirect_to with optional turbo_action and turbo_frame parameters, and turbo_clear_cache.

Finally there are the Turbo frame actions, turbo_frame_reload and turbo_frame_set_src, and the Turbo progress bar actions, turbo_progress_bar_show, turbo_progress_bar_hide and turbo_progress_bar_set_value.

## The scroll action, and the one that has several forms

One entry in the catalogue is worth pulling out, because it is the only one documented with more than one signature and it shows how the library handles an API with optional parameters.

The scroll action is listed as a single action with three forms underneath it:

```
  * `turbo_stream.scroll_into_view(targets)`
  * `turbo_stream.scroll_into_view(targets, align_to_top)`
  * `turbo_stream.scroll_into_view(targets, behavior:,  block:, inline:)`
```

The first takes the targets and nothing else. The second adds align_to_top. The third takes the native scrolling options, behaviour, block and inline, with the values left to the caller.

The reason the catalogue shows three rather than one is a practical one about how these actions travel. A Turbo Stream action is serialised into the stream payload as a name and a set of arguments, and it arrives in the browser as a call. If the action always accepted all the optional arguments, every server-side call would have to pass nulls for the ones it did not care about, and the browser code would have to sort out which ones were really set. Splitting into forms lets the common case stay small and the full case be available when a page genuinely needs to control the scroll behaviour.

The behaviour, block and inline names map directly to the standard scrollIntoView options in the browser, so this is not an invented vocabulary. align_to_top is the additional one, and it is the option that decides whether an element scrolls to the top of the viewport or just becomes visible, which is the difference between a sticky header covering the content and the content being readable.

This is the kind of detail that the whole library is really about. Turbo Stream gives you a set of primitives, and a large part of ordinary interface work needs a primitive that is not in the set. Rather than every application writing its own version of scroll into view, or worse, each writing it slightly differently, TurboPower supplies one and names it the same way as the rest.

## Where the idea came from, and the framework integrations

The README credits the influence directly: TurboPower is heavily inspired by CableReady and its operations, and the library aims to bring the same level of operation-diversity to Turbo Streams.

CableReady is the StimulusReflex library that grew a large catalogue of what it calls operations, and the comparison is instructive because Turbo Streams have historically been much thinner. A Rails developer using CableReady could update a form's value, set a cookie, dispatch an event or focus an element from the server, and the equivalent in plain Turbo often meant writing a custom action in JavaScript and a matching helper on the Ruby side.

TurboPower moves that catalogue into Turbo itself, and the naming convention follows the Turbo Stream action style with an underscore separator, so the turbo_stream.set_local_storage_item reads the way the built-in actions do.

The framework integration is handled by separate projects rather than by this one, and the README points to each. For Rails, checkout the instructions in the turbo_power-rails repo. For Django, checkout the doc of the django-turbo-helper repo.

That separation is the right call for a JavaScript library, because the helper side is where the ergonomics live. On the Ruby side you want a turbo_stream.set_local_storage_item(key, value) helper that reads naturally in a view, and on the Python side a similar wrapper, and neither belongs in the package that registers the browser actions. The TurboPower package stays framework-agnostic and the two integrations build on it.

The morph action is the exception, and it is worth noting that it comes from a separate package too. The README lists the morph action as a link to the Turbo Morph repository, and the acknowledgements section confirms Turbo Morph is MIT-licensed open source software from the same author, alongside Turbo itself which is MIT-licensed from Basecamp. TurboPower is MIT-licensed open source software from Marco Roth.

So the whole stack is MIT licensed, which matters for a library that a commercial Rails application depends on.

## The build, the tests and the playground

The package.json and the repository layout show how the library is built and verified.

The build is a two step TypeScript and Rollup pipeline. The build script runs tsc with noEmit false, declaration true and emitDeclarationOnly true, outputting to dist/types, and then runs rollup -c. So the type declarations are produced by tsc and the JavaScript bundle by Rollup, and the two live in the same dist directory.

The package entry points are declared alongside that. The main and module fields both point at dist/turbo_power.js, the unpkg field points at dist/turbo_power.umd.js for a script tag, and the types field points at dist/types/index.d.ts. The sideEffects flag is false, which lets a bundler tree-shake the package if a project only uses part of it.

Rollup is configured through rollup.config.mjs with the node-resolve, typescript and terser plugins, and rollup-plugin-filesize is in the dependency list, so the bundle size is reported as part of the build. There is a badge for it on the npm package page.

The tests are run in a real browser rather than in a simulated DOM. The test script is web-test-runner test/**/*.test.js, with @web/test-runner at 0.20.0 and @open-wc/testing at 4.0.0 as the test helpers, plus sinon for stubbing. The pretest script runs the build first, so the tests always run against the freshly built bundle rather than against the source.

That is a sensible choice for this particular library, because the actions manipulate the DOM and the browser history and the Web Storage API, and there is a lot of behaviour in a jsdom environment that does not exist. A test runner that opens an actual browser can check that a focus action really focuses and that a storage action really writes to localStorage.

The development workflow is a playground. The start script does cd playground && yarn start, the watch script runs rollup in watch mode, and the dev script runs both at once with concurrently. There is a playground/ directory in the repository, so there is a place to try the actions against a running Turbo application while you work on them.

The linting setup is a .eslintrc with the typescript-eslint parser and plugin and an eslint-config-prettier, and format runs the lint script with --fix. The repository also has a .node-version file, which pins the Node version for the project, a .prettierrc.json, an .npmignore and a .eslintignore.

## Conclusion

Adopt TurboPower if you are building Rails or Django applications with Hotwire and keep reaching for a custom Turbo action that the framework does not ship, since most of the common ones are already implemented here with consistent naming. Do not adopt it if you only need morph, because Turbo Morph is a separate package you can add on its own, and note that the library registers itself against Turbo's global StreamActions, so it changes behaviour for every stream in the application. Before you ship, check the Turbo version your project runs, since the README asks for Turbo 7.2 or newer while the package peer dependency asks for 8.0 or newer.

## FAQ

### What does the TurboPower package do?

It is a power-pack for Turbo Streams that provides Turbo Streams with a large number of new actions, and additionally adds the morph action from Turbo Morph. It covers DOM, attribute, event, form, storage, browser, document, history, debug, notification and Turbo specific operations.

### How do I install and set up TurboPower?

Run yarn add turbo_power, then in application.js import TurboPower from turbo_power and call TurboPower.initialize(Turbo.StreamActions). The README notes this requires Turbo 7.2 or newer, while the package peer dependency asks for @hotwired/turbo 8.0.0 or newer.

### Which storage operations does TurboPower support?

A generic clear_storage, remove_storage_item and set_storage_item that take a type, plus specialised clear_local_storage, clear_session_storage, remove_local_storage_item, remove_session_storage_item, set_local_storage_item and set_session_storage_item actions for the two common scopes.

### What other projects provide TurboPower integrations?

For Rails there is the turbo_power-rails repository with the instructions, and for Django there is the django-turbo-helper repository with the documentation. The TurboPower package itself is framework-agnostic JavaScript that registers the browser actions.

### How are the TurboPower tests run?

With web-test-runner test/**/*.test.js, using @web/test-runner and @open-wc/testing with sinon for stubbing, so the tests run in a real browser against the freshly built bundle. The pretest script runs the build first.

## Sources

- [Issues](https://github.com/marcoroth/turbo_power/issues)
- [License: MIT](https://github.com/marcoroth/turbo_power/blob/main/LICENSE)
- [marcoroth/turbo_power on GitHub](https://github.com/marcoroth/turbo_power)
- [README](https://github.com/marcoroth/turbo_power/blob/main/README.md)
- [Releases](https://github.com/marcoroth/turbo_power/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/marcoroth-turbo-power
