No build
One <script src> from a CDN. Open the file from disk. That is the whole toolchain.
SvenJS 3
A tiny UI runtime you drop on a page. No Node, no Vite, no compiler. Tagged templates instead of JSX — save the file and double-click it.
You have clicked 0 times.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SvenJS</title>
<style>:root {
color-scheme: light dark;
--bg: light-dark(#f7f1ea, #141210);
--fg: light-dark(#1c1712, #f2ebe3);
--muted: light-dark(#6b6158, #a3988c);
--line: light-dark(color-mix(in oklab, #1c1712 12%, transparent), color-mix(in oklab, #f2ebe3 14%, transparent));
--accent: light-dark(#c45c20, #e07a3d);
--accent-fg: #fff;
--card: light-dark(#fffdf9, #1c1915);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
background: var(--bg);
color: var(--fg);
font: 16px/1.5 "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
}
#app { min-height: 100%; padding: 1.25rem; }
button, input, textarea {
font: inherit;
color: inherit;
}
button {
cursor: pointer;
background: var(--accent);
color: var(--accent-fg);
border: 0;
border-radius: 8px;
padding: 0.55rem 0.95rem;
}
button:hover { filter: brightness(1.05); }
input[type="text"] {
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.5rem 0.7rem;
}
.demo-card {
max-width: 28rem;
background: var(--card);
border: 1px solid var(--line);
border-radius: 12px;
padding: 1.25rem 1.4rem;
}
.demo-card h1, .demo-card h2, .demo-card h3 {
font-family: Newsreader, Georgia, serif;
font-weight: 650;
margin: 0 0 0.75rem;
}
.demo-card p { color: var(--muted); margin: 0.75rem 0 0; }
.todo-app { max-width: 32rem; margin: 0 auto; }
.todo-app h1 {
font-family: Newsreader, Georgia, serif;
font-weight: 650;
font-size: 2.4rem;
margin: 0 0 1rem;
letter-spacing: -0.03em;
}
.todo-list { list-style: none; margin: 1rem 0; padding: 0; }
.todo-list li {
display: flex;
gap: 0.6rem;
align-items: center;
padding: 0.55rem 0;
border-bottom: 1px solid var(--line);
}
.todo-list li.done label { color: var(--muted); text-decoration: line-through; }
.todo-list .destroy {
margin-left: auto;
background: transparent;
color: var(--muted);
padding: 0.2rem 0.45rem;
}
.todo-filters { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.todo-filters a {
color: var(--fg);
text-decoration: none;
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.2rem 0.7rem;
font-size: 0.85rem;
}
.todo-filters a.selected { border-color: var(--accent); color: var(--accent); }
.compose-grid { display: grid; gap: 0.75rem; }
.compose-grid > * {
background: var(--card);
border: 1px solid var(--line);
border-radius: 12px;
padding: 1rem 1.1rem;
}
.svenjs-credit {
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--ink, #312725);
background: var(--field, #fffdf8);
border: 1px solid var(--rule, #d9d0c0);
padding: 6px 14px 6px 6px;
margin-top: 1.5rem;
}
.svenjs-credit:hover {
border-color: var(--accent, #e07a3d);
}
.svenjs-mark {
display: block;
width: 36px;
height: 36px;
border-radius: 8px;
}
.svenjs-credit-copy {
display: flex;
flex-direction: column;
gap: 1px;
line-height: 1.15;
}
.svenjs-credit-kicker {
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted, #6a5e50);
}
.svenjs-credit-name {
font-size: 0.95rem;
font-weight: 600;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/svenjs@3.4.0"></script>
<script type="importmap">{"imports":{"svenjs":"data:text/javascript,export%20const%20%7B%20Fragment%2C%20create%2C%20createStore%2C%20flushSync%2C%20h%2C%20html%2C%20hydrate%2C%20jsx%2C%20jsxDEV%2C%20jsxs%2C%20render%2C%20renderToString%2C%20unmountRoot%2C%20version%20%7D%20%3D%20globalThis.Svenjs%3B%20export%20default%20globalThis.Svenjs.default%20%3F%3F%20globalThis.Svenjs%3B","svenjs/jsx-runtime":"data:text/javascript,export%20const%20%7B%20Fragment%2C%20create%2C%20createStore%2C%20flushSync%2C%20h%2C%20html%2C%20hydrate%2C%20jsx%2C%20jsxDEV%2C%20jsxs%2C%20render%2C%20renderToString%2C%20unmountRoot%2C%20version%20%7D%20%3D%20globalThis.Svenjs%3B%20export%20default%20globalThis.Svenjs.default%20%3F%3F%20globalThis.Svenjs%3B","svenjs/jsx-dev-runtime":"data:text/javascript,export%20const%20%7B%20Fragment%2C%20create%2C%20createStore%2C%20flushSync%2C%20h%2C%20html%2C%20hydrate%2C%20jsx%2C%20jsxDEV%2C%20jsxs%2C%20render%2C%20renderToString%2C%20unmountRoot%2C%20version%20%7D%20%3D%20globalThis.Svenjs%3B%20export%20default%20globalThis.Svenjs.default%20%3F%3F%20globalThis.Svenjs%3B"}}</script>
<script type="module">
const { create, render, html } = Svenjs;
const App = create({
initialState: { clicks: 0 },
render() {
return html`
<div class="demo-card">
<h1>The Click App</h1>
<button onClick=${() => this.setState({ clicks: this.state.clicks + 1 })}>
Why not click me?
</button>
<p>You have clicked ${this.state.clicks} times.</p>
</div>
`;
},
});
render(App, document.getElementById("app"));
</script>
<a class="svenjs-credit" href="https://svenjs.xyz/" rel="noopener noreferrer">
<svg class="svenjs-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 326 326" width="36" height="36" aria-hidden="true">
<rect width="326" height="326" rx="72" fill="#312725"/>
<g fill="#e07a3d" transform="translate(0 4)">
<polygon points="127,73 258,73 204,130 78,129"/>
<polygon points="78,129 121,129 172,184 131,184"/>
<polygon points="154,139 198,139 249,195 205,195"/>
<polygon points="116,193 249,195 199,250 66,250"/>
</g>
</svg>
<span class="svenjs-credit-copy">
<span class="svenjs-credit-kicker">UI built with</span>
<span class="svenjs-credit-name">SvenJS 3.4.0</span>
</span>
</a>
</body>
</html>
One <script src> from a CDN. Open the file from disk. That is the whole toolchain.
setState replaces the object. Copy the fields you keep. Accidental mutation throws in development.
Tweak the example, hit Download .html, mail the file to yourself. It still runs.
Demos