No frontend framework
Source: dockview/public/ · gh-collab-manager/public/ Category: Pattern — architecture
No framework — the frontend is three files: index.html, style.css, app.js. No build step, no node_modules, no bundler. Deliberately chosen for single-user admin dashboards where React would pay a complexity tax for no real benefit.
What it is
Section titled “What it is”A frontend written entirely with the platform: HTML for structure, CSS for style, vanilla JS (fetch + DOM) for interactivity. No transpilation, no JSX, no virtual DOM. The browser reads the files as-is.
Served statically by Express or nginx. Deployment is rsync public/ /var/www/app/.
Why it exists
Section titled “Why it exists”The problem: Every web-adjacent project defaults to React + Vite + Tailwind. That’s the right answer for 90% of web apps. For the other 10% — single-user admin panels, homelab dashboards, tool UIs — you’re paying framework setup costs for zero framework benefits.
React solves: component reuse across a large team, state management across many views, SSR for SEO, type-safety with TS, rich ecosystem. A dockview-style dashboard needs none of those.
The fix: ship the platform. HTML/CSS/JS is genuinely fine for small UIs.
When this works
Section titled “When this works”- Single user, single browser. You know the target browser; you’re not shipping to unknown clients.
- Small surface area. A dashboard with 5-10 “views” fits in a couple hundred lines of JS.
- Live-reload not required. You refresh the tab after editing. Acceptable for dev.
- No state across views. Each page is self-contained; no global store.
- Zero build cost is the point. Deploy is
rsync; dev ispython -m http.serveror similar.
When this doesn’t work
Section titled “When this doesn’t work”- Complex state — multiple views depend on the same object, edits in one update others. You’ll reinvent Redux poorly.
- Many developers — the discipline to not import Nth dependency erodes fast.
- Growth horizon — if the UI is going to double in a year, pay the framework tax now rather than rewriting from vanilla later.
- Type safety matters — vanilla JS is quick but loose. Ambitious UIs want TS.
dockview/├── public/│ ├── index.html # structure + inline script tag at the bottom│ ├── style.css # CSS custom properties, grid, flex│ └── app.js # fetch, render, subscribe└── src/ └── server.ts # serves public/, exposes /api/*index.html is the app:
<!doctype html><html><head> <title>dockview</title> <link rel="stylesheet" href="/style.css"></head><body> <header><h1>dockview</h1><span id="status"></span></header> <main> <section id="containers"></section> <section id="metrics"></section> </main> <script src="/app.js" defer></script></body></html>app.js is the entire client:
// Fetch + render on loadfetch('/api/containers').then(r => r.json()).then(renderContainers);
// Subscribe to live updatesconst socket = io();socket.on('container:update', c => updateOne(c));socket.on('metrics', m => renderMetrics(m));
function renderContainers(list) { const el = document.getElementById('containers'); el.innerHTML = list.map(c => ` <div class="card" data-id="${c.id}"> <h3>${c.name}</h3> <span class="status ${c.state}">${c.state}</span> <button onclick="restartContainer('${c.id}')">Restart</button> </div> `).join('');}
async function restartContainer(id) { await fetch(`/api/containers/${id}/restart`, { method: 'POST' });}How it’s used
Section titled “How it’s used”- dockview — the whole frontend
- gh-collab-manager — same approach; ~700 lines of JS for the full admin
- Pattern generalizes to any internal tool where framework setup doesn’t pay off
Gotchas
Section titled “Gotchas”- Templating via
innerHTMLis an XSS vector. User-generated content +innerHTML= trivial XSS. If any string in your template comes from user input, escape it first. For dashboards showing system state, usually not an issue. For anything with user input, usetextContentor build nodes. - Event delegation for dynamic lists. Don’t wire
onclickper card on each render; one click listener on the parent delegates viaevent.target. Cleaner DOM, fewer listeners. - CSS custom properties are your component library. Declare a theme at the root; every “component” (card, button, badge) uses the variables. One file, no specifier hell.
- State in data-attributes.
<div data-id="..." data-state="...">— reads like HTML, updates without re-rendering the whole parent. - Module scripts are nice.
<script type="module" src="app.js">gives youimportwithout a bundler. Keeps code organized for medium-size apps. - Dev convenience. A one-line static-file server (
python -m http.server,caddy file-server, or justnpx serve) is enough for local dev. - Hot reload. You don’t have it. Refresh the tab. For the size of app this pattern targets, that’s fine.
- Drift to framework happens quietly. “Just this one state library…”, “Just a small router…”, “Just a templating engine…”. At some point you’ve built half of React. Notice; either accept and migrate, or push back.
See also
Section titled “See also”- projects/dockview · projects/gh-collab-manager
- components/progress-bar-vanilla-js — what one of these “no-framework components” looks like