State.js looks simple — just HTML attributes — but it introduces a new mental model for building UI:
HTML holds the data.
CSS reacts to the data.
State.js keeps them in sync.
If you’re used to React, Vue, or Svelte, this feels strange at first.
If you’re used to CSS, this feels like “why didn’t CSS always work like this?”
This tutorial will teach you the core ideas behind State.js so you can build reactive UI without JavaScript logic, without a framework, and without a build step.
⭐ 1. What State.js Actually Does
State.js turns HTML attributes into live CSS variables.
Example:
<div data-state data-count="0"></div>
State.js automatically exposes:
--state-count: 0;
If data-count changes, the CSS variable updates instantly.
This is the foundation of everything.
Learn more: Reactive attributes
⭐ 2. Your First Reactive Element
Let’s make a simple counter.
HTML
<div id="counter"
data-state
data-count="0"
data-state-text="Count: {count}">
</div>
What’s happening?
-
data-state→ activates State.js -
data-count="0"→ creates--state-count: 0 -
data-state-text="Count: {count}"→ binds text to the value
State.js replaces {count} with the live value.
⭐ 3. Updating State with Triggers
Add a button that increments the counter:
<button
data-state-trigger
data-state-target="#counter"
data-state-attr="count"
data-state-increment="1">
+1
</button>
What this means:
-
data-state-trigger→ this element performs an action -
data-state-target="#counter"→ update that element -
data-state-attr="count"→ update thedata-countattribute -
data-state-increment="1"→ add 1
Clicking the button updates:
- the attribute
- the CSS variable
- the text
- all automatically
Learn more: Triggers
⭐ 4. Styling with CSS Variables
Because State.js exposes attributes as CSS variables, you can style reactively:
#counter {
color: hsl(calc(var(--state-count) * 20), 80%, 50%);
}
As the count increases, the color changes.
No JS.
No re-renders.
Just CSS reacting to state.
Learn more: CSS variable projection
⭐ 5. Conditions (Reactive Logic in HTML)
Let’s change the background when the count reaches 5.
<div id="counter"
data-state
data-count="0"
data-state-class="big: count >= 5"
data-state-text="Count: {count}">
</div>
This adds the class .big when the condition is true.
CSS:
.big {
background: gold;
}
Learn more: Conditions
⭐ 6. Autofire (State.js “Game Loop”)
State.js can update values on an interval:
<div data-state
data-time="0"
data-state-autofire="time += 1 every 100ms"
data-state-text="Time: {time}">
</div>
This creates a reactive timer with no JavaScript.
Learn more: Intervals
⭐ 7. Two-Way Binding (Forms Without JS)
<input type="range"
data-state
data-value="50"
data-state-bind="value">
<div data-state
data-state-text="Value: {value}">
</div>
Moving the slider updates the text automatically.
Learn more: Binding
⭐ 8. Putting It All Together — A Mini App
<div id="app" data-state data-count="0">
<h1 data-state-text="Count: {count}"></h1>
<button
data-state-trigger
data-state-target="#app"
data-state-attr="count"
data-state-increment="1">
+1
</button>
<button
data-state-trigger
data-state-target="#app"
data-state-attr="count"
data-state-increment="-1">
-1
</button>
<div data-state-class="warning: count < 0">
<p>Count is negative!</p>
</div>
</div>
What you get:
- reactive text
- reactive classes
- reactive styling
- reactive state
- no JavaScript logic
This is the State.js mental model.
⭐ 9. Why This Matters
State.js gives you:
✔ Reactivity without JavaScript
No functions.
No hooks.
No re-renders.
✔ Components without frameworks
Just templates + includes.
✔ State without state management libraries
Attributes are the state.
✔ UI logic without JS
CSS handles behavior.
✔ Zero build step
Works in plain HTML.
This is why people say:
“It feels like something CSS should have done.”
Because State.js makes the browser behave the way developers wish it behaved.
Top comments (0)