@ezbug/slash
v0.3.0
Published
htm + hyper + reactive signals (no VDOM) — tiny, fast, DX-first
Readme
slash
htm + hyper + reactive signals — Tiny, fast, DX-first framework with zero VDOM overhead.
Features
- ✅ Tagged templates via htm
- ✅ Reactive signals with fine-grained reactivity
- ✅ Zero VDOM — Direct DOM manipulation
- ✅ SSR with automatic hydration — Single API for client and server
- ✅ Tiny bundle — Minimal runtime overhead
- ✅ TypeScript support
Installation
bun install slashQuick Start
SPA (Client-Side Rendering)
import { html, render, createSignal } from "slash";
function Counter() {
const count = createSignal(0);
return html`
<div>
<button onClick=${() => count.set(c => c + 1)}>
Count: ${count}
</button>
</div>
`;
}
render(() => Counter(), "#app");SSR with Automatic Hydration
Server:
import { htmlString, renderToString } from "slash/server";
function App() {
const count = createSignal(0);
return htmlString`
<div>
<button onClick=${() => count.set(c => c + 1)}>
Count: ${count}
</button>
</div>
`;
}
const { html, state } = renderToString(() => App());
// Send to client
const htmlResponse = `
<!DOCTYPE html>
<html>
<body>
<div id="app">${html}</div>
<script id="__SLASH_STATE__" type="application/json">
${JSON.stringify(state)}
</script>
<script type="module" src="/client.js"></script>
</body>
</html>
`;Client:
import { html, render, createSignal } from "slash";
function App() {
const count = createSignal(0);
return html`
<div>
<button onClick=${() => count.set(c => c + 1)}>
Count: ${count}
</button>
</div>
`;
}
// render() automatically detects and hydrates server-rendered HTML!
render(() => App(), "#app");That's it! No need to call hydrate() — render() auto-detects when:
- The container has pre-rendered HTML
- A
__SLASH_STATE__script tag exists
The same render() call works for:
- ✅ SSR hydration — Preserves server DOM, attaches events
- ✅ SPA rendering — Creates fresh DOM from scratch
Zero Flash, Zero Re-render
The hydration process:
- Detects server-rendered HTML + state script
- Restores signal values from serialized state
- Re-executes components to attach event listeners
- Reconnects signals to existing DOM markers
- Preserves 100% of server DOM — no flash, no re-render
API Reference
Client API
html
Tagged template for creating elements:
html`<div class="container">${content}</div>`render(view, container)
Renders or hydrates a view into a container:
render(() => App(), "#app");Auto-detects:
- Hydration mode if container has HTML +
__SLASH_STATE__ - Normal mode if container is empty
createSignal(initialValue)
Creates a reactive signal:
const count = createSignal(0);
count.get(); // Get current value
count.set(5); // Set new value
count.set(c => c + 1); // Update with function
count.subscribe(val => console.log(val)); // Subscribe to changesRepeat(listSignal, keyFn, renderFn)
Efficient keyed list rendering:
const items = createSignal([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]);
Repeat(
items,
item => item.id,
item => html`<li>${item.name}</li>`
);Server API
htmlString
Tagged template for SSR (same syntax as html):
import { htmlString } from "slash/server";
const view = htmlString`<div>${content}</div>`;renderToString(view)
Renders view to HTML string with serialized state:
import { renderToString } from "slash/server";
const { html, state } = renderToString(() => App());
// html: "<div>...</div>"
// state: { s0: 0, s1: "value", ... }Migration from hydrate()
If you're using the old hydrate() API:
Before:
import { hydrate } from "slash";
hydrate(() => App(), "#app", { state: window.__SLASH_STATE__ });After:
import { render } from "slash";
render(() => App(), "#app"); // That's it!The hydrate() function is now deprecated. Use render() for everything.
Examples
Counter with Signal
import { html, render, createSignal } from "slash";
function Counter() {
const count = createSignal(0);
return html`
<div>
<button onClick=${() => count.set(c => c - 1)}>-</button>
<span>${count}</span>
<button onClick=${() => count.set(c => c + 1)}>+</button>
</div>
`;
}
render(() => Counter(), "#app");Todo List with Repeat
import { html, render, createSignal, Repeat } from "slash";
function TodoList() {
const todos = createSignal([
{ id: 1, text: "Learn Slash", done: false },
{ id: 2, text: "Build app", done: false }
]);
const addTodo = (text: string) => {
todos.set(t => [...t, { id: Date.now(), text, done: false }]);
};
const toggle = (id: number) => {
todos.set(t => t.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
return html`
<div>
<ul>
${Repeat(
todos,
todo => todo.id,
todo => html`
<li>
<input
type="checkbox"
checked=${todo.done}
onClick=${() => toggle(todo.id)}
/>
<span style=${{ textDecoration: todo.done ? 'line-through' : 'none' }}>
${todo.text}
</span>
</li>
`
)}
</ul>
</div>
`;
}
render(() => TodoList(), "#app");Nested Components
import { html, render, createSignal } from "slash";
function Header({ title }: { title: string }) {
return html`<header><h1>${title}</h1></header>`;
}
function Counter() {
const count = createSignal(0);
return html`
<div>
<button onClick=${() => count.set(c => c + 1)}>
Clicks: ${count}
</button>
</div>
`;
}
function App() {
return html`
<main>
<${Header} title="My App" />
<${Counter} />
</main>
`;
}
render(() => App(), "#app");Development
Install Dependencies
bun installBuild
bun run buildRun Tests
bun testType Checking
bun run build:typesWhy Slash?
- No VDOM overhead — Direct DOM manipulation is faster
- Fine-grained reactivity — Only update what changed
- Simple mental model — Tagged templates + signals
- SSR with zero config — Automatic hydration detection
- Tiny runtime — Minimal JavaScript shipped to client
- Great DX — TypeScript support, simple API
License
MIT
Created with Bun
