Stimulus — небольшой JavaScript-фреймворк от Basecamp, который строится вокруг уже существующего HTML. Он не создаёт виртуальный DOM и не требует описывать интерфейс в компонентах: вы добавляете к разметке атрибуты data-controller, data-action и data-*-target, а контроллер оживляет поведение. В 2026 году подход остаётся востребованным в проектах на Rails и Hotwire, но Stimulus спокойно работает и со статическим сайтом, и с любым серверным рендерингом.
Где уместен Stimulus
Stimulus решает локальные задачи: раскрыть меню, посчитать символы, отправить форму по изменению, показать модальное окно, синхронизировать два поля. Он не пытается управлять всем состоянием приложения и не заменяет React или Vue там, где нужен сложный клиентский роутинг.
- HTML остаётся источником правды, а сервер — основным рендерером.
- Контроллеры подключаются к участку страницы и не мешают друг другу.
- Нет сборки обязательной: можно начать с import map или бандлера.
- Хорошо сочетается с Turbo: после навигации контроллеры переподключаются автоматически.
Установка
Для проекта с npm достаточно одного пакета. Дальше выберите способ сборки: Vite, esbuild, webpack или обычный import map.
npm install @hotwired/stimulus
npm install --save-dev vite
В Rails установка автоматизирована, но требует, чтобы приложение уже использовало importmap или jsbundling:
bin/rails stimulus:install
Вручную создайте точку входа. Важно зарегистрировать контроллеры до старта приложения или сразу после — порядок влияет только на то, когда контроллеры начнут получать события.
import { Application } from "@hotwired/stimulus"
import HelloController from "./hello_controller"
const app = Application.start()
app.register("hello", HelloController)
Первый контроллер
Разметка связывает элемент с контроллером по имени. В контроллере используется класс с методами жизненного цикла. Ниже — минимальный пример приветствия.
<div data-controller="hello">
<input data-hello-target="name"
data-action="input->hello#greet">
<p data-hello-target="output"></p>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["name", "output"]
greet() {
this.outputTarget.textContent =
`Привет, ${this.nameTarget.value}`
}
}
Имя в register должно совпадать с атрибутом data-controller; имя файла обычно делают таким же. Если контроллер не срабатывает, первым делом проверьте консоль: Stimulus пишет предупреждение о незарегистрированном имени.
Targets
Targets дают типизированный доступ к элементам внутри контроллера. Для каждого объявленного имени создаются свойства nameTarget, nameTargets и hasNameTarget.
static targets = ["input", "output", "error"]
update() {
if (!this.hasErrorTarget) return
this.errorTarget.hidden = this.inputTarget.value.length > 0
}
Не злоупотребляйте querySelector: targets переживают перерисовку фрагмента и лучше читаются. Если элемент появляется динамически, используйте колбэки inputTargetConnected и inputTargetDisconnected — они вызываются при подключении и отключении цели.
Values
Values передают данные из HTML в контроллер и приводят их к типу. Поддерживаются String, Number, Boolean, Array и Object.
static values = {
limit: Number,
url: String,
open: Boolean
}
connect() {
this.element.dataset.state =
this.openValue ? "open" : "closed"
}
В разметке значение задаётся как data-counter-limit-value="280". При изменении атрибута вызывается limitValueChanged; это удобно для реакции на внешние обновления, но не стоит использовать как замену полноценному хранилищу состояния.
Actions и события
Action связывает событие с методом. Формат — событие->контроллер#метод. Модификаторы :prevent, :stop, :self и :once управляют поведением.
<form data-controller="search"
data-action="submit->search#run">
<div data-controller="menu">
<button data-action="click->menu#toggle
keydown.esc@window->menu#close">
Меню
</button>
</div>
</form>
@window вешает обработчик на окно, что полезно для закрытия по Escape или глобального клика. Клавиатурные фильтры вроде .enter и .ctrl избавляют от ручных проверок event.key.
Для общения между контроллерами используйте this.dispatch("updated", { detail: { id } }). Событие всплывает от элемента контроллера, поэтому его можно слушать обычным addEventListener или действием другого контроллера.
Outlets
Outlets — это ссылки на другие контроллеры, а не на произвольные элементы. Они нужны, когда один компонент должен вызвать метод другого.
static outlets = ["result"]
search() {
this.resultOutlets.forEach((outlet) =>
outlet.show(this.element))
}
В HTML укажите data-search-result-outlet=".result". Селектор ищет элементы в документе, поэтому не делайте его слишком широким. Для необязательной связи проверяйте hasResultOutlet.
Практика: счётчик символов
Соберём поле с ограничением и индикатором. Контроллер обновляет счётчик при вводе и меняет класс, когда лимит превышен.
<form data-controller="counter"
data-counter-limit-value="280">
<textarea data-counter-target="input"
data-action="input->counter#update"></textarea>
<span data-counter-target="output">0</span>
</form>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "output"]
static values = { limit: Number }
connect() { this.update() }
update() {
const size = this.inputTarget.value.length
this.outputTarget.textContent =
`${size} / ${this.limitValue}`
this.outputTarget.classList.toggle(
"over", size > this.limitValue)
}
}
Обратите внимание на connect(): он выставляет начальное значение при загрузке страницы и при возврате через Turbo. Если форма приходит из кэша браузера, значение в поле может отличаться от текста в счётчике — тогда обновляйте индикатор по событию pageshow.
Отладка и ограничения
Включите отладочный режим у приложения: в консоли появятся сообщения о подключении контроллеров, срабатывании actions и изменении values.
const app = Application.start()
app.debug = true
Stimulus не выполняет код из строк и не использует eval, поэтому дружит со строгой CSP. Но он не даёт реактивности уровня компонентов: если состояние сложное, придётся вручную синхронизировать DOM и данные. Для тестов используйте Playwright или jsdom: официального набора утилит нет, зато контроллер можно импортировать как обычный модуль.
Не забывайте про доступность: переключатель меню должен менять aria-expanded, модальное окно — удерживать фокус, а скрытые элементы лучше прятать атрибутом hidden, а не только классом.
Итог
Stimulus — практичный выбор, когда HTML уже отрисован сервером, а JavaScript нужен точечно. Он даёт понятную связь разметки и кода через controllers, targets, values и outlets, не навязывая архитектуру приложения. Начните с одного контроллера на странице, проверьте его в отладочном режиме и постепенно выносите повторяющиеся поведения. Если же интерфейс требует сложного клиентского состояния и роутинга, честнее взять полноценный фреймворк, чем бороться с ограничениями лёгкого инструмента.