VanillaCreamJS is a JavaScript library that literally gives “superpowers” to native JavaScript, enabling powerful and modern capabilities like reactivity without the need for build tools or compilation steps.
It’s a one-of-a-kind technology because it:
- Brings reactivity to plain JavaScript
- Works with outstanding performance thanks to the absence of diffing or recalculation algorithms
- Offers powerful tools with a minimal, intuitive API
- Helps improve your understanding of vanilla JavaScript
- Can integrate with any backend stack with zero friction
- Increases developer productivity in small to mid-size projects
- Combines the best of tools like HTMX, Vue, and Alpine
- Its name reflects its goal: refine vanilla JavaScript and elevate it, without ever straying too far from its roots.
VanillaCreamJS is created and maintained by Riccardo Degni, a senior Italian web developer.
👉 https://www.riccardodegni.com/projects/vanillacreamjs
📦 Installation
Simply include VanillaCreamJS via CDN:
<script src="https://cdn.jsdelivr.net/npm/vanillacream/dist/vanillacream.min.js"></script>
Or, install it via NPM:
npm install vanillacreamjs
💥 Superpowers for DOM Elements
At the core of VanillaCream lies the $
function. At first glance it might remind you of jQuery, but it's an entirely different — and much more modern — universe of functionality.
This function returns an element (or a set of elements) with superpowers, including reactive state binding:
<div id="app">
<div id="el"></div>
</div>
<script>
const [el, s] = $('#el', { counter: 10 })
el.text = () => s.counter
el.onClick(() => s.counter++)
</script>
Here, the $
function returns a DOM element (in this case el
) and a reactive state object s
. Every element enhanced by $
exposes powerful properties:
-
text
: set text content -
html
: set inner HTML -
class
: dynamically toggle CSS classes -
on
: add event listeners (likeonClick
,onInput
, etc.) -
attr
: set HTML attributes -
data
: set data attributes
When you assign a function to one of these properties, it becomes reactive — automatically updating the DOM when the state changes.
Example: toggling a CSS class
<div id="app">
<div id="el">box</div>
</div>
<script>
const [el, s] = $('#el', { hl: true })
el.class.highlighted = () => s.hl
el.on.click(() => s.hl = !s.hl)
</script>
<style>
.highlighted { background-color: lightgreen; padding: 10px; }
</style>
Clicking the element toggles the value of s.hl
, which in turn dynamically adds or removes the highlighted class — with no need for manual DOM manipulation.
⚙️ Bulk Reactivity with .set
You can also apply multiple reactive properties at once using .set
:
el.css.set({
color: 'blue',
backgroundColor: () => state.bg,
width: () => state.x + 'px'
})
el.class.set({
active: () => state.active,
box: () => state.counter >= 10,
otherClass: () => state.active && state.counter >= 10
})
el.attr.set({
title: () => `Title: ${state.title}`
})
el.data.set({
x: () => state.x * 10
})
Refs: Powerful Destructuring
The $.refs
function accepts a root element and returns auto-bound variables for every ref in the template, plus a shared reactive state object.
<div id="app">
<div ref="el"></div>
<button ref="btn">Update state</button>
</div>
<script>
const { el, btn, state } = $.refs('#app', { counter: 0, bg: 'lightblue', boxClass: false })
el.html = () => `Counter: ${state.counter}`
el.css.backgroundColor = () => state.bg
el.class.box = () => state.boxClass
btn.onClick(() => {
state.counter++
if (state.bg !== 'lightgreen') state.bg = 'lightgreen'
state.boxClass = !state.boxClass
})
</script>
You can even namespace your refs for more organized destructuring:
<button ref="btn.AZ">Sort A-Z</button>
<button ref="btn.ZA">Sort Z-A</button>
<button ref="btn.Random">Randomize</button>
<ul ref="el"></ul>
<script>
const { el, btn, state } = $.refs('#app', { ... })
</script>
🧠 Deep Reactivity
VanillaCream’s reactive engine supports both primitive and reference types like arrays, objects, and matrices. You can trigger reactivity simply by mutating array elements or object properties — even using methods like push
, pop
, or splice
.
<div id="app">
<div ref="box"></div>
Insert fruit: <input ref="fruit">
<button ref="btn">Add Fruit</button>
</div>
<script>
const { box, fruit, btn, state } = $.refs('#app', { fruits: ['apple', 'pear', 'kiwi'] })
box.text = () => 'Fruits: ' + state.fruits.join(', ')
btn.onClick(() => {
state.fruits.push(fruit.attr.value)
fruit.attr.value = ''
})
</script>
Here, updating the array automatically updates the DOM. No need for complex hooks or rendering logic — just plain JavaScript.
Reactive HTML Templates
VanillaCream supports reactive expressions directly in your HTML using x- attributes:
<div id="app">
<div x-text="`Counter: ${state.counter}`"></div>
<button x-on.click="() => state.counter++">Increase counter</button>
<div x-if="state.counter === 10">Counter is 10!</div>
</div>
<script>
const { state } = $.refs('#app', { counter: 0 })
</script>
AJAX Requests Made Easy
VanillaCream gives you total control over AJAX with a very simple syntax.
data: {
title: 'VanillaCream.js',
body: 'Best Vanilla JS framework',
userId: 1
}
})
el.html = result
You can also bind requests directly to elements:
<div id="app">
<div ref="el"></div>
</div>
<script>
const { el } = $.refs('#app')
el.post('https://jsonplaceholder.typicode.com/posts/', {
start: () => el.text = 'Loading...',
data: { title: 'VanillaCream.js', body: 'Best Vanilla JS framework', userId: 1 }
})
</script>
And yes — you can even define AJAX requests entirely from the HTML (like HTMX, but with just one HTML attribute):
<div x-get="{
url: 'https://httpbin.org/html',
headers: {'Content-type': 'application/json', 'X-Custom': 'Hello!'},
on: 'click'
}">Click here</div>
🧩 Creating Reusable Components with VanillaCream
One of the most powerful features of VanillaCream.js is the ability to build reusable, reactive components in pure JavaScript — without the need for custom render functions, hooks, or any virtual DOM.
Here’s how you can define a minimal, yet fully reactive component:
const Counter = {
state: () => ({ count: 0 }),
template: `
<p ref="p"></p>
<button ref="btn">+</button>
`,
setup({ refs, state }) {
const {p, btn} = refs
p.text = () => `Count: ${state.count}`
btn.onClick(() => state.count++)
}
}
To render this component in one or more places, just use:
$('#counter1, #counter2').component(Counter)
Components in VanillaCream are ultra-light, reactive by default, and can be created with just a simple object structure: state, template, and setup.
Bonus Tip: Components with Parameters
VanillaCream components can also receive custom input data. Just pass it as a second parameter:
<div id="app">
<div ref="counter1"></div>
<div ref="counter2"></div>
</div>
<script>
const Counter = {
data: {start: 0},
state: () => ({ count: 0 }),
template: `<p ref="p"></p><button ref="btn">+</button>`,
setup({ refs, state, data }) {
state.count = data.start
refs.p.text = () => `Count: ${state.count}`
refs.btn.onClick(() => state.count++)
}
}
const {counter1, counter2} = $.refs('#app')
counter1.component(Counter, { data: {start: 5} })
counter2.component(Counter, { data: {start: 100} })
</script>
Using Computed Properties
In VanillaCream, computed properties allow you to derive values based on other reactive state — similar to what you’d find in Vue or SolidJS — but in a cleaner, lighter syntax.
<div id="app">
<div ref="box"></div>
<input ref="input" type="text">
</div>
<script>
const { box, input, state } = $.refs('#app', {
firstName: 'John',
lastName: 'Doe'
})
const fullName = () => `${state.firstName} ${state.lastName}`
box.text = () => `Hello, ${fullName()}!`
input.onInput(() => {
const [fn, ln] = input.attr.value.split(' ')
state.firstName = fn
state.lastName = ln ?? ''
})
</script>
Works with Any Backend
Although VanillaCream supports components, you can also delegate component rendering to your backend. It integrates seamlessly with any backend stack.
For example, in Laravel you could have a Blade view providing data from the DB:
<!-- add-fruit.blade.php -->
<div id="app">
<div ref="box"></div>
Insert fruit: <input ref="fruit">
<button ref="btn">Add Fruit</button>
</div>
<script>
const { box, fruit, btn, state } = $.refs('#app', {
fruits: @json($fruits)
})
box.text = () => 'Fruits: ' + state.fruits.join(', ')
btn.onClick(async () => {
const v = fruit.attr.value
await $.get('/my-backend/add-fruit', {
data: { fruit: v }
})
state.fruits.push(v)
fruit.attr.value = ''
})
</script>
🚀 Ready to Try VanillaCream?
Whether you’re building a quick prototype, enhancing an existing app, or simply want to rediscover the joy of writing clean reactive JavaScript — VanillaCream.js offers a lightweight, elegant solution with zero build steps.
🛠️ Playground
Try it out, and experience the power of reactivity — the vanilla way.
Top comments (0)