A JavaScript framework for building reactive web components with template-based architecture and automatic state synchronization.
- 🥱 Reactive State Management - Automatic DOM updates when state changes
- 🥱 Template-based Components - Define components using HTML templates with
data-componentattributes - 🥱 Hot Module Reloading - Built-in dev server with file watching and auto-reload
- 🥱 Zero Configuration - Works out of the box with sensible defaults
- 🥱 CLI Tools - Development server and build tools included
- 🥱 TypeScript Support - Full TypeScript definitions included
- 🥱 Project Generator - Quick project scaffolding with
create-boredom
# Install the framework
pnpm install @mr_hugo/boredom
# Or create a new project
npx create-boredom my-app
cd my-app
pnpm dev- Create an HTML file with component templates:
<!DOCTYPE html>
<html>
<head>
<title>My boreDOM App</title>
</head>
<body>
<h1>Counter Example</h1>
<simple-counter></simple-counter>
<template data-component="simple-counter">
<div>
<p>Count: <slot name="counter">0</slot></p>
<button onclick="['increase']">+</button>
<button onclick="['decrease']">-</button>
</div>
</template>
<script src="main.js" type="module"></script>
</body>
</html>- Create the component logic in JavaScript:
// main.js
import { inflictBoreDOM, webComponent } from "@mr_hugo/boredom";
// Initialize with state
const uiState = await inflictBoreDOM({ count: 0 });
// simple-counter.js (or inline)
export const SimpleCounter = webComponent(({ on }) => {
on("increase", ({ state }) => {
state.count += 1;
});
on("decrease", ({ state }) => {
state.count -= 1;
});
return ({ state, slots }) => {
slots.counter = state.count;
};
});boreDOM includes a built-in development server with hot reloading:
# Start dev server (watches for changes)
npx boredom
# Custom options
npx boredom --index ./src/index.html --html ./components --static ./publicThe CLI will:
- Watch for file changes in components, HTML, and static files
- Automatically rebuild and inject components
- Serve your app with hot reloading
- Copy static files to the build directory
boreDOM supports multiple deployment modes without requiring a build step.
<script type="module" src="boreDOM.min.js"></script>Full debug features: error context in console, $state/$refs globals, visual indicators.
Disable debug at runtime:
await inflictBoreDOM(state, logic, { debug: false });Use the production bundle for smallest size (~13KB, debug code eliminated):
<script type="module">
import { inflictBoreDOM } from "@mr_hugo/boredom/prod";
await inflictBoreDOM(state, logic);
</script>When errors occur in development mode:
// Console globals (auto-loaded on error)
$state // Mutable state proxy
$refs // Component refs
$rerender()// Retry render after fixing
// Programmatic access
boreDOM.errors // Map of all errors
boreDOM.lastError // Most recent error
boreDOM.rerender() // Re-render errored component
boreDOM.export() // Export state snapshotSee BUILDING_WITH_BOREDOM.md for full debug documentation.
Browser tests are written in Mocha and run in a real browser environment.
# One-shot headless run with Playwright
pnpm test:browser
# Serve the browser test page for Playwright MCP (prints BROWSER_TESTS_URL)
pnpm test:browser:serveNotes:
- Pending tests (1): Phase 6: Validation Edge Cases State edge cases should handle circular references in state.
Playwright MCP example:
// After navigating to BROWSER_TESTS_URL
await page.waitForFunction(() => Boolean(window.__boreDOMTestResults));
const results = await page.evaluate(() => window.__boreDOMTestResults);
console.log(results.stats);Update the pending list after test changes:
pnpm update:pending-testsInitializes the boreDOM framework and creates reactive state.
initialState- Initial application state objectcomponentsLogic- Optional inline component definitionsconfig- Optional configuration ({ debug: boolean | DebugOptions })- Returns - Proxified reactive state object
// Development (default)
const state = await inflictBoreDOM({
users: [],
selectedUser: null,
});
// Production-lite (no build required)
const state = await inflictBoreDOM({ count: 0 }, logic, { debug: false });
// Granular control
const state = await inflictBoreDOM({ count: 0 }, logic, {
debug: {
console: true, // Log errors
globals: false, // Don't expose $state etc.
errorBoundary: true, // Always catch errors
}
});Creates a web component with reactive behavior.
initFunction- Component initialization function- Returns - Component definition for use with boreDOM
const MyComponent = webComponent(({ on, state, refs, self }) => {
// Setup event handlers
on("click", ({ state }) => {
state.clicked = true;
});
// Return render function
return ({ state, slots, refs }) => {
slots.content = `Clicked: ${state.clicked}`;
};
});Components receive these parameters:
on(eventName, handler)- Register event listenersstate- Reactive state accessorrefs- DOM element referencesself- Component instancedetail- Component-specific data
state- Current state (read-only in render)slots- Named content slots for the templaterefs- DOM element referencesmakeComponent(tag, options)- Create child components
Templates use standard HTML with special attributes:
<template data-component="my-component">
<!-- Named slots for dynamic content -->
<div>
<h2><slot name="title">Default Title</slot></h2>
<p><slot name="content">Default content</slot></p>
</div>
<!-- Event dispatching -->
<button onclick="['save']">Save</button>
<button onclick="['cancel']">Cancel</button>
<!-- Reference elements -->
<input ref="userInput" type="text">
</template>- Declare a template with a tag name
<simple-counter></simple-counter>
<template data-component="simple-counter" data-aria-label="Counter">
<p>Count: <slot name="count">0</slot></p>
<button onclick="['increment']">+</button>
<button onclick="['decrement']">-</button>
<!-- Any other data-* on the template is mirrored to the element -->
<!-- e.g., data-aria-label -> aria-label on <simple-counter> -->
<!-- Add shadowrootmode="open" to render into a ShadowRoot -->
<!-- <template data-component=\"simple-counter\" shadowrootmode=\"open\"> -->
<!-- Optional: external script for behavior -->
<script type="module" src="/simple-counter.js"></script>
</template>- Provide behavior (first export is used)
// /simple-counter.js
import { webComponent } from "@mr_hugo/boredom";
export const SimpleCounter = webComponent(({ on }) => {
on("increment", ({ state }) => {
state.count += 1;
});
on("decrement", ({ state }) => {
state.count -= 1;
});
return ({ state, slots }) => {
slots.count = String(state.count);
};
});- Initialize once
import { inflictBoreDOM } from "@mr_hugo/boredom";
await inflictBoreDOM({ count: 0 });What happens under the hood
- The runtime scans
<template data-component>and registers custom elements. - It mirrors template
data-*to host attributes and wires inlineonclick="['...']"to custom events ("[]" is the dispatch action). - Scripts are dynamically imported and run for every matching instance in the DOM (including multiple instances).
- Subsequent instances created programmatically use the same initialization via
makeComponent().
Rendering subscribes to the state paths it reads, and mutations trigger batched updates.
import { inflictBoreDOM, webComponent } from "@mr_hugo/boredom";
export const Counter = webComponent(({ on }) => {
on("inc", ({ state }) => {
state.count++;
}); // mutable state in handlers
return ({ state, slots }) => { // read-only during render
slots.value = String(state.count); // reading subscribes to `count`
};
});
await inflictBoreDOM({ count: 0 });- Subscriptions: Any property read in render (e.g.,
state.count) registers that render as a subscriber to that path. - Mutations: Changing arrays/objects (e.g.,
state.todos.push(...),state.user.name = 'X') schedules a single rAF to call subscribed renders. - Scope: Subscriptions are per component instance; only components that read a path re-render when that path changes.
A typical boreDOM project structure:
my-app/
├── index.html # Main HTML file
├── main.js # App initialization
├── components/ # Component files
│ ├── user-card.html # Component template
│ ├── user-card.js # Component logic
│ └── user-card.css # Component styles
├── public/ # Static assets
│ └── assets/
└── build/ # Generated build files
const Counter = webComponent(({ on }) => {
on("increment", ({ state }) => state.count++);
on("decrement", ({ state }) => state.count--);
return ({ state, slots }) => {
slots.value = state.count;
};
});const TodoList = webComponent(({ on }) => {
on("add-todo", ({ state, e }) => {
state.todos.push({ id: Date.now(), text: e.text, done: false });
});
on("toggle-todo", ({ state, e }) => {
const todo = state.todos.find((t) => t.id === e.id);
if (todo) todo.done = !todo.done;
});
return ({ state, slots, makeComponent }) => {
slots.items = state.todos.map((todo) =>
makeComponent("todo-item", { detail: { todo } })
).join("");
};
});# Development server with file watching
npx boredom [options]
Options:
--index <path> Base HTML file (default: index.html)
--html <folder> Components folder (default: components)
--static <folder> Static files folder (default: public)boreDOM includes full TypeScript definitions:
import { inflictBoreDOM, webComponent } from "@mr_hugo/boredom";
interface AppState {
count: number;
users: User[];
}
const state = await inflictBoreDOM<AppState>({
count: 0,
users: [],
});
const MyComponent = webComponent<AppState>(({ on, state }) => {
// TypeScript will infer correct types
on("increment", ({ state }) => {
state.count++; // ✓ Type-safe
});
return ({ state, slots }) => {
slots.count = state.count.toString();
};
});By default, boreDOM dynamically imports component scripts from separate .js
files. For simpler deployments—CDN usage, embedded widgets, or truly zero-build
setups—you can inline all component logic in a single HTML file.
Pass your components as the second argument to inflictBoreDOM():
<!DOCTYPE html>
<html>
<head>
<title>Single File boreDOM</title>
<script type="module">
import { inflictBoreDOM, webComponent } from "https://esm.sh/@mr_hugo/boredom";
// Define components inline
const Counter = webComponent(({ on }) => {
on("increment", ({ state }) => state.count++);
on("decrement", ({ state }) => state.count--);
return ({ state, slots }) => {
slots.value = String(state.count);
};
});
const Greeter = webComponent(() => {
return ({ state, slots }) => {
slots.message = `Hello, ${state.name}!`;
};
});
// Initialize with state and inline components
await inflictBoreDOM(
{ count: 0, name: "World" },
{
"my-counter": Counter,
"my-greeter": Greeter,
}
);
</script>
</head>
<body>
<my-greeter></my-greeter>
<my-counter></my-counter>
<template data-component="my-counter">
<p>Count: <slot name="value">0</slot></p>
<button onclick="['increment']">+</button>
<button onclick="['decrement']">-</button>
</template>
<template data-component="my-greeter">
<h1><slot name="message">Hello!</slot></h1>
</template>
</body>
</html>This approach:
- Works without any build step or bundler
- Can be served from a CDN or as a static file
- Keeps everything self-contained in one HTML file
- Skips dynamic imports entirely
For larger applications, the standard multi-file approach with the CLI is recommended for better organization.
- Official Documentation: https://hugodaniel.com/pages/boredom/
- Repository: https://github.com/HugoDaniel/boreDOM
- Examples: Check the
/examplesdirectory for complete examples
boreDOM by Hugo Daniel is marked with CC0 1.0