🚀 Pfusch

Rapid Prototyping with Zero Build Tools

No npm. No bundler. No build step. Just results in minutes.

336 Lines of Code
3.0K Gzipped
0 Dependencies

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>

Showcase

Live demos built with Pfusch.

🏰

Civilization Wars

Turn-based strategy sandbox with emergent battles.

Open Demo

Ready to Build Something?

Stop fighting with build tools. Start building.

View on GitHub Read the Docs