State.js is a lightweight CSS frontend framework that exposes DOM element states as CSS variables for data-driven animations and reactive UIs. Build dynamic, interactive interfaces using pure CSS and HTML.
State.js is a super simple, efficient and lightweight CSS framework that exposes DOM element states as CSS variables. Track data attributes, form inputs, media playback, and element visibility - all automatically exposed for use in your CSS animations and transitions.
A CSS-first approach to reactive interfaces.
Using nothing but CSS, HTML and State.js, you can create:
- 📊 Dynamic dashboards and data visualizations
- 🎯 Interactive web applications with writing only CSS
- 🎨 Data-driven animations in CSS
- 🎮 Complex UIs (including game interfaces, health bars, score systems)
State.js is really lightweight and created with vanilla JavaScript without requiring any dependencies. Perfect for CSS-first development and reactive UI patterns!
npm i @idevgames/state-js<script src="https://cdn.jsdelivr.net/npm/@idevgames/state-js/src/state.js"></script>Download state.js and include it in your project:
<script src="/js/state.js"></script>State.js automatically tracks when elements become visible:
<div class="fadeIn" data-state></div>.fadeIn {
opacity: 0;
}
.fadeIn.state {
animation: fadeIn 1s forwards ease-in-out;
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}Watch data attributes and expose them as CSS variables. Here's an example using a health bar (perfect for games, but works for any progress indicator):
<div id="player"
data-state
data-state-watch="health,score"
data-state-var="true"
data-health="100"
data-health-min="0"
data-health-max="100"
data-score="0">
<div class="health-bar"></div>
</div>#player .health-bar {
width: var(--state-health-percent);
background: linear-gradient(90deg, red 0%, yellow 50%, green 100%);
}
/* Automatically triggered animations */
[data-health="0"] {
animation: death 2s forwards;
}
[data-health="10"],
[data-health="20"],
[data-health="30"] {
animation: pulse-red 1s infinite;
}Update the state by simply changing the data attribute:
// Change health (State.js watches and updates CSS vars automatically)
document.getElementById('player').setAttribute('data-health', '75');No JavaScript needed! Automatically bind form inputs to update other elements:
<!-- Input automatically updates the healthBar element -->
<input type="range"
id="healthSlider"
data-state
data-state-bind="healthBar"
data-state-attr="health"
min="0"
max="100"
value="75">
<!-- This element auto-updates when slider changes -->
<div id="healthBar"
data-state
data-state-watch="health"
data-health="75">
<div class="bar" style="width: var(--state-health-percent)"></div>
<span data-state-display="health">75</span>
</div>Bind to multiple elements (comma-separated):
<input data-state-bind="player,enemyHealthBar,scoreDisplay" data-state-attr="health">Make any element clickable to control state:
<!-- Player with power-up state -->
<div id="player"
data-state
data-state-toggles="powered"
data-powered="false">
Player Character
</div>
<!-- Button that toggles the power-up on/off -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-toggle="powered">
Toggle Power-Up
</button>
<!-- Button that sets health to a specific value -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="health"
data-state-value="100">
Full Health
</button>
<!-- Button that increments score by 10 (perfect for clickers!) -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="score"
data-state-increment="10">
Add 10 Points
</button>Trigger Modes:
- Toggle:
data-state-toggle="attribute"- Flips between true/false - Set:
data-state-attr="attribute"+data-state-value="value"- Sets specific value - Increment:
data-state-attr="attribute"+data-state-increment="amount"- Adds to current value - Decrement:
data-state-attr="attribute"+data-state-decrement="amount"- Subtracts from current value
Advanced: Dynamic Calculations
Both increment and decrement support calc() expressions with CSS variables:
<!-- Static increment -->
<button data-state-increment="10">Add 10</button>
<!-- Dynamic: increment scales with level -->
<button data-state-increment="calc(var(--state-level) * 5)">
Level-scaled Click
</button>
<!-- Dynamic: cost increases with score -->
<button data-state-increment="calc(1 + var(--state-score) * 0.1)">
Increasing Returns
</button>Both increment and decrement automatically respect data-[attr]-min and data-[attr]-max bounds!
Conditional Triggers:
Use data-state-condition to only execute operations when a condition is met (perfect for costs, requirements, unlock systems):
<!-- Only works if score >= 20 -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="level"
data-state-increment="1"
data-state-condition="score >= 20">
Level Up (costs 20)
</button>
<!-- Complex conditions with AND/OR -->
<button data-state-condition="gold >= 100 and level < 10">
Affordable Upgrade
</button>
<!-- Multiple attributes -->
<button data-state-condition="health > 0 and mana >= 50">
Cast Spell
</button>When a condition fails, the button gets the state-disabled class automatically! Style it with CSS:
.state-disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}Chaining Multiple Operations:
Use data-state-trigger-chain to perform multiple operations sequentially (perfect for complex game mechanics):
<!-- Level up button that both spends gold AND increases level -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-condition="gold >= 100"
data-state-trigger-chain="spendGold,gainLevel">
Level Up (costs 100 gold)
</button>
<!-- Hidden trigger: deduct gold -->
<button id="spendGold"
data-state
data-state-trigger
data-state-bind="player"
data-state-attr="gold"
data-state-decrement="100"
style="display:none">
</button>
<!-- Hidden trigger: add level -->
<button id="gainLevel"
data-state
data-state-trigger
data-state-bind="player"
data-state-attr="level"
data-state-increment="1"
style="display:none">
</button>Auto-firing Triggers:
Use data-state-autofire="true" to automatically fire a trigger whenever its condition becomes true (perfect for passive income, auto-unlocks, achievements, and automatic progression):
<!-- Passive income: auto-collect gold whenever it reaches 10 -->
<button id="autoCollect"
data-state
data-state-trigger
data-state-bind="player"
data-state-attr="gold"
data-state-decrement="10"
data-state-condition="gold >= 10"
data-state-autofire="true"
data-state-trigger-chain="addScore"
style="display:none">
</button>
<button id="addScore"
data-state
data-state-trigger
data-state-bind="player"
data-state-attr="score"
data-state-increment="10"
style="display:none">
</button>
<!-- Auto-unlock: automatically upgrade when level reaches 5 -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="upgraded"
data-state-set="true"
data-state-condition="level >= 5"
data-state-autofire="true"
style="display:none">
</button>
<!-- Achievement system: auto-trigger when condition met -->
<button data-state
data-state-trigger
data-state-bind="achievements"
data-state-attr="firstWin"
data-state-set="true"
data-state-condition="wins >= 1"
data-state-autofire="true"
style="display:none">
</button>The magic: When the condition transitions from false → true, the trigger fires automatically! No click required. No visibility required. This is the missing primitive for automatic game mechanics.
Works with any element:
<div data-state-trigger data-state-bind="player" data-state-toggle="shielded">
Click me to toggle shield!
</div>State.js v1.1.0 adds seven powerful declarative primitives specifically designed for game development and interactive experiences. Build complete games with zero hand-written JavaScript logic.
Automatically fire triggers at regular intervals (perfect for passive income, cooldowns, game ticks):
<!-- Passive gold income: +1 gold every second -->
<button id="passiveGold"
data-state
data-state-trigger
data-state-bind="player"
data-state-attr="gold"
data-state-increment="1"
data-state-interval="1000"
style="display:none">
</button>
<!-- Health regeneration: +5 HP every 2 seconds (only if alive) -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="health"
data-state-increment="5"
data-state-interval="2000"
data-state-condition="health > 0 and health < 100"
style="display:none">
</button>How it works:
- Fires the trigger automatically every N milliseconds
- Respects
data-state-condition(won't fire if condition is false) - Uses a single efficient shared scheduler for all interval triggers
- Perfect for idle games, passive effects, and time-based mechanics
Set an attribute to an exact value (unlike increment/decrement). Supports calc() expressions:
<!-- Reset health to full -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="health"
data-state-set="100">
Full Heal
</button>
<!-- Set mana to half of max -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="mana"
data-state-set="calc(var(--state-manamax) / 2)">
Restore 50% Mana
</button>
<!-- Level-scaled restore -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="gold"
data-state-set="calc(var(--state-level) * 100)">
Set Gold to Level × 100
</button>Use cases:
- Reset/restore mechanics
- Level-scaled rewards
- Percentage-based calculations
- Achievement unlocks (set boolean flags)
Display dynamic text using {token} syntax that updates automatically:
<div id="player"
data-state
data-state-watch="level,health,healthmax,gold"
data-level="1"
data-health="100"
data-healthmax="100"
data-gold="0">
</div>
<!-- Text updates automatically when attributes change -->
<h1 data-state
data-state-bind="player"
data-state-text="Level {level} Hero">
</h1>
<p data-state
data-state-bind="player"
data-state-text="HP: {health}/{healthmax}">
</p>
<div data-state
data-state-bind="player"
data-state-text="Gold: {gold} | Level: {level}">
</div>
<!-- Works with any attribute -->
<span data-state
data-state-bind="player"
data-state-text="You have {gold} gold coins!">
</span>How it works:
- Replaces
{attributeName}tokens with current attribute values - Updates automatically when any referenced attribute changes
- Supports multiple tokens in one template
- No manual display element management required
Dynamically add/remove CSS classes based on conditions:
<!-- Add 'critical' class when health is low -->
<div id="healthBar"
data-state
data-state-bind="player"
data-state-class="critical"
data-state-class-condition="health <= 20">
</div>
<!-- Multiple conditional classes using numbered suffixes -->
<div id="player"
data-state
data-state-bind="game"
data-state-class="low-health"
data-state-class-condition="health <= 30"
data-state-class-2="powered-up"
data-state-class-condition-2="powerup == true"
data-state-class-3="max-level"
data-state-class-condition-3="level >= 99">
</div>
<!-- Style the classes in CSS -->
<style>
.critical {
animation: critical-pulse 0.5s infinite;
border: 3px solid red;
}
.low-health {
filter: hue-rotate(180deg);
}
.powered-up {
box-shadow: 0 0 20px gold;
animation: glow 1s infinite;
}
.max-level {
background: linear-gradient(45deg, gold, orange);
}
</style>Features:
- Supports up to 10 class/condition pairs per element (use
-2,-3, etc.) - Classes add/remove automatically when conditions change
- Perfect for visual state feedback
- Works with any CSS animations or effects
Play procedurally generated Web Audio sounds on trigger clicks (no audio files needed!):
<!-- Built-in sounds: click, levelup, buy, error, coin -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="score"
data-state-increment="1"
data-state-sound="click">
Click (+1 score)
</button>
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="level"
data-state-increment="1"
data-state-sound="levelup"
data-state-condition="xp >= 100">
Level Up!
</button>
<button data-state
data-state-trigger
data-state-bind="shop"
data-state-attr="gold"
data-state-decrement="50"
data-state-sound="buy"
data-state-condition="gold >= 50">
Buy Item (50g)
</button>
<!-- Error sound when clicking disabled buttons -->
<button data-state
data-state-trigger
data-state-sound="error"
data-state-condition="gold >= 1000">
Expensive Item (1000g)
</button>
<!-- Coin pickup sound -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="gold"
data-state-increment="10"
data-state-sound="coin">
Collect Gold
</button>Built-in sounds:
- click - 80ms sawtooth beep (UI feedback)
- levelup - 3-note arpeggio C4→E4→G4 (achievements)
- buy - 100ms sine tone at 600Hz (purchases)
- error - 80ms square wave at 120Hz (failures)
- coin - Rising pitch 880→1200Hz (pickups)
Features:
- Zero external dependencies (uses Web Audio API)
- Procedurally generated (no audio files to load)
- Plays on trigger click before executing the action
- Respects browser autoplay policies
Automatically save and restore state to localStorage:
<div id="gameState"
data-state
data-state-watch="level,gold,health,xp"
data-state-persist="true"
data-state-persist-key="my-game-save"
data-level="1"
data-gold="0"
data-health="100"
data-xp="0">
</div>How it works:
- Automatically loads saved state on page load
- Saves changes to localStorage with 500ms debounce (prevents excessive writes)
- Saves all attributes listed in
data-state-watch - Uses element ID as save key if
data-state-persist-keynot specified - Perfect for idle games, progress persistence, user preferences
Clear saved data:
// From browser console or your own JS:
localStorage.removeItem('my-game-save');Dispatch CustomEvents when triggers fire (perfect for external integrations, analytics, achievements):
<!-- Dispatch event when score increases -->
<button data-state
data-state-trigger
data-state-bind="player"
data-state-attr="score"
data-state-increment="10"
data-state-event="score-increased">
+10 Score
</button>
<!-- Listen to events in JavaScript -->
<script>
document.addEventListener('state:score-increased', (e) => {
console.log('Score changed!', e.detail);
// e.detail contains:
// {
// element: <the trigger button>,
// attr: "score",
// oldValue: "0",
// newValue: "10",
// boundId: "player"
// }
});
// Track level-ups
document.addEventListener('state:level-up', (e) => {
// Send to analytics
gtag('event', 'level_up', { level: e.detail.newValue });
});
// Achievement tracking
document.addEventListener('state:achievement-unlocked', (e) => {
showNotification(`Achievement unlocked: ${e.detail.attr}!`);
});
</script>Use cases:
- Analytics integration
- Achievement systems
- External UI updates
- Debug logging
- Third-party integrations
Event naming:
- Event name is prefixed with
state:(e.g.,data-state-event="win"→state:win) - Events bubble up the DOM
- Not cancelable (fire-and-forget)
Combining all extensions, here's a complete idle clicker game:
<div id="game"
data-state
data-state-watch="gold,goldPerClick,goldPerSecond,level"
data-state-persist="true"
data-state-persist-key="idle-game-v1"
data-gold="0"
data-goldPerClick="1"
data-goldPerSecond="0"
data-level="1">
<!-- Display with template interpolation -->
<h1 data-state
data-state-bind="game"
data-state-text="Level {level} Miner">
</h1>
<p data-state
data-state-bind="game"
data-state-text="Gold: {gold} | Per Click: {goldPerClick} | Per Second: {goldPerSecond}">
</p>
<!-- Manual clicking -->
<button data-state
data-state-trigger
data-state-bind="game"
data-state-attr="gold"
data-state-increment="calc(var(--state-goldPerClick))"
data-state-sound="coin"
data-state-event="gold-mined">
Mine Gold
</button>
<!-- Upgrades with conditional classes -->
<button id="upgradeClick"
data-state
data-state-trigger
data-state-bind="game"
data-state-trigger-chain="payUpgrade,addPower"
data-state-condition="gold >= 50"
data-state-sound="buy"
data-state-class="affordable"
data-state-class-condition="gold >= 50">
Upgrade Pickaxe (50g)
</button>
<!-- Hidden triggers for upgrade chain -->
<button id="payUpgrade"
data-state-trigger
data-state-bind="game"
data-state-attr="gold"
data-state-decrement="50"
style="display:none">
</button>
<button id="addPower"
data-state-trigger
data-state-bind="game"
data-state-attr="goldPerClick"
data-state-increment="1"
style="display:none">
</button>
<!-- Passive income with intervals -->
<button data-state
data-state-trigger
data-state-bind="game"
data-state-attr="gold"
data-state-increment="calc(var(--state-goldPerSecond))"
data-state-interval="1000"
data-state-condition="goldPerSecond > 0"
style="display:none">
</button>
<!-- Auto-level-up when gold reaches threshold -->
<button data-state
data-state-trigger
data-state-bind="game"
data-state-attr="level"
data-state-increment="1"
data-state-condition="gold >= 500"
data-state-autofire="true"
data-state-sound="levelup"
data-state-event="level-up"
style="display:none">
</button>
</div>
<style>
/* Visual feedback with conditional classes */
.affordable {
background: gold;
animation: pulse 0.5s infinite;
}
#game[data-level="10"],
#game[data-level="25"],
#game[data-level="50"] {
animation: milestone-celebration 1s ease-out;
}
</style>This game has:
- ✅ Manual clicking with dynamic rewards
- ✅ Upgrade system with costs
- ✅ Passive income ticking every second
- ✅ Auto-level-up when reaching milestones
- ✅ Sound effects for all actions
- ✅ Visual feedback for affordability
- ✅ Persistent save/load with localStorage
- ✅ Event dispatch for analytics/achievements
- ✅ ZERO hand-written game logic JavaScript!
State.js automatically creates CSS variables based on your configuration:
--state-visible(0 or 1)--state-intersection(0-100%)--state-viewport-x(0-100%)--state-viewport-y(0-100%)
When using data-state-watch="health,score,level":
--state-health(raw value)--state-health-percent(0-100%)--state-health-normalized(0-1)--state-health-deg(0-360deg)--state-health-reverse(100%-0%)--state-score(raw value)--state-level(raw value)
--state-value(current value)--state-value-percent(percentage of range)--state-min,--state-max(range bounds)
--state-time(current time)--state-progress(0-100%)--state-playing(0 or 1)--state-volume(0-100)
--state-width(px)--state-height(px)--state-aspect-ratio(calculated)
<div data-state></div>
<!-- OR -->
<div class="enable-state"></div>| Attribute | Description | Example |
|---|---|---|
data-state-var="true" |
Enable all CSS variables | data-state-var="true" |
data-state-watch="attr1,attr2" |
Watch specific data attributes | data-state-watch="health,mana,xp" |
data-state-bind="id1,id2" |
Auto-bind input to element IDs | data-state-bind="player,enemy" |
data-state-attr="attrName" |
Which attribute to update when binding | data-state-attr="health" |
data-state-value="value" |
Value to set when trigger is clicked (supports calc()) | data-state-value="100" or calc(var(--state-level) * 10) |
data-state-increment="amount" |
Amount to add when trigger is clicked (supports calc(), respects min/max) | data-state-increment="10" or calc(var(--state-level) * 5) |
data-state-decrement="amount" |
Amount to subtract when trigger is clicked (supports calc(), respects min/max) | data-state-decrement="5" or calc(var(--state-cost)) |
data-state-trigger |
Make element clickable to trigger state changes | data-state-trigger |
data-state-trigger-chain="id1,id2" |
Click other triggers sequentially after this one | data-state-trigger-chain="payCost,addLevel" |
data-state-condition="expression" |
Only execute if condition is true (adds state-disabled class when false) |
data-state-condition="score >= 20" or "gold >= 100 and level < 10" |
data-state-autofire="true" |
Automatically fire trigger when condition becomes true (requires data-state-condition) |
data-state-autofire="true" |
data-state-toggle="attrName" |
Toggle boolean attribute on/off when clicked | data-state-toggle="powered" |
data-state-display="attrName" |
Auto-display attribute value as text | data-state-display="health" |
| NEW v1.1.0 | Game Development Extensions | |
data-state-interval="ms" |
Auto-fire trigger every N milliseconds (respects conditions) | data-state-interval="1000" |
data-state-set="value" |
Set attribute to exact value (supports calc()) | data-state-set="100" or calc(var(--state-max)) |
data-state-text="template" |
Template string with {token} interpolation | data-state-text="HP {health}/{healthmax}" |
data-state-class="className" |
Conditional CSS class application | data-state-class="critical" |
data-state-class-condition="expr" |
Condition for class (use with data-state-class) | data-state-class-condition="health <= 20" |
data-state-sound="soundName" |
Play Web Audio sound on trigger (click, levelup, buy, error, coin) | data-state-sound="coin" |
data-state-persist="true" |
Auto-save/restore to localStorage | data-state-persist="true" |
data-state-persist-key="key" |
localStorage key (optional, defaults to element ID) | data-state-persist-key="my-game" |
data-state-event="eventName" |
Dispatch CustomEvent as "state:eventName" | data-state-event="score-up" |
data-state-toggles="attr1,attr2" |
Boolean state toggles | data-state-toggles="active,locked" |
data-state-dimensions="true" |
Track width/height | data-state-dimensions="true" |
data-state-media="true" |
Track media playback | data-state-media="true" |
data-state-global="true" |
Set CSS vars on :root |
data-state-global="true" |
data-state-increment="10" |
Update increment for selectors | data-state-increment="10" |
<div data-state
data-state-watch="health"
data-health="100"
data-health-min="0"
data-health-max="100">
</div>State.js includes state-animations.css - a companion stylesheet with predefined animations for common UI patterns and interactive elements.
<link rel="stylesheet" href="src/state-animations.css">.state-notification- Notification slide.state-warning- Warning shake.state-success- Success bounce.state-error- Error shake.state-loading- Loading spin
.state-health-low- Low value warning pulse.state-health-critical- Critical state shake[data-health="0"]- Empty state animation[data-health="100"]- Full/complete glow
.state-score-increase- Value increase pop.state-score-milestone- Milestone celebration.state-level-up- Level/tier change flash
.state-powered- Active/powered state glow.state-invincible- Protected state shimmer.state-shielded- Shield/protection pulse.state-stunned- Disabled/paused effect.state-poisoned- Negative effect pulse.state-frozen- Frozen/locked shake.state-burning- Active damage flicker.state-healing- Positive effect sparkle
View full animation documentation →
<div id="player"
data-state
data-state-watch="health,mana,xp,level"
data-state-var="true"
data-health="100"
data-mana="80"
data-xp="450"
data-level="5"
data-health-max="100"
data-mana-max="100"
data-xp-max="1000">
<div class="health-bar" style="width: var(--state-health-percent)"></div>
<div class="mana-bar" style="width: var(--state-mana-percent)"></div>
<div class="xp-bar" style="width: var(--state-xp-percent)"></div>
<div class="level">Level <span style="--content: var(--state-level)"></span></div>
</div><video data-state
data-state-media="true"
data-state-var="true">
<source src="video.mp4">
</video>
<style>
video::after {
content: "";
width: var(--state-progress);
height: 5px;
background: red;
position: absolute;
bottom: 0;
left: 0;
}
</style><div data-state
data-state-toggles="active,locked,complete"
data-active="true"
data-locked="false"
data-complete="false">
</div>/* Automatically applied classes */
.state-active {
filter: brightness(1.2);
transform: scale(1.05);
}
.state-locked {
filter: grayscale(1) brightness(0.6);
cursor: not-allowed;
}
.state-complete {
animation: complete-check 0.5s forwards;
}<div id="clicker"
data-state
data-state-watch="score"
data-state-var="true"
data-score="0"
data-score-max="100">
<h1>Score: <span data-state-display="score">0</span></h1>
<button data-state
data-state-trigger
data-state-bind="clicker"
data-state-attr="score"
data-state-increment="1">
Click Me!
</button>
</div>
<style>
/* Celebrate milestones with CSS alone */
#clicker[data-score="10"],
#clicker[data-score="20"],
#clicker[data-score="30"] {
animation: milestone-burst 0.5s ease-out;
}
#clicker[data-score="100"] {
animation: victory-flash 1s ease-out;
}
/* Progress bar using CSS variables */
#clicker::after {
content: "";
width: var(--state-score-percent);
height: 10px;
background: linear-gradient(90deg, red, yellow, green);
}
</style><div id="audio"
data-state
data-state-watch="volume"
data-state-var="true"
data-volume="50"
data-volume-min="0"
data-volume-max="100">
<h2>Volume: <span data-state-display="volume">50</span>%</h2>
<!-- Decrement button (auto-stops at 0) -->
<button data-state
data-state-trigger
data-state-bind="audio"
data-state-attr="volume"
data-state-decrement="10">
-
</button>
<!-- Increment button (auto-stops at 100) -->
<button data-state
data-state-trigger
data-state-bind="audio"
data-state-attr="volume"
data-state-increment="10">
+
</button>
<!-- Visual bar updates automatically -->
<div class="volume-bar" style="width: var(--state-volume-percent);"></div>
</div><div id="idleGame"
data-state
data-state-watch="gold,level,clickPower"
data-state-var="true"
data-gold="0"
data-level="1"
data-clickPower="1">
<h1>Gold: <span data-state-display="gold">0</span></h1>
<h2>Level: <span data-state-display="level">1</span></h2>
<p>Click Power: <span data-state-display="clickPower">1</span></p>
<!-- Basic click: adds clickPower to gold -->
<button data-state
data-state-trigger
data-state-bind="idleGame"
data-state-attr="gold"
data-state-increment="calc(var(--state-clickPower))">
Mine Gold
</button>
<!-- Upgrade: increases clickPower, costs gold -->
<button data-state
data-state-trigger
data-state-bind="idleGame"
data-state-attr="clickPower"
data-state-increment="1">
Upgrade Pick (+1 power)
</button>
<!-- Level up: costs increase with level -->
<button data-state
data-state-trigger
data-state-bind="idleGame"
data-state-attr="level"
data-state-increment="1">
Level Up
</button>
</div>
<style>
/* Different animations per level */
#idleGame[data-level="5"],
#idleGame[data-level="10"] {
animation: level-milestone 1s ease-out;
}
/* Click power visualization */
#idleGame::after {
content: "";
width: calc(var(--state-clickPower) * 10px);
height: 5px;
background: gold;
}
</style>State.js is part of a complete CSS/HTML UI development toolkit from iDev Games:
Five libraries working together for pure CSS/HTML interactive experiences:
-
Keys.js - Keyboard input tracking
--key-space,--key-up,--key-down, etc.
-
Cursor.js - Mouse position tracking
--cursor-x,--cursor-y,--cursor-speed, etc.
-
Touch.js - Touch gesture tracking
--touch-x,--touch-velocity-x,--touch-distance, etc.
-
Motion.js - Time/animation tracking
--motion-progress,--motion-time,--motion-loop, etc.
-
State.js ⭐ - UI state & data binding
--state-health,--state-score,--state-level, etc.
<div id="game"
data-state
data-state-watch="health,score"
data-health="100"
data-score="0"
data-cursor
data-cursor-var="true"
data-keys
data-keys-watch="space,up,down">
<!-- Health bar follows cursor -->
<div class="health-bar" style="
width: var(--state-health-percent);
transform: translateY(var(--cursor-y));
"></div>
<!-- Score pulses when space pressed -->
<div class="score" style="
transform: scale(calc(1 + var(--key-space) * 0.5));
">
Score: <span data-state-value="score"></span>
</div>
</div>
<style>
/* When health is low AND cursor is idle */
body.cursor-idle [data-health="10"],
body.cursor-idle [data-health="20"] {
animation: warning-pulse 1s infinite;
}
/* When up arrow pressed AND health full */
.key-up[data-health="100"] {
animation: victory-jump 0.5s ease-out;
}
</style>Result: A complete interactive UI system with dynamic data, user input tracking, and reactive animations - all in CSS! Perfect for games, dashboards, data visualizations, and interactive experiences.
State.js uses modern browser APIs:
- IntersectionObserver API
- MutationObserver API
- CSS Custom Properties
Supported browsers:
- Chrome/Edge 58+
- Firefox 55+
- Safari 12.1+
- Opera 45+
State.js is optimized for performance:
- ✅ Passive event listeners
- ✅ requestAnimationFrame for DOM updates
- ✅ Map-based attribute caching
- ✅ Conditional updates (only when values change)
- ✅ Efficient MutationObserver usage
Check out the documentation page code as an example: https://github.com/iDev-Games/State-JS/blob/master/index.html
Declarative over Imperative
State.js follows the same philosophy as all iDev Games libraries:
- ✅ Describe what you want (HTML data attributes)
- ✅ Style how it looks (CSS)
- ❌ No complex JavaScript APIs to learn
- ❌ No framework dependencies
The goal: Enable developers to build reactive, data-driven interfaces using HTML and CSS skills they already have - whether for dashboards, web apps, visualizations, or games.
MIT License - see LICENSE file for details
iDev Games
- GitHub: @iDev-Games
- Dev.to: @idevgames
Contributions, issues, and feature requests are welcome!
Feel free to check the issues page.
Give a ⭐️ if this project helped you!
