# stimulus-use: mixins that add browser behaviours to a Stimulus controller

> stimulus-use is an MIT-licensed TypeScript collection of composable mixins for Stimulus controllers, wrapping observer APIs such as IntersectionObserver and ResizeObserver and exposing them as new lifecycle callbacks. The judgement is that the mixin mechanism is the real product, and that the mixins that add value are the ones that do something a Stimulus controller cannot express on its own.

**stimulus-use/stimulus-use** — A collection of composable behaviors for your Stimulus Controllers.

- Repository: https://github.com/stimulus-use/stimulus-use
- Website: https://stimulus-use.dev
- Stars: 1,681 · Forks: 71
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/stimulus-use-stimulus-use

## One call in connect, one new callback on the controller

stimulus-use is described as a collection of composable behaviors for Stimulus Controllers, and the shape of the whole library is visible in one example.

```js
import { Controller } from '@hotwired/stimulus'
import { useIntersection, useResize } from 'stimulus-use'

export default class extends Controller {
  connect() {
    useIntersection(this)
    useResize(this)
  }

  appear(entry) {
    // new callback whenever the element appears.
    // entry is an IntersectionObserverEntry
  }

  resize({ width, height }) {
    // callback whenever the element gets resized
  }
}
```

The mechanism is a mixin applied in `connect`. The mixin sets up an observer, and when the observer fires, it calls a method on the controller that you did not write, in this case `appear` and `resize`. The controller stays a plain Stimulus controller with extra lifecycle callbacks.

That is the whole idea, and it is a good one. Stimulus gives you a controller lifecycle and a data-attribute convention, and it deliberately does not give you anything to observe the browser with. Every project that needs to know when an element scrolls into view writes an IntersectionObserver, tears it down in `disconnect`, and remembers to cancel it. This library writes that once.

The stated properties are new lifecycle behaviors, composability with mixins in a single controller, modularity through ES6 modules so tree shaking removes what you do not import, TypeScript types for autocompletion, and small size, around 7k gzipped with tree shaking.

The author is listed as Adrien Poly with Marco Roth as a contributor, and the package metadata records `"sideEffects": false`, which is the package.json field that tells a bundler it can drop unused imports safely.

## The observer mixins are where the value is

The mixins are grouped into four categories, and the largest group is the observers, which are built around the browser Observer APIs and custom events.

`useClickOutside` tracks clicks outside the element and adds a `clickOutside` callback. `useHover` tracks mouse movements over an element and adds `mouseEnter` and `mouseLeave`. `useIdle` tracks whether the user is idle on the page and adds `away` and `back`. `useIntersection` tracks the element's intersection and adds `appear` and `disappear`, which is the one in the opening example and the one most Stimulus projects end up writing by hand.

`useLazyLoad` lazily loads an image once it scrolls into the viewport and adds `loading` and `loaded`. `useMatchMedia` tracks whether the window matches a media query string and adds `is[Name]`, `not[Name]` and `[name]Changed`, which is a media-query-scoped naming scheme rather than a fixed callback name. `useMutation` tracks mutations on an element, its attributes and or its subtree and adds a `mutate` callback. `useResize` tracks the element's size and adds a `resize` callback. `useVisibility` tracks page visibility and adds `visible` and `invisible`. `useWindowFocus` tracks window focus and adds `focus` and `unfocus`. `useWindowResize` tracks the size of the `window` object and adds a `windowResize` callback.

`useTargetMutation` is the most interesting of the set, because it solves a problem specific to Stimulus. It tracks when targets are added or removed from the controller's scope, or their contents changed, and adds `[target]TargetAdded`, `[target]TargetRemoved` and `[target]TargetChanged` callbacks for each specified target. In a Stimulus application where a list is rendered dynamically, this is the difference between a controller that updates correctly and one that silently drifts.

There is one outlier in the observers group. `useHotkeys` registers hotkeys using the hotkeys-js library and binds them to handler methods, and it adds no new lifecycle callback, because it does not need one.

The rest of the categories are smaller. The optimization group has `useDebounce` and `useThrottle`, which add an array of functions to debounce or throttle, and `useMemo`, which memoizes expensive getters through a static `memos` array. The animation group has `useTransition`, a mixin or controller that applies classes to the various stages of an element's transition. The application group has `useApplication` with an `ApplicationController`, described as a controller with extra behaviour for your application, `useDispatch` for emitting custom events to communicate between controllers, and `useMeta` for getting at `<head>` meta values.

## Two ways to use it, and one of them is preferred

The README describes two ways of using the library, and it names a preference.

Composing with mixins is the preferred approach because it brings the most flexibility. You import a mixin and apply it in `connect` or `initialize` to add a behaviour to your controller, and you can combine several mixins in the same controller. The second way is extending built-in controllers, which the documentation does not rank as favourably.

The distinction matters for a structural reason that the mixin approach makes obvious. A mixin does not change your class hierarchy, it adds methods and sets up an observer when you call it. That means the mixin can be applied conditionally, at the point where the controller decides it needs the behaviour, and it means the teardown is handled by the mixin rather than by you remembering to write a `disconnect`.

The `initialize` option in the compositing section is worth noting. Stimulus has both an `initialize` and a `connect` hook, and applying a mixin in `initialize` rather than `connect` changes when the observer starts.

The `useMemo` mixin has an unusual shape compared with the rest. It works through a static `memos` array rather than through the controller instance, and the same pattern applies to `useDebounce` and `useThrottle`, which take an array of function names. Those three mixins are configured at the class level, so the array is part of the class definition rather than something you pass at connect time.

The `useApplication` and `ApplicationController` entry is different again, since it is a controller rather than a mixin, and it is meant to be extended rather than applied.

## The version line depends on which Stimulus you are on

This is the practical decision to make first, and the README is unusually clear about it.

For Stimulus 3, you use the version `0.50.0+`, and that and all future versions are designed to work with the `@hotwired/stimulus` npm package. The install is:

```bash
npm i stimulus-use @hotwired/stimulus
```

or with yarn:

```bash
yarn add stimulus-use @hotwired/stimulus
```

For Stimulus 2 and below, you use version `0.41.0`, designed to work with the old `stimulus` npm package:

```bash
npm i stimulus-use@0.41.0 stimulus@2.0.0
```

There is also a compatibility note that is worth internalising if you are mid-migration. If other packages still depend on the `stimulus` npm package, you can safely keep that in your `package.json` and it will not break the `stimulus-use` compatibility. So a project can hold both package names in the dependency tree during a transition.

The release history shows the scale of that transition. Version 0.52.3 was published on 2024-12-15, then 0.53.0 on 2026-06-30 and 0.53.1 on 2026-09-04. The last push was on 2026-09-04, the same day as the 0.53.1 release, and the repository is not archived.

The 0.53 line is a major bump from 0.52, so the 0.50 series is where the Stimulus 3 support landed and 0.53 is a separate step forward on top of it. The `CHANGELOG.md` in the repository is where to look for what changed.

## How the package is built and what it costs you

The packaging is modern and, unusually for a library of this age, the build has moved off the older toolchain.

The `build` script first runs `tsc --noEmit false --declaration true --emitDeclarationOnly true --outDir dist/types` to emit the type declarations, and then runs `rolldown -c`. Rolldown is the Rust-based bundler that is taking over from Rollup, and the repository has a `rolldown.config.mjs` for it. The `dev` script is `rolldown -wc`, so watch mode runs through the same bundler.

Testing is Vitest, with `test` running `vitest run` and `test:watch` the interactive variant, and there is a separate `test:treeshake` script that runs `npx agadoo`. Agadoo is the tool for checking that a package's exports are actually tree-shakeable, so the small-size claim is verified in continuous integration rather than asserted. That is a good sign, because the whole value of an ES module mixin library depends on the tree shaking actually working.

The documentation is built with VitePress, with `docs:dev` and `docs:build` scripts, and the repository has a `playground/` directory for local experimentation, wired into the `start` and `prestart` scripts.

The `exports` map in `package.json` has two entries, the root and a `./hotkeys` subpath, which is the `useHotkeys` mixin separated out so that the hotkeys-js dependency is only pulled in when you use it. Each entry exposes `main`, `browser`, `import`, `module`, `umd` and `types` conditions, and the package also declares an `unpkg` field pointing at the UMD build. The root `main` and `module` both point at `./dist/index.js`.

Linting is Prettier rather than ESLint, with `lint` running `prettier --check` over `src`, `spec` and `playground`, and `format` running the write variant. There is a `.node-version` file, a `vitest.config.mts`, a `tsconfig.json`, a `.prettierrc.json` and a `.npmignore`.

## Where this is the wrong tool

There are three boundaries, and the first is the one that catches people out.

The version split is the sharp edge. A project on Stimulus 2 must pin `stimulus-use@0.41.0`, and that is a different code line with a different feature set from the 0.53 line. The library's own naming and the fact that the 0.41 line predates several mixins in the current documentation means the mixin list in the README is not necessarily the mixin list you get on the older line. If you are on Stimulus 2, check the documentation version that matches your line rather than the current site.

The second is that mixins add behaviour, they do not replace it. Some of the observers are a few lines of code, and a project with a strict house style might prefer to own that code. `useResize` is a ResizeObserver with a callback. `useVisibility` is a document visibility listener. The mixins earn their place when the pattern recurs across controllers, and they are overhead when it appears once. The mixin that tracks when targets are added or removed from a controller's scope is a different proposition, because that is Stimulus-specific logic that most projects would get wrong.

The third is lifecycle control. A mixin sets up and tears down its own observer, which is the point, but it means the observer's configuration is not something you can adjust per instance. If you need a root margin different from the default, or a threshold tuned for your layout, you are reading the mixin's documentation and then possibly writing the observer yourself.

One thing the documentation does not cover, and it is worth knowing before you rely on it, is how the mixins behave across a Turbo page visit. The topic tags include `turbo` and `hotwire`, and Stimulus controllers do reconnect on a Turbo navigation, so the mixins that set up observers in `connect` should be fine. But the mixins applied in `initialize` rather than `connect` are a different case, and the README does not discuss it.

## Conclusion

stimulus-use fits a Rails team using Stimulus and Hotwire who keeps rewriting the same IntersectionObserver and ResizeObserver boilerplate in every controller. It does not fit a team on Stimulus 2, unless you deliberately pin the older `0.41.0` line, and it does not fit a project that wants full control over the observer lifecycle rather than a preset. Verify first that your Stimulus major version matches the line you install, since 0.50.0 and later target `@hotwired/stimulus` while `0.41.0` targets the old `stimulus` package, that the tree shaking claim holds in your bundler, which the project checks with the `test:treeshake` script, and that the mixin you want is one of the observer-based ones rather than something you would write yourself in ten lines.

## FAQ

### How do I add a behaviour to a Stimulus controller with stimulus-use?

Import the mixin and apply it to the controller inside `connect` or `initialize`, for example `useIntersection(this)`. The mixin sets up the observer and calls the matching new callback on the controller, such as `appear` and `disappear` for `useIntersection`.

### Which version of stimulus-use should I install?

For Stimulus 3, use version `0.50.0` or later, which is designed for the `@hotwired/stimulus` package, with `npm i stimulus-use @hotwired/stimulus`. For Stimulus 2 or below, use version `0.41.0` with `npm i stimulus-use@0.41.0 stimulus@2.0.0`.

### What mixins does stimulus-use provide for observers?

Thirteen, including `useClickOutside`, `useHover`, `useIdle`, `useIntersection`, `useLazyLoad`, `useMatchMedia`, `useMutation`, `useResize`, `useTargetMutation`, `useVisibility`, `useWindowFocus` and `useWindowResize`, plus `useHotkeys` which registers hotkeys through the hotkeys-js library.

### What is the useTargetMutation mixin for in stimulus-use?

It tracks when targets are added or removed from the controller's scope, or when their contents change, and adds `[target]TargetAdded`, `[target]TargetRemoved` and `[target]TargetChanged` callbacks for each specified target. It is aimed at controllers whose target list is rendered dynamically.

### Is stimulus-use tree-shakeable?

The package metadata sets `"sideEffects": false` and the modules are ES6, so unused mixins can be dropped by the bundler. The project checks this in continuous integration with a `test:treeshake` script that runs `npx agadoo`, and the stated bundle size is around 7k gzipped with tree shaking.

## Sources

- [License: MIT](https://github.com/stimulus-use/stimulus-use/blob/main/LICENSE)
- [Project website](https://stimulus-use.dev)
- [README](https://github.com/stimulus-use/stimulus-use/blob/main/README.md)
- [Releases](https://github.com/stimulus-use/stimulus-use/releases)
- [stimulus-use/stimulus-use on GitHub](https://github.com/stimulus-use/stimulus-use)

---

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