On February 12, 2024, I committed a file with the message:
A not so good scope architecture
Five days later, the next commit said:
New scoping system
That's the most honest summary of how Brio.js was built. I wrote something, I learned why it was wrong, and I wrote it again. I did that a lot, for fun and to learn how the frameworks I used every day actually work.
At some point the "learning project" quietly started running in one small production project of mine. Then it got tests. Then docs. Then a playground, a starter CLI, and a website with a small creature on it (we'll get to the creature).
This week I released Brio.js v5 and made the whole thing public. This post is the "hello, here's what I've been doing in my free time" post.
What Brio is
Brio adds reactivity to HTML with b-* attributes. It works on the real DOM, in place. No virtual DOM, no JSX, no required compiler, and no runtime dependencies.
Here's the smallest useful Brio app. You can save it as a file and open it from any static server:
<div id="counter" b-cloak>
<button b-on-click="$data.count++">
Count: <span b-text="$data.count"></span>
</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/@jnreis/briojs@5.1.1/dist/brio.min.js"></script>
<script>
brio.createApp({ state: () => ({ count: 0 }) }).mount('#counter');
</script>
(Add <style>[b-cloak] { display: none; }</style> so the button doesn't flash before Brio starts.)
That's it: one script tag, one mount, and a button that counts. If you want a build setup instead, npm create brio@latest gives you a starter.
Meet the creature
Every framework website needs a demo. Most use a todo list. I made a small creature with feelings.
You pick a mood, a color, a size, and a name, and the creature updates as you go. There's also a 🎲 Surprise me button, because it's fun.
Here's a trimmed version of the real markup from the homepage. The horns only exist while it's being mischievous:
<div role="img" b-bind-aria-label="$data.greeting"
b-bind-class="['creature', 'is-' + $data.mood]"
b-bind-style="{ '--body': $data.color, '--size': $data.size / 100 }">
<span class="creature-zzz" b-if="$data.mood === 'sleepy'">z z z</span>
<span class="creature-horns" b-if="$data.mood === 'mischievous'"></span>
<span class="creature-eyes"><i></i><i></i></span>
</div>
<label><input type="radio" name="mood" value="sleepy" b-model="$data.mood"> 😴 Sleepy</label>
<input type="range" min="70" max="130" b-model.number="$data.size">
And the greeting is just a getter on the state:
get greeting() {
const name = this.name.trim() || 'This creature';
if (this.mood === 'sleepy') return `${name} needs a nap.`;
if (this.mood === 'mischievous') return `${name} is up to something.`;
return `${name} is having a great day.`;
},
The best part: the playground opens with this exact creature. You can edit its HTML, CSS, and JavaScript and see the changes right away. Give it a fourth mood. Give it a hat. I won't stop you.
Three things I'm excited about
1. Every value says where it came from
In Brio templates you write $data.count, $props.label, or $for.item. Not count, label, or item.
Yes, it's a little more typing. I went back and forth on this for a long time. It became a deliberate breaking change in 4.0, because once lists and components were nested inside each other, "where does item come from?" stopped being a fun puzzle. Now you can read any expression and know which drawer to open. (There's a whole post coming on this decision.)
2. It only owns what you mount
You can mount Brio around one form in a Rails view, one menu in a WordPress theme, or one calculator in an old PHP page. It doesn't care what's around it, and it won't touch anything outside the element you gave it. When the widget is removed, it cleans up after itself.
That's the use case Brio was born from, and the one I care about most: adding interactivity to pages that already exist, without asking the rest of the page to change.
3. It grows with you
It starts with a single counter, but it doesn't stop there:
-
Five starters from
npm create brio: widget, multi-page, SPA, Astro islands, and plugin. - Optional add-ons for routing, HTTP, persistence, extra inputs, and observers. You load only what you use.
- A public plugin API for your own directives and helpers. I moved some of Brio's own features out of the core into add-ons built on that same API, partly to prove it works.
- Parts: copy-paste dialog, disclosure, menu, and tabs components you own and can change.
- Ten example apps, from a counter to a TodoMVC, a 1,000-row table, a Kanban board, and a collaborative editor.
- Integration guides for Rails, Laravel, Django, WordPress, and plain HTML with Express.
There are 36 directives in total, and 1,258 tests that are much stricter with me than I am with myself.
What it's not good at (yet)
I'd rather you hear this from me than find out an hour into trying it:
-
Strict CSP needs a separate build. The standard build uses
new Function, which a policy that forbidsunsafe-evalblocks. Since 5.1 there's a CSP build (@jnreis/briojs/csp) that runs withouteval. It hydrates only components defined in JavaScript, so plain page markup stays inert. - Server-side rendering. Brio enhances HTML your server (or Astro) already rendered. It doesn't render on the server itself.
- Old browsers. It targets modern evergreen browsers.
- It's young, and it's one person. I'm the only maintainer. The ecosystem is the docs, the examples, and whatever you and I build next. If you need a big community and long-term guarantees today, that's a fair reason to wait.
Why share it now?
Because I've reached the point where I can't see its flaws anymore. I know where everything is, so everything feels obvious. That's exactly when you need people who don't know where everything is.
So here's my ask, and it's a small one:
- Open the playground, break the creature, and tell me what confused you.
- Answer this in the comments: if you had to add interactivity to one part of a page you already have, what would it be? A filter? A form that shows and hides fields? Something weirder?
Every answer helps me decide what to write about and build next.
Links:
- Website: briojs.com
- Playground: briojs.com/play
- GitHub: github.com/jnreis/briojs
- Discussions: Say hi or ask anything
Thanks for reading. Bean says hi. Bean is also asleep.

Top comments (0)