There is a specific kind of waste that stops being funny once you have shipped it a few times: a page whose only interactive behaviour is a button that opens a panel, served with two hundred kilobytes of framework, a build pipeline, a node_modules directory, and a deploy step that can fail.
The panel is four lines of logic. Everything else is scaffolding for the four lines.
Summit.js is my answer to that, and the short version of the pitch is that Alpine got the ergonomics right and stopped short on the engine. Behaviour belongs on the element it affects. That instinct is correct and it is why HTML-sprinkle runtimes keep getting reinvented. What I wanted was that same authoring model sitting on a real reactivity engine, with a real answer for Content Security Policy, and without a build step ever appearing in the argument.
Behaviour belongs next to the thing it changes
Here is the whole hello world. There is no bundler in this story and there is no step between writing it and it working.
<script src="https://velofy.github.io/summitjs/summit.min.js" defer></script>
<div s-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<p s-show="open">Hello from Summit</p>
</div>
The reason this authoring model keeps coming back is that it collapses the distance between cause and effect. In a component framework, finding out what makes a panel open means opening a component file, tracing a prop, finding the state that owns it, and checking whether a parent re-renders. Here the answer is on the element. You read one line.
That property matters more now than it did five years ago, because a large share of frontend code is written by an agent that has to hold the relevant context in one window. Locality is not a stylistic preference any more. It is a budget.
Where the classic engines stop
The HTML-first runtimes generally implement reactivity as effects that re-run. Something in a scope changed, so the effects registered against that scope run again, and each one reasserts its piece of the DOM. It works. It is also coarser than it needs to be: an effect that reads five values re-runs when any of the five change, even when the change touches something it never displays.
Summit owns a fine-grained signal core instead. Reading a value inside a reactive context subscribes that context to that value. Writing the value notifies exactly the subscribers that read it, and nothing else. A change updates only the DOM that read the value that changed.
The difference shows up in the shape of the work, not in a benchmark you can screenshot. On a dense admin table where one cell changes, the coarse model re-runs the effect that owns the row; the fine-grained model touches one text node. On a page with a header clock and a form, ticking the clock does not walk the form. You stop needing to think about where to draw component boundaries to avoid re-render cascades, because the boundary is the value.
The same core is available on its own, so the reactivity is not welded to the directive layer. That was deliberate: an engine you can only reach through attributes is an engine you cannot test in isolation.
The eval problem
This is the part that decided the architecture, and it is the part most HTML-first runtimes quietly lose.
An attribute like @click="open = !open" contains a string. Something
has to turn that string into behaviour. The path of least resistance is
new Function(...), which is a compiler you get for free from the
runtime. It is also exactly what a strict Content Security Policy exists to
forbid. A CSP without unsafe-eval is one of the highest-leverage
headers you can set, because it converts a whole class of injection bugs from
remote code execution into a console error.
So the options are: give up the CSP, or ship a separate limited build where expressions are pre-compiled and most of the expressive syntax is gone, or write an interpreter.
Summit writes the interpreter. Expressions are parsed and evaluated by hand-written code, never handed to the JavaScript engine as source. That single decision has consequences that ripple outward:
- A strict CSP works out of the box, with no
unsafe-evaland no special build. - The expression language is a closed set, so what an expression can reach is a property of the interpreter rather than a property of the page's global scope.
- Markup generated at runtime, by a template, a CMS, or a model, runs under the same constraints as markup you wrote by hand.
That last point is the one I care about most. If you are going to let a machine write markup, the runtime that executes it should not also be a general-purpose code loader.
The features that are boring until you need them
A framework earns its size in the places where the simple version runs out. Three of those places came up often enough that they are in the core rather than in a plugin:
Keyed list reconciliation
Rendering a list is easy. Rendering a list that changes without
destroying and rebuilding every node, losing focus, losing scroll position and
losing input state, is not. Summit does keyed reconciliation on
s-for, so reordering a list moves nodes instead of recreating them.
The version of this feature that does not exist is the version where you find
out mid-project that your select boxes reset on every sort.
s-if on any element
The classic runtimes restrict conditional rendering to <template>
wrappers. That is an implementation detail leaking into your markup: you end up
adding a wrapper element that exists only to satisfy the framework, which then
breaks your grid or flex layout because there is now an extra box in the tree.
Summit puts s-if on any element.
Cached computed getters
A getter in a scope that runs on every read is a performance trap that scales with how readable you made your template. Summit caches computed getters and invalidates them through the same signal graph as everything else, so you can put a derived value in a getter and read it in six places without paying six times.
What 16KB has to earn
Summit is about 16KB gzipped. The comparison that matters is not against a minimal core, because a minimal core is not what you actually deploy. It is against the core plus the plugins you inevitably add: focus trapping for a modal, floating positioning for a menu, persisted state, input masking, transitions. Those are in Summit's 16KB. In the classic runtimes they are separate downloads, each with its own version and its own way of hooking in.
It also ships TypeScript types, which sounds like a checkbox and is not. Types are how the API surface gets discovered without reading docs, both by a person in an editor and by a model with the definitions in context.
What is not in the 16KB is worth stating too. There is no virtual DOM, because the signal graph already knows exactly which nodes to touch, and a diffing layer would be a second, slower source of truth. There is no build step, which means there is nothing to configure, nothing to keep current, and nothing that can be broken by a transitive dependency you did not choose.
The registration API, and load order
Most of the pain in small frameworks is not the feature set. It is timing: you registered a component after the framework started, or you started it before the DOM was ready, and now you are reading changelogs about lifecycle events.
Summit.data("dropdown", (open = false) => ({
open,
toggle() { this.open = !this.open },
init() { /* runs before the component renders */ },
}));
Summit.store("theme", { dark: false, toggle() { this.dark = !this.dark } });
Summit.directive("uppercase", (el, meta, utils) => {
utils.effect(() => {
el.textContent = String(utils.evaluate(meta.expression)).toUpperCase();
});
});
Registration in Summit is timing-safe by construction. Built-ins register at
import, your registrations override them, and Summit.start() can be
called whenever you like. There is no load-order event to miss, because there is
no load-order event.
$watch got the same treatment. It returns an unwatch function, and
it only fires on an actual change, which means mutating the watched value inside
its own callback cannot spin into an infinite loop. That is not a clever
feature. It is a bug I did not want to debug twice.
Written to be written by a machine
The stated goal of the project is that an AI agent can write a working, good-looking frontend on the first try. Every decision above serves that, but three do it directly.
A closed vocabulary. A small fixed set of s-
directives and $ magics, with one obvious way to do most things.
Models are excellent at reproducing a pattern they have seen and unreliable at
choosing between six near-equivalent idioms. Removing the choice removes the
failure.
Safety by construction, not by review. Generated markup runs through the interpreter under a strict CSP. The guardrail holds whether or not anyone read the diff, which is the only kind of guardrail worth having.
Discoverable in one fetch. The docs publish
llms.txt as an index and llms-full.txt as the entire
corpus in markdown, every page is available as markdown by appending
index.md to its URL, and AGENTS.md is a brief you can
hand to your own agent. An agent should not have to scrape rendered HTML to
learn an API. That is a solved problem that most projects have not bothered to
solve.
What it is not
Summit is not a replacement for React on an application with deep client-side routing, server components, or a large team that needs the component model as an organisational boundary as much as a technical one. If your frontend is a real application, use a real application framework.
It is for the very large category of pages that are mostly server-rendered HTML with pockets of behaviour: marketing sites, dashboards, admin surfaces, documentation, internal tools, and anything where the build step costs more than the interactivity is worth. That category is bigger than the industry admits, and it has been paying application-framework prices for years.
The framework is MIT, on npm as summitjs, and one script tag from a
CDN if you would rather skip npm entirely. It is sponsored by NodeMaven. The
source is at
github.com/velofy/summitjs and
the docs, including the component library, are at
velofy.github.io/summitjs.