Why Pfusch?
Instant Setup
Drop a script tag in your HTML and start building. No configuration files, no package.json, no webpack.
Design System Ready
Works seamlessly with Pure CSS, Bootstrap, Tailwind, or your own CSS. Shadow DOM integration built-in.
Progressive Enhancement
Start with semantic HTML, enhance with interactivity. Works without JavaScript, better with it.
Zero Dependencies
No node_modules folder with 500MB of packages. No build pipeline. No fighting with tooling.
Get Started in 30 Seconds
<script type="module">
import { pfusch, html } from "https://matthiaskainer.github.io/pfusch/pfusch.min.js";
pfusch("live-counter", { count: 0 }, (state) => [
html.div(
html.p`Count: ${state.count}`,
html.button({ click: () => state.count++ }, "Increment")
)
]);
</script>
<live-counter></live-counter>
Interactive Examples
Simple Counter
Reactive state management with direct mutation
pfusch("my-counter", { count: 0 }, (state) => [
html.div(
html.p`Count: ${state.count}`,
html.button({
click: () => state.count++
}, "Increment")
)
]);
Event-Driven Architecture
Loosely-coupled components with global events
<event-counter></event-counter>
<counter-display
eventIncrement="event-counter.increment">
</counter-display>
---
// Component that triggers events
pfusch("event-counter", {}, (state, trigger) => [
html.button({
click: () => trigger("increment")
}, "Increment")
]);
// Component that listens to events
pfusch("counter-display", { count: 0, eventIncrement: "" }, (state) => [
script(function() {
window.addEventListener(state.eventIncrement, () => {
state.count++;
});
}),
html.div`Count: ${state.count}`
]);
Form Validation
Native form integration with live validation
pfusch("my-input", {
name: "", value: "", required: false, errorMessage: ""
}, (state, trigger) => [
html.input({
name: state.name,
value: state.value,
required: state.required,
input: (e) => {
state.value = e.target.value;
// Validation logic...
trigger("validation", { name, isValid, errorMessage });
}
}),
state.errorMessage ? html.div(state.errorMessage) : null
]);
Todo Application
Component composition and state management
pfusch("todo-app", { todos: [] }, (state) => [
html.div(
html.input({
keydown: (e) => {
if (e.key === 'Enter') {
state.todos = [...state.todos, {
id: Date.now(),
text: e.target.value,
completed: false
}];
e.target.value = '';
}
}
}),
html.ul(
...state.todos.map(todo =>
html["todo-item"]({ ...todo })
)
)
)
]);
Progressive Enhancement
Enhance existing HTML with interactivity
| Name | Status | Priority |
|---|---|---|
| Zebra Project | Active | High |
| Apple Initiative | Pending | Low |
| Mango Task | Active | Medium |
| Banana Feature | Completed | High |
// can be any table
<enhanced-table>
<table>
<thead>
<tr>
<th>Name></th>
<th>Status></th>
<th>Priority></th>
</tr>
</thead>
<tbody>
<tr>
<td>Zebra Project></td>
<td>Active></td>
<td>High></td>
</tr>
<tr>
...
</tr>
</tbody>
</table>
<enhanced-table>
----
pfusch("enhanced-table", { sortBy: null, items: [], columns: [] },
(state, trigger, { children }) => {
const sortItems = (items, sortBy) => {
if (!sortBy) return items;
return [...items].sort((a, b) =>
String(a[sortBy]).localeCompare(String(b[sortBy]))
);
};
return [
script(function() {
// Extract data from existing table in light DOM
const tables = children('table');
const headers = Array.from(tables[0].querySelectorAll('thead th'));
// Build columns dynamically from headers
state.columns = headers.map(th => ({
name: th.textContent.trim(),
key: th.textContent.trim().toLowerCase()
}));
// Build items dynamically from rows
const rows = Array.from(tables[0].querySelectorAll('tbody tr'));
state.items = rows.map(row => {
const cells = row.querySelectorAll('td');
const item = {};
state.columns.forEach((col, idx) => {
item[col.key] = cells[idx]?.textContent?.trim() || '';
});
return item;
});
}),
// Add sort buttons
html.div(
...state.columns.map(col =>
html.button({
click: () => state.sortBy = col.key
}, `Sort by ${col.name}`)
),
html.button({ click: () => state.sortBy = null }, "Clear")
),
// Show original while loading, then sorted table
state.loading ? html.slot() : renderSortedTable(state)
];
});
Feedback Panel
Server-friendly form integration with local history and status
<div class="dashboard">
<feedback-panel>
<form method='post' action='https://jsonplaceholder.typicode.com/todos'>
<label>Comment<input name='comment' placeholder='Say hi'></label>
<button type='submit'>Send</button>
</form>
</feedback-panel>
</div>
<script type="module">
import { pfusch, html, script } from './pfusch.js';
pfusch('feedback-panel', { status: 'idle', history: [] }, (state, trigger, { children }) => [
script(function() {
const [form] = children('form');
if (!form) return;
const textarea = form.querySelector('textarea, input[name=\'comment\']');
form.addEventListener('submit', async (event) => {
event.preventDefault();
state.status = 'saving';
const payload = new FormData(form);
await fetch(form.action || '#', { method: form.method || 'post', body: payload });
const text = textarea ? textarea.value : '';
state.history = [{ text, at: Date.now() }, ...state.history].slice(0, 5);
state.status = 'saved';
trigger('submitted', { text });
});
}),
html.slot(),
html.div({ class: 'status' }, state.status),
html.ul(
...state.history.map((item) =>
html.li(
html.time(new Date(item.at).toLocaleTimeString()),
html.span(' ', item.text)
)
)
)
]);
window.addEventListener('feedback-panel.submitted', (event) => {
console.log('Feedback event', event.detail);
});
</script>
Ready to Build Something?
Stop fighting with build tools. Start building.
View on GitHub Read the Docs