Introduction
Targets provide named references to DOM elements within a controller's scope. Values store typed data on the controller element via data attributes. Together, they eliminate the need for manual DOM queries and data parsing.
Key Concepts
- Targets: Named DOM element references declared with
static targetsand accessed viathis.nameTarget. - Values: Typed data attributes declared with
static valuesand accessed viathis.nameValue. - Target Callbacks: Methods called when targets connect or disconnect (e.g.,
nameTargetConnected). - Value Changed Callbacks: Methods called when a value attribute changes (e.g.,
nameValueChanged).
Real World Context
Instead of littering your controller with document.querySelector calls and JSON.parse on data attributes, targets and values provide a clean, declarative API. They make controllers self-documenting — you can see at a glance what DOM elements and configuration a controller needs.
Deep Dive
Declaring and Using Targets
javascriptimport { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["input", "results", "count"] search() { const query = this.inputTarget.value // this.inputTarget — single element (throws if missing) // this.inputTargets — array of all matching elements // this.hasInputTarget — boolean check this.resultsTarget.innerHTML = `Searching: ${query}` this.countTarget.textContent = this.resultsTargets.length } }
In HTML, targets are marked with data-[controller]-target:
html<div data-controller="search"> <input data-search-target="input" data-action="input->search#search"> <span data-search-target="count">0</span> <div data-search-target="results"></div> </div>
Each target name generates three properties: nameTarget (first match), nameTargets (all matches), and hasNameTarget (boolean).
Declaring and Using Values
javascriptimport { Controller } from "@hotwired/stimulus" export default class extends Controller { static values = { url: String, refreshInterval: { type: Number, default: 5000 }, autoStart: { type: Boolean, default: false } } connect() { if (this.autoStartValue) { this.startRefreshing() } } startRefreshing() { setInterval(() => { fetch(this.urlValue) .then(r => r.text()) .then(html => this.element.innerHTML = html) }, this.refreshIntervalValue) } }
Values are passed via data attributes in HTML:
html<div data-controller="dashboard" data-dashboard-url-value="/api/stats" data-dashboard-refresh-interval-value="10000" data-dashboard-auto-start-value="true"> </div>
Supported types: String, Number, Boolean, Array, Object. Stimulus handles parsing automatically.
Value Changed Callbacks
javascriptstatic values = { count: { type: Number, default: 0 } } countValueChanged(newValue, oldValue) { // Called whenever data-controller-count-value changes this.element.querySelector(".badge").textContent = newValue }
These callbacks fire whenever the data attribute is updated, even from external code. They enable reactive patterns without a framework.
Common Pitfalls
- Accessing
nameTargetwhen it doesn't exist — Throws an error. UsehasNameTargetto check first, or usenameTargets(empty array if none). - Wrong data attribute format — Values use
data-[controller]-[name]-value. The kebab-case attribute maps to camelCase in JavaScript.
Best Practices
- Use values for configuration, targets for DOM — Values hold data (URLs, intervals, flags). Targets reference elements.
- Prefer value changed callbacks over observers — They are cleaner than MutationObserver for reacting to data changes.
Summary
- Targets provide named DOM references via
static targetsanddata-*-targetattributes. - Values store typed configuration via
static valuesanddata-*-valueattributes. - Three accessor patterns:
nameTarget(single),nameTargets(array),hasNameTarget(boolean). - Value changed callbacks fire automatically when data attributes change.
- Supported value types: String, Number, Boolean, Array, Object.
Code Examples
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["output"]
static values = {
greeting: { type: String, default: "Hello" },
count: { type: Number, default: 0 }
}
greet() {
this.countValue++ // Triggers countValueChanged
this.outputTarget.textContent =
`${this.greetingValue}! (clicked ${this.countValue} times)`
}
countValueChanged() {
// Automatically called when countValue changes
console.log(`Count is now: ${this.countValue}`)
}
}