DEV Community

Cover image for I Built a 2KB React Alternative That Uses Zero Custom Syntax
Cheela Sathvik
Cheela Sathvik

Posted on

I Built a 2KB React Alternative That Uses Zero Custom Syntax

You know what's funny? We keep inventing new ways to do the same thing: make HTML dynamic.

React gave us JSX. Vue gave us v-bind. Alpine gave us x-data. Svelte gave us its own template syntax. Each one reinvented how we write HTML, then asked us to install build tools to compile it back into... HTML and JavaScript.

What if we just used HTML and JavaScript?

That's Lume.js. I just released version 1.0.0, and it's a bet that web standards are actually good enough.


TL;DR

Lume.js is a 2KB reactive framework that uses only standard HTML and JavaScript. No JSX, no custom syntax, no build step required. Just data-bind attributes and ES6 Proxies. It's 9-21x smaller than React/Vue/Alpine.

→ Try it in 30 seconds | GitHub | npm


The Entire API in 30 Seconds

Here's a two-way bound input:

HTML:

<div>
  <h1>Hello, <span data-bind="name"></span>!</h1>
  <input data-bind="name" placeholder="Type your name">
</div>
Enter fullscreen mode Exit fullscreen mode

JavaScript:

import { state, bindDom } from 'lume-js';

const store = state({ name: 'World' });
bindDom(document.body, store);
Enter fullscreen mode Exit fullscreen mode

That's it. No JSX. No v-model. No x-data. No compiler. Just data-bind, which has been valid HTML since 2008.

Type in the input? The span updates. Change store.name in your code? The input updates. It's reactive, it's two-way, and your HTML passes validation.

→ See it working live


Why This Exists

Here's the thing: 80% of websites don't need a framework. Vanilla JavaScript is powerful enough for most things. DOM manipulation? Easy. Event listeners? Simple. Animations with GSAP? Works great.

The one thing vanilla JS lacks is reactivity.

You can write element.textContent = count, but when count changes, you have to remember to update the element manually. You can build dynamic forms, but you end up with spaghetti code tracking which values changed and which DOM elements need updates.

That's the only real problem. Not the language. Not the DOM APIs. Just reactivity.

So I built Lume to solve that one problem. Now I can use vanilla JS with reactivity everywhere:

  • WordPress sites where I can't install a build pipeline
  • Shopify themes that just need a dynamic cart
  • GSAP animations driven by reactive state
  • Internal tools that don't need webpack

No framework. No build step. Just JavaScript with the one missing piece added back in.

Want to see real examples? Check out the Todo app and Tic-Tac-Toe.


What Makes This Different (And Why You Might Care)

It's small. Under 2KB gzipped for the core. That's smaller than most images on your page. React + ReactDOM is ~42KB. Vue 3 is ~18KB. Alpine.js is ~15KB. Lume is under 2KB.

It uses standards. data-* attributes are standard HTML5. Proxies are standard ES6. It works in HTML validators, screen readers, email templates, CMS themes—places where custom syntax breaks.

No build step. Drop it in via CDN and start coding. Want to use a bundler? Fine. Don't want to? Also fine. I built working examples in single HTML files because I could.

It's fast. No virtual DOM. No diffing. When you change store.count, it directly updates element.textContent. For most UIs, this is faster than frameworks that diff virtual trees.


What You Actually Get

Automatic dependency tracking:

import { effect } from 'lume-js';

effect(() => {
  console.log(`Total: ${store.price * store.quantity}`);
  // Re-runs automatically when price or quantity changes
});
Enter fullscreen mode Exit fullscreen mode

Computed values:

import { computed } from 'lume-js/addons';

const total = computed(() => store.price * store.quantity);
console.log(total.value); // Cached, updates automatically
Enter fullscreen mode Exit fullscreen mode

Efficient list rendering:

import { repeat } from 'lume-js/addons';

repeat(container, store, 'items', {
  key: item => item.id,
  render: (item, el) => {
    el.textContent = item.name;
  }
});
// Reuses DOM elements, faster than re-rendering
Enter fullscreen mode Exit fullscreen mode

Microtask batching: Update the same property 10 times? The DOM updates once. Performance without thinking.

All of this in 2KB.

→ Play with these examples in the interactive playground


Where It Actually Works

Lume isn't meant to replace full frameworks for every scenario. If you need a complete application framework with extensive routing and complex state management, there are excellent options out there built for that.

Lume.js works when you need:

  • Reactivity on a mostly static site (blogs, marketing pages)
  • Dynamic forms without framework overhead
  • Something that works in WordPress, Shopify, or other restricted environments
  • Progressive enhancement (page works with JS disabled)
  • Prototypes where webpack feels like overkill

Think of it as Knockout.js for 2025. Simple, standards-based reactivity when you don't need a full framework.


Install & Try It Now

Via CDN (quickest way):

<script type="module">
  import { state, bindDom } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';

  const store = state({ count: 0 });
  bindDom(document.body, store);
</script>

<button onclick="store.count++">Clicked <span data-bind="count">0</span> times</button>
Enter fullscreen mode Exit fullscreen mode

Via npm:

npm install lume-js
Enter fullscreen mode Exit fullscreen mode

The website has:

  • Live playground - Test it in your browser
  • Step-by-step tutorials - Build a Todo app and Tic-Tac-Toe
  • Full API docs - Everything you need to know

→ Start building now


The Philosophy Behind It

Standards age better than frameworks. JavaScript frameworks change every few years. data-* attributes have been stable since 2008. ES6 Proxies since 2015. What you build with Lume today will probably work in browsers 10 years from now.

Explicit beats magic. Lume requires you to wrap nested objects in state() explicitly. Yeah, it's more typing. But you know exactly what's reactive. No surprises when performance tanks because the framework auto-wrapped your entire Redux store.

Small and complete beats large and comprehensive. I didn't add features to compete with React. I added the minimum needed for reactive UIs: state, bindings, effects, computed values, list rendering. If you need routing or global state management, use another library. Lume plays well with others.


What This Took

Six months of nights and weekends. I rewrote the core three times. The first version used manual subscriptions and was too verbose. The second used automatic tracking but leaked memory. The third got it right: automatic tracking with explicit cleanup.

I wrote 114 tests with full coverage. Built working examples (Todo app, Tic-Tac-Toe, multi-step forms). Made sure bundle size never exceeded 2KB. Set up a website with docs and interactive tutorials.

The hardest part? Figuring out what not to add. Every feature request meant more bytes. More API surface. More ways to use it wrong. I said no to routing, animations, transitions, form validation, internationalization. Not because they're bad, but because they'd make the core worse.

Lume does one thing: makes your HTML reactive. Everything else is optional addons or separate libraries.


What Happens Next

The core API is stable. No breaking changes planned. The repeat addon is marked experimental because I want to refine its API before locking it in, but it works and it's tested.

I'm considering addons for routing and form validation, but only if they stay small and standards-based. No scope creep.

Mostly, I want to see what people build. If you try Lume and it doesn't fit your use case, tell me why. If you build something cool, show me. If you find a bug, open an issue.

This is version 1.0 of a bet that JavaScript and HTML are good enough without reinventing them every two years.


Links

Start with the interactive playground - you'll be productive in 5 minutes.

Top comments (0)