Skip to content

Stimulus Values — Complete Guide with Examples

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you'll learn about Stimulus Values. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Stimulus values provide typed configuration from HTML attributes to controllers, enabling data-driven components without touching JavaScript for different configurations.

What You'll Learn

  • Declaring values with static values = {}
  • Supported value types: String, Number, Boolean, Array, Object
  • HTML attribute syntax for each type
  • Value change callbacks: {name}ValueChanged(current, previous)
  • Default values and type coercion
  • Practical patterns for reusable, configurable components

Why It Matters

Values make controllers reusable. The same controller can behave differently based on HTML attributes, eliminating the need for separate controller classes for each variation. In the Doda Browser extension, values configure panel widths, refresh intervals, API endpoints, and UI preferences without modifying JavaScript code.

Learning Path

flowchart LR
  A[Actions] --> B[Values]
  B --> C[Classes]
  C --> D[Outlets]
  B --> E[Lifecycle &
State] style B fill:#4f46e5,color:#fff,stroke:#4f46e5,stroke-width:2px style E fill:#059669,color:#fff

Declaring Values

Values are declared in a static object with type and optional default:

app.register('timer', class extends Controller {
  static values = {
    interval: { type: Number, default: 1000 },
    autostart: { type: Boolean, default: false },
    label: { type: String, default: 'Timer' }
  };

  static targets = ['display'];

  connect() {
    this.displayTarget.textContent = `${this.labelValue}: 0`;
    if (this.autostartValue) {
      this.start();
    }
  }

  start() {
    let count = 0;
    this._interval = setInterval(() => {
      this.displayTarget.textContent = `${this.labelValue}: ${++count}`;
    }, this.intervalValue);
  }

  disconnect() {
    clearInterval(this._interval);
  }
});
<div data-controller="timer"
     data-timer-interval-value="500"
     data-timer-autostart-value="true"
     data-timer-label-value="Countdown">
  <p data-timer-target="display"></p>
  <button data-action="click->timer#start">Start</button>
</div>

Teacher explains: The HTML attribute name follows the pattern data-{controller}-{name}-value. The controller accesses it as this.{name}Value (camelCase). The default is used when the HTML attribute is absent.

Value Types

Stimulus supports five value types. Each type has a specific HTML format.

String

static values = { name: { type: String, default: 'World' } };
<div data-controller="hello" data-hello-name-value="Alice">

Access: this.nameValue returns 'Alice'

Number

static values = { count: { type: Number, default: 0 } };
<div data-controller="counter" data-counter-count-value="42">

Access: this.countValue returns 42 (number, not string)

Boolean

static values = { active: { type: Boolean, default: false } };
<div data-controller="toggle" data-toggle-active-value="true">

Access: this.activeValue returns true (boolean) Values: "true" and "false" strings are parsed; any value means true

Array

static values = { items: { type: Array, default: [] } };
<div data-controller="list"
     data-list-items-value='["apple","banana","cherry"]'>

Access: this.itemsValue returns ['apple', 'banana', 'cherry'] Must be valid JSON array string.

Object

static values = { config: { type: Object, default: {} } };
<div data-controller="dashboard"
     data-dashboard-config-value='{"theme":"dark","columns":3}'>

Access: this.configValue returns { theme: 'dark', columns: 3 } Must be valid JSON object string.

Shorthand Syntax

For values with only a default (and type inferred from the default):

static values = {
  interval: Number,    // same as { type: Number }
  autostart: Boolean,  // same as { type: Boolean }
  name: String,        // same as { type: String }
  items: Array,        // same as { type: Array, default: [] }
  config: Object       // same as { type: Object, default: {} }
};

Value Change Callbacks

When a value changes (either programmatically or via HTML mutation), Stimulus calls a {name}ValueChanged callback.

app.register('progress', class extends Controller {
  static values = { percent: { type: Number, default: 0 } };
  static targets = ['bar', 'label'];

  percentValueChanged(current, previous) {
    console.log(`Progress: ${previous}% -> ${current}%`);
    this.barTarget.style.width = `${current}%`;
    this.labelTarget.textContent = `${current}%`;

    if (current >= 100) {
      this.barTarget.classList.add('complete');
      this.dispatch('complete');
    }
  }
});
<div data-controller="progress" data-progress-percent-value="0">
  <div data-progress-target="bar" class="progress-bar"></div>
  <span data-progress-target="label">0%</span>
</div>

Teacher explains: The callback receives current (new value) and previous (old value). This is useful for reacting to value changes, updating the DOM, or dispatching events when a threshold is reached.

Programmatic Value Changes

app.register('progress', class extends Controller {
  static values = { percent: Number };
  static targets = ['bar'];

  increment() {
    this.percentValue = Math.min(this.percentValue + 10, 100);
    // Setting percentValue triggers percentValueChanged
  }

  reset() {
    this.percentValue = 0;
  }
});

Real-world: Configurable Polling Controller

<div data-controller="polling"
     data-polling-url-value="/api/status"
     data-polling-interval-value="5000"
     data-polling-method-value="GET"
     data-polling-retry-value="3">
  <div data-polling-target="status">Checking...</div>
  <div data-polling-target="error" class="hidden">Connection error</div>
</div>
app.register('polling', class extends Controller {
  static values = {
    url: String,
    interval: { type: Number, default: 3000 },
    method: { type: String, default: 'GET' },
    retry: { type: Number, default: 3 }
  };

  static targets = ['status', 'error'];

  #pollTimer = null;
  #retryCount = 0;

  connect() {
    this.poll();
  }

  disconnect() {
    this.stopPolling();
  }

  async poll() {
    await this.fetchStatus();
    this.#pollTimer = setTimeout(() => this.poll(), this.intervalValue);
  }

  stopPolling() {
    if (this.#pollTimer) {
      clearTimeout(this.#pollTimer);
      this.#pollTimer = null;
    }
  }

  async fetchStatus() {
    try {
      const response = await fetch(this.urlValue, { method: this.methodValue });
      const data = await response.json();
      this.statusTarget.textContent = `Status: ${data.status}`;
      this.errorTarget.classList.add('hidden');
      this.#retryCount = 0;
    } catch (error) {
      this.#retryCount++;
      if (this.#retryCount >= this.retryValue) {
        this.errorTarget.textContent = 'Failed after retries';
        this.errorTarget.classList.remove('hidden');
        this.stopPolling();
      }
    }
  }

  intervalValueChanged() {
    // Restart polling with new interval if value changes
    if (this.#pollTimer) {
      this.stopPolling();
      this.poll();
    }
  }
});

Reactive UI with Values

app.register('slider', class extends Controller {
  static values = { min: Number, max: Number, step: Number, current: Number };
  static targets = ['input', 'display', 'track', 'thumb'];

  connect() {
    this.inputTarget.min = this.minValue;
    this.inputTarget.max = this.maxValue;
    this.inputTarget.step = this.stepValue;
  }

  currentValueChanged(current) {
    this.displayTarget.textContent = current;
    this.thumbTarget.style.left = `${this.#percent(current)}%`;
  }

  update(event) {
    this.currentValue = parseFloat(event.target.value);
  }

  #percent(value) {
    return ((value - this.minValue) / (this.maxValue - this.minValue)) * 100;
  }
});
<div data-controller="slider"
     data-slider-min-value="0"
     data-slider-max-value="100"
     data-slider-step-value="1"
     data-slider-current-value="50">
  <input type="range" data-slider-target="input"
         data-action="input->slider#update">
  <span data-slider-target="display">50</span>
  <div data-slider-target="track">
    <div data-slider-target="thumb"></div>
  </div>
</div>

Common Mistakes

1. Using Wrong HTML Attribute Format

<!-- ❌ Wrong: missing "value" suffix -->
<div data-controller="timer" data-timer-interval="1000">

<!-- ✅ Correct: includes "-value" suffix -->
<div data-controller="timer" data-timer-interval-value="1000">

2. Providing Invalid JSON for Array/Object Values

<!-- ❌ Single quotes are invalid JSON -->
<div data-controller="list" data-list-items-value="['a', 'b']">

<!-- ✅ Double quotes for JSON -->
<div data-controller="list" data-list-items-value='["a","b"]'>

3. Forgetting That Arrays and Objects Are Parsed via JSON

// HTML: data-items-value='["1","2","3"]'
this.itemsValue // ['1', '2', '3'] -- strings!
// If you need numbers, pass JSON: '[1, 2, 3]'

4. Not Providing Default for Optional Values

// ❌ No default -- accessing this.delayValue before it's set errors
static values = { delay: Number };

// ✅ Provide default
static values = { delay: { type: Number, default: 300 } };

5. Overwriting Values Without Triggering Callbacks

// Direct mutation of an object value doesn't trigger change callbacks
this.configValue.key = 'newValue'; // No callback!
// Set the whole value to trigger the callback
this.configValue = { ...this.configValue, key: 'newValue' };

Practice Questions

1. What types does Stimulus support for values?

String, Number, Boolean, Array, and Object.

2. How do you set a default value for a Number value?

static values = { count: { type: Number, default: 0 } }.

3. What callback is triggered when a value changes?

{name}ValueChanged(current, previous). For a value named interval, the callback is intervalValueChanged.

4. How do you pass an array value from HTML?

Use a JSON array string: data-controller-items-value='["a","b","c"]'. The array value is parsed via JSON.parse.

Challenge

Build a pagination controller with values for page, totalPages, and perPage. It should render page numbers, handle next/previous buttons, and dispatch a page-change event with the new page number.

FAQ

### Can I change a value from outside the controller?

Yes. You can mutate the data-*-value attribute on the DOM element directly, and Stimulus will detect the change and trigger the value change callback.

What happens if I omit a value that has no default?

Accessing this.{name}Value before the value is set in HTML returns undefined. The value-only callbacks are not triggered on connect.

Can values be used with actions?

Yes. Values are commonly used alongside actions. For example, an action method can read or update values to change controller behavior.

Are value change callbacks triggered on connect?

No. The initial value is available in connect() but the change callback only fires when the value changes after initialization.

What's Next

Topic Description
{{< ref "stimulus-classes" >}} Dynamic CSS class mapping through HTML configuration
{{< ref "stimulus-outlets" >}} Cross-controller references with outlet targets
{{< ref "stimulus-loading" >}} Lazy Loading controllers and integrating with Turbo
HTML Data Attributes Review data attribute conventions and usage

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. This values tutorial powers the configurable panels and settings UI in the Doda Browser extension.

What's Next

Congratulations on completing this Stimulus Values tutorial! Here's where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro