See the project on Github, the code is fairly small. If you need anything, the discussions are open!
Bruh Docs
(This is a work in progress)
Why You Should Care
Bruh is a modern javascript library made to build anything for the web. You get a simple API to both generate HTML and play with the DOM - at basically no cost. Bruh optionally integrates with vite for an instantaneous dev server, easy building, jsx/ts/mdx support, and more.
It has a small code size (0 if you just use prerendering) and is highly performant. Everything about it is optimized for simplicity and flexibility - so you can be creative! Bruh enables the right abstractions (like functional reactivity) without stealing your control over the underlying imperative Web/DOM API's.
No matter what you build - a tiny one-off static web page or a complex web app - bruh aims to be simpler, more performant, and more flexible than other libraries like React.
In Abstract
This library's design is derived from these needs:
- Deliver arbitrary "prerendered" HTML first
- "Hydrate" this HTML in arbitrary ways
- Dynamically create interfaces with the DOM
- Manipulate the DOM using modern and fast browser API's
- Do all of this in a unified and highly composable way
So the typical way to use bruh would look like:
- Make a web page as a component
- Render that component as a string of HTML and serve that (write the string to a file to statically serve it or stream it if it is dynamically prerendered)
- Load a javascript entry point that contains code which "hydrates" the HTML
If you decide to skip #3, then bruh can function as a library that simply builds fully static web pages. If you want to make a fully-fledged web app, then this process is open to extreme flexibility. You could go the Jamstack route (statically prerendered html) or arbitrarily integrate bruh into any nodejs server.
Quick Start
If you want to make a new project:
npm init bruh
If you want to add bruh to an existing project:
npm i bruh
and if you want to also include vite:
npm i -D vite vite-plugin-bruh
A Tour
Making Elements
A function named e
(for "element") is one very important piece of the puzzle:
it enables you to make elements in an intuitive and compact way.
Here is an example of its usage:
import { e } from "bruh/dom"
const component =
e("div")(
e("p")("bruh is neat"),
e("p")(
"it is ", e("strong")("very"), " simple"
)
)
document.body.append(component)
If the above code were to be run on a blank page, it would result in this DOM structure:
<!doctype html>
<html>
<head></head>
<body>
<div>
<p>bruh is neat</p>
<p>it is <strong>very</strong> simple</p>
</div>
</body>
</html>
Of course, writing e("")
all of the time gets annoying, so you can use jsx or even generate your own wrapper functions.
Using the jsx syntax for bruh is pretty familiar:
document.body.append(
<div>
<p>bruh is neat</p>
<p>it is <strong>very</strong> simple</p>
</div>
)
But if you really want to use functions only (maybe to avoid a build tool), you can use this proxy around e()
:
import { e } from "bruh/dom"
import { functionAsObject } from "bruh/util"
const { div, p, strong } = functionAsObject(e)
document.body.append(
div(
p("bruh is neat"),
p(
"it is ", strong("very"), " simple"
)
)
)
Basic Prerendering
Here is a nodejs script that prerenders the same above HTML and saves it as a file:
import { e } from "bruh/dom"
import { functionAsObject } from "bruh/util"
const {
html, head, body,
div, p, strong
} = functionAsObject(e)
import { writeFile } from "fs/promises"
const component =
div(
p("bruh is neat"),
p(
"it is ", strong("very"), " simple"
)
)
const prerendered =
"<!doctype html>" +
html(
head(),
body(
component
)
)
writeFile(new URL("./index.html", import.meta.url), prerendered)
The jsx version (using vite) is even easier, and includes dev server support:
/index.html.jsx
const component =
<div>
<p>bruh is neat</p>
<p>it is <strong>very</strong> simple</p>
</div>
export default () =>
"<!doctype html>" +
<html>
<head />
<body>
{ component }
</body>
</html>
Adding Attributes
Nested elements are only half of the story, though. To be complete, we also need to be able to add attributes.
For jsx, keep in mind that React-specific attributes don't apply. So the class
attribute is just class
, not className
.
For the function version, we only need to place an object as the first argument, e.g. to make
<p id="my-paragraph">
<span class="my-span">Some</span> text.
</p>
we can write
p({ id: "my-paragraph" },
span({ class: "my-span" }, "Some"), " text."
)
Note: in the above case, the jsx version is identical to the intended html output.
Basic Composition
This ability to write HTML in jsx or vanilla javascript becomes extremely powerful later on once you begin to functionally generate and compose components.
Here is a more realistic example of a static web page:
const info = {
title: "My cool web page",
description: "This web page is made with bruh"
}
const styles = ["./index.css", "/css/some-other.css"]
const scripts = ["./hydrate.mjs"]
html({ lang: "en-US" },
head(
// Title and description
title(info.title),
meta({
name: "description",
content: info.description
}),
// Useful metadata
meta({ charset: "UTF-8" }),
meta({
name: "viewport",
content: "width=device-width, initial-scale=1"
}),
// Include styles and scripts
...styles.map(href =>
link({
rel: "stylesheet",
type: "text/css",
href
})
),
...scripts.map(src =>
script({
type: "module",
src
})
)
),
body(
main(
section({ id: "hero" },
hgroup(
h1({ class: "title" }, "Bruh."),
h2({ class: "subtitle" },
"The only library you need for your next web project"
)
)
),
picture(
img({
class: "logo",
alt: "The bruh logo",
src: "/images/logo.svg",
width: 256,
height: 256
})
)
)
)
)
Dynamic DOM Interaction
That covers the simple function call API for building HTML, but there is more to both bruh and the DOM than just nested elements with attributes.
Text Nodes
The DOM has a concept of text nodes, even though you can't write them in HTML. Text nodes are useful for efficiently and cleanly manipulating substrings in the DOM, here is an example of how to work with them in bruh:
import { t } from "bruh/dom"
const unixTime = t(Date.now())
document.body.replaceChildren(
<p>
It has been { unixTime } milliseconds since the
<a href="https://en.wikipedia.org/wiki/Unix_time">Unix Epoch</a>
</p>
)
const updateTime = () => {
unixTime.textContent = Date.now()
requestAnimationFrame(updateTime)
}
requestAnimationFrame(updateTime)
Reactivity
Bruh has no automatic virtual DOM diffing (which is very expensive, complex, and almost never desirable in the first place). For the rare cases where it does make sense, custom, high performance, importable reconciliation functions are being worked on in this PR. Help is wanted, so if you are familiar with longest common subsequence algorithms, please join in!
Now, diffing is not the only declarative way to have reactivity - actually, bruh implements the
even more flexible and performant concept of Reactive
's.
Reactive values wrap any javascript value, enabling references as well as update tracking.
import { r } from "bruh/reactive"
const counter = r(0)
// counter.value === 0
counter.value++
// counter.value === 1
// updates can be reacted to as if they were events
counter.addReaction(() => console.log(counter.value))
counter.value++
// (in console) -> 2
counter.value++
// (in console) -> 3
Functional-Reactive State Graphs
State can be managed however you want - but optimally, you should minimize how many things you touch, and let all "derived state" automatically update from changed "source state". A very elegant way of accomplishing this is to use functional reactive style. Reactive values can depend on other reactive values, only updating when their sources do. Bruh's implementation transparently batches updates across all derived state graphs, ensures that the minimum possible updates happen, and always keeps state consistent.
import { r, FunctionalReactive } from "bruh/reactive"
const a = r(0)
const b = r(0)
const c = r([a, b], () => a.value + b.value)
c.addReaction(() => console.log(c.value))
a.value++
b.value++
/*
Preemptively apply the above updates
Without this, the updates would apply either:
1: just before a reactive value is read
2: during the next microtask
This means that updates are transacted
pretty much in the same way that the DOM does
*/
FunctionalReactive.applyUpdates()
// (in console) -> 2
a.value = 5
b.value = 0
b.value--
// (in console) -> 4
Reactive values in the DOM
With a solid foundation for reactive state, bruh enables a fairly declarative style:
import { r } from "bruh/reactive"
const count = r(0)
const counter =
<button data-value={count}>
Click to increment: { count }
</button>
counter.addEventListener("click", () => count.value++)
document.body.append(counter)
That's all that I have written down at the moment, but there is so much more. This covers the core of bruh, I hope it demonstrated how useful such a simple abstraction over the DOM can be, and how easy it is to have principled SSR.
If you are adventurous, read the (relatively short) code starting here - I'm curious to hear your thoughts!