DEV Community

Riccardo Degni
Riccardo Degni

Posted on

VanillaCreamJS: JavaScript with Superpowers

Image description

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>
Enter fullscreen mode Exit fullscreen mode

Or, install it via NPM:

npm install vanillacreamjs
Enter fullscreen mode Exit fullscreen mode

💥 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>
Enter fullscreen mode Exit fullscreen mode

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 (like onClick, 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>
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

🧠 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

🧩 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++)
  }
}
Enter fullscreen mode Exit fullscreen mode

To render this component in one or more places, just use:

$('#counter1, #counter2').component(Counter)
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

🚀 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.

👉 Documentation

🧪 Live Demos

🛠️ Playground

Try it out, and experience the power of reactivity — the vanilla way.

Top comments (0)