SvenJS 3

SvenJS 3

One HTML file. A button. State. That’s it.

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.

unpkg.com/svenjs@3 · script tag · html`…`

The Click App

You have clicked 0 times.

hello.html — open in a browser, no npm
<!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>

No build

One <script src> from a CDN. Open the file from disk. That is the whole toolchain.

State you can see

setState replaces the object. Copy the fields you keep. Accidental mutation throws in development.

Download from the playground

Tweak the example, hit Download .html, mail the file to yourself. It still runs.

Demos