elit
v3.4.5
Published
Optimized lightweight library for creating DOM elements with reactive state
Maintainers
Readme
Elit
Elit is a TypeScript toolkit for building browser UIs, dev servers, SSR pages, tests, desktop WebView apps, and small file-backed backends from one package.
The package is split by runtime. Browser-facing APIs live in elit and the client subpaths, server APIs live in elit/server, desktop APIs live in elit/desktop, and the CLI is elit.
AI Quick Context
If you are generating or editing code for Elit, follow these rules first:
- Use
elitor the client subpaths for browser UI code. - Use
elit/serverfor HTTP routes, middleware, dev server, preview server, and server-side shared state. - Use
elit/desktoponly insideelit desktop ...runtime. Those APIs are injected by the native desktop runtime and are not normal browser globals. - Use
elit/buildfor programmatic bundling. - Use
elit/databasefor the VM-backed file database helpers. - Do not use
elit-server. Old docs may mention it, but the current package export iselit/server. - Prefer subpath imports in generated code. They make environment boundaries obvious.
createRouterView(router, options)returns a function. Render it insidereactive(router.currentRoute, () => RouterView()).- Browser-facing code may import local
.tsfiles during development. Elit rewrites those imports for browser output. - Config files can be
elit.config.ts,elit.config.mts,elit.config.js,elit.config.mjs,elit.config.cjs, orelit.config.json. - Environment files are loaded in this order:
.env.{mode}.local,.env.{mode},.env.local,.env. - Only
VITE_variables are injected into client bundles.
Install
Create a new app with the scaffold:
npm create elit@latest my-app
cd my-app
npm install
npm run devOther package managers:
yarn create elit my-app
pnpm create elit my-app
bun create elit my-app
deno run -A npm:create-elit my-appManual install:
npm install elitIf you want desktop mode, install Cargo as well. The native desktop runtime is built with Rust.
Module Map
Use this table as the import map for generated code.
| Import | Use it for | Main exports |
| --- | --- | --- |
| elit | Client-side all-in-one entry | DOM helpers, element factories, state, styles, router, HMR |
| elit/dom | DOM renderer and SSR string rendering | dom, render, renderToString, mount |
| elit/el | HTML, SVG, and MathML element factories | div, button, html, body, script, and many more |
| elit/state | Reactive state and render helpers | createState, computed, reactive, text, bindValue, bindChecked, createSharedState |
| elit/style | CSS generation and injection | CreateStyle, styles, renderStyle, injectStyle, addClass, addTag |
| elit/router | Client-side routing | createRouter, createRouterView, routerLink |
| elit/server | HTTP router, dev server, middleware, shared server state | ServerRouter, createDevServer, cors, logger, rateLimit, compress, security, StateManager |
| elit/build | Programmatic build API | build |
| elit/desktop | Native desktop window APIs | createWindow, createWindowServer, onMessage, windowQuit, windowSetTitle, windowEval |
| elit/database | VM-backed file database | Database, create, read, save, update, rename, remove |
| elit/test | Test runner module entry | test runtime helpers used by the CLI |
Advanced subpaths also exist for lower-level adapters and internals: elit/http, elit/https, elit/ws, elit/wss, elit/fs, elit/path, elit/mime-types, elit/chokidar, elit/runtime, elit/test-runtime, elit/test-reporter, and elit/types.
Fastest Working App
This is the smallest browser app that works with the current CLI.
Project structure:
my-app/
index.html
src/
main.tssrc/main.ts
import { div, h1, button } from 'elit/el';
import { createState, reactive } from 'elit/state';
import { render } from 'elit/dom';
const count = createState(0);
const app = div(
{ className: 'app' },
h1('Hello Elit'),
reactive(count, (value) =>
button({ onclick: () => count.value++ }, `Count: ${value}`)
)
);
render('app', app);index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Elit App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Run it:
npx elit devNotes:
render('app', app)andrender('#app', app)both work.- During development you can point the browser to
/src/main.tsdirectly. - For production builds, your copied HTML should point to the built asset path such as
/main.js.
CLI
Main commands:
npx elit dev
npx elit build --entry ./src/main.ts --out-dir dist
npx elit preview
npx elit test
npx elit desktop ./src/main.ts
npx elit desktop build ./src/main.ts --release
npx elit wapk pack .
npx elit wapk run ./app.wapk
npx elit desktop wapk run ./app.wapkUseful flags:
elit dev --port 3000 --host 0.0.0.0 --no-openelit build --entry ./src/main.ts --out-dir dist --format esm --sourcemapelit preview --root dist --base-path /appelit test --watchelit test --file ./testing/unit/database.test.tselit test --describe "Database"elit test --it "saves records"elit test --coverage --coverage-reporter text,htmlelit desktop --runtime quickjs|node|bun|denoelit desktop build --platform windows|linux|macos --out-dir distelit desktop build --compiler auto|none|esbuild|tsx|tsupelit wapk ./app.wapk --runtime node|bun|denoelit wapk run ./app.wapk --sync-interval 100 --watcherelit wapk pack . --include-depselit wapk inspect ./app.wapkelit wapk extract ./app.wapkelit desktop wapk ./app.wapk --runtime node|bun|deno --watcher
Desktop mode notes:
- Cargo is required the first time the native runtime is built.
- TypeScript entries are transpiled automatically when needed.
- Desktop build can prebuild the native runtime even without an entry file.
tsxcompiler mode is Node-only and keeps loading the original source tree instead of bundling it.tsxandtsupcompiler modes require those packages to be installed in the project.- Desktop icon support includes
.ico,.png, and.svg. - Desktop build auto-detects
icon.*andfavicon.*in the entry directory, project directory, and siblingpublic/folders.
WAPK mode notes:
elit wapk <file.wapk>andelit wapk run <file.wapk>run packaged apps.elit desktop wapk <file.wapk>andelit desktop wapk run <file.wapk>run packaged apps in desktop mode.- During run, the archive is expanded into a temporary work directory and changes are synced back to the same
.wapkfile. - Use
--sync-interval <ms>for polling mode, or--watcher/--use-watcherfor event-driven sync. - Configure package metadata in
elit.config.*underwapk. - See the full example app at
examples/wapk-example.
Config File
Elit loads one of these files from the project root:
elit.config.tselit.config.mtselit.config.jselit.config.mjselit.config.cjselit.config.json
The config shape is:
{
dev?: DevServerOptions;
build?: BuildOptions | BuildOptions[];
preview?: PreviewOptions;
test?: TestOptions;
desktop?: {
runtime?: 'quickjs' | 'node' | 'bun' | 'deno';
compiler?: 'auto' | 'none' | 'esbuild' | 'tsx' | 'tsup';
release?: boolean;
outDir?: string;
platform?: 'windows' | 'linux' | 'macos';
wapk?: {
runtime?: 'node' | 'bun' | 'deno';
syncInterval?: number;
useWatcher?: boolean;
release?: boolean;
};
};
wapk?: {
name?: string;
version?: string;
runtime?: 'node' | 'bun' | 'deno';
entry?: string;
scripts?: Record<string, string>;
port?: number;
env?: Record<string, string | number | boolean>;
desktop?: Record<string, unknown>;
};
}Example:
import { api } from './src/server';
import { documentShell } from './src/document';
export default {
dev: {
port: 3003,
host: '0.0.0.0',
open: false,
logging: true,
clients: [
{
root: '.',
basePath: '',
ssr: () => documentShell,
api,
},
],
},
build: [
{
entry: './src/main.ts',
outDir: './dist',
outFile: 'main.js',
format: 'esm',
sourcemap: true,
copy: [
{ from: './public/index.html', to: './index.html' },
],
},
],
preview: {
root: './dist',
index: './index.html',
port: 4173,
},
test: {
include: ['testing/unit/**/*.test.ts'],
},
desktop: {
runtime: 'quickjs',
compiler: 'auto',
release: false,
outDir: 'dist',
platform: 'windows',
wapk: {
runtime: 'bun',
syncInterval: 150,
useWatcher: true,
release: true,
},
},
wapk: {
name: 'my-app',
version: '1.0.0',
runtime: 'bun',
entry: './src/server.ts',
port: 3000,
env: {
NODE_ENV: 'production',
},
},
};Important details:
buildmay be a single object or an array. If it is an array, all builds run sequentially.dev.clientsis the most flexible setup when you want SSR, API routes, multiple apps, or per-client proxy rules.previewsupports the same concepts asdev: multiple clients, API routes, proxy rules, workers, SSR, and HTTPS.- Only
VITE_variables are exposed to client code during bundling. desktopconfig provides defaults forelit desktop,elit desktop build, andelit desktop wapk.wapkconfig is loaded fromelit.config.*, then package metadata is used as fallback.wapk runanddesktop wapk runsync runtime file changes back into the same.wapkarchive.
Browser Patterns
Elements and State
import { div, input, button, span } from 'elit/el';
import { createState, computed, reactive, bindValue } from 'elit/state';
import { render } from 'elit/dom';
const name = createState('Elit');
const count = createState(0);
const label = computed([name, count], (currentName, currentCount) => {
return `${currentName}: ${currentCount}`;
});
const app = div(
input({ type: 'text', ...bindValue(name) }),
reactive(label, (value) => span(value)),
button({ onclick: () => count.value++ }, 'Increment')
);
render('app', app);Router
import { div, nav } from 'elit/el';
import { reactive } from 'elit/state';
import { createRouter, createRouterView, routerLink } from 'elit/router';
const routerOptions = {
mode: 'history' as const,
routes: [
{ path: '/', component: () => div('Home') },
{ path: '/about', component: () => div('About') },
{ path: '/post/:id', component: (params: Record<string, string>) => div(`Post ${params.id}`) },
],
notFound: () => div('404'),
};
const router = createRouter(routerOptions);
const RouterView = createRouterView(router, routerOptions);
const app = div(
nav(
routerLink(router, { to: '/' }, 'Home'),
routerLink(router, { to: '/about' }, 'About')
),
reactive(router.currentRoute, () => RouterView())
);Styling
import { CreateStyle } from 'elit/style';
const css = new CreateStyle();
css.addClass('app', {
minHeight: '100vh',
display: 'grid',
placeItems: 'center',
fontFamily: 'system-ui, sans-serif',
});
css.addClass('button', {
padding: '12px 18px',
borderRadius: '12px',
border: '1px solid #222',
background: '#111',
color: '#fff',
cursor: 'pointer',
});
css.addPseudoClass('hover', {
opacity: 0.92,
}, '.button');
css.inject('app-styles');You can also use the shared singleton export:
import styles from 'elit/style';Server Patterns
Server Router
import { ServerRouter, cors, logger } from 'elit/server';
export const api = new ServerRouter();
api.use(cors());
api.use(logger());
api.get('/api/hello', async (ctx) => {
ctx.res.json({ message: 'Hello from Elit' });
});
api.post('/api/echo', async (ctx) => {
ctx.res.json({ body: ctx.body });
});ServerRouter supports both Elit-style handlers and Express-like handlers:
async (ctx) => { ... }async (req, res) => { ... }- middleware with
use(middleware)oruse('/prefix', middleware)
Programmatic Dev Server
import { createDevServer } from 'elit/server';
const server = createDevServer({
port: 3000,
root: '.',
open: false,
logging: true,
});
console.log(server.url);Shared State Between Server and Client
Client:
import { createSharedState } from 'elit/state';
const counter = createSharedState('counter', 0);
counter.value++;Server:
import { createDevServer } from 'elit/server';
const server = createDevServer({ root: '.', open: false });
const counter = server.state.create('counter', { initial: 0 });
counter.value = 10;
counter.update((value) => value + 1);The client createSharedState() connects over WebSocket to the current host unless you pass a custom wsUrl.
SSR Document Shell
For SSR-style setups, export a document shell and return it from dev.clients[].ssr.
import { html, head, body, title, meta, div, script } from 'elit/el';
export const documentShell = html(
head(
title('My Elit App'),
meta({ charset: 'UTF-8' }),
meta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' })
),
body(
div({ id: 'app' }),
script({ type: 'module', src: '/src/main.ts' })
)
);For production builds, make sure your built HTML points at the production asset path such as /main.js.
Desktop Mode
Desktop mode runs an Elit entry file inside a native WebView shell.
Example desktop entry:
import { createWindow, onMessage, windowQuit, windowSetTitle } from 'elit/desktop';
onMessage((message) => {
if (message === 'desktop:ready') {
windowSetTitle('Elit Desktop');
windowQuit();
}
});
createWindow({
title: 'Elit Desktop',
width: 960,
height: 640,
icon: './public/favicon.svg',
html: `<!doctype html>
<html lang="en">
<body>
<main>Hello from Elit Desktop</main>
<script>
window.addEventListener('DOMContentLoaded', () => {
window.ipc.postMessage('desktop:ready');
});
</script>
</body>
</html>`,
});Run it:
npx elit desktop ./src/main.tsBuild a standalone executable:
npx elit desktop build ./src/main.ts --releaseDesktop notes:
- Runtime choices:
quickjs,node,bun,deno - Transpiler choices:
auto,none,esbuild,tsx,tsup tsxis a Node loader mode, not a bundle mode. Useesbuildortsupwhen you want a relocatable output.- Icon input supports
.ico,.png, and.svg - EXE icon embedding and runtime window icon loading both support SVG now
createWindowServer(app, opts)is available when you want to run an HTTP app inside the desktop shell
Database
Elit includes a small VM-backed database helper that stores files under a directory and can execute TypeScript or JavaScript code against them.
import { Database } from 'elit/database';
const db = new Database({
dir: './databases',
language: 'ts',
});
db.create('users', `export const users = [];`);
await db.execute(`
import { users } from '@db/users';
console.log(users.length);
`);Useful methods:
create(dbName, code)read(dbName)save(dbName, code)update(dbName, fnName, code)rename(oldName, newName)remove(dbName, fnName?)
Programmatic Build API
If you want to bundle from code instead of the CLI:
import { build } from 'elit/build';
await build({
entry: './src/main.ts',
outDir: './dist',
outFile: 'main.js',
format: 'esm',
sourcemap: true,
copy: [
{ from: './public/index.html', to: './index.html' },
],
});Testing
CLI test runner examples:
npx elit test
npx elit test --watch
npx elit test --file ./testing/unit/database.test.ts
npx elit test --describe "Database"
npx elit test --it "should save data"
npx elit test --coverage --coverage-reporter text,htmlThe package also exports elit/test, elit/test-runtime, and elit/test-reporter for advanced use, but most users should stay on the CLI.
Good Defaults For Generated Code
When writing new Elit code, these defaults are usually correct:
- Prefer
elit/el,elit/state, andelit/domin new examples. - Keep server code on
elit/serverand browser code on client subpaths. - Use
ServerRouterfor APIs instead of inventing another HTTP layer first. - Use
createStateplusreactivefor UI updates before adding abstractions. - Use
CreateStyleorelit/stylefor injected CSS. - Use
elit.config.tswhen the project needs SSR, APIs, preview customization, workers, or proxy rules. - Use
examples/correct-configas the reference for a minimal full-stack setup.
Repository Guide
If you are working in this repository, these locations matter most:
src/index.ts: root client exportssrc/dom.ts: renderer and SSR string renderingsrc/state.ts: reactive state, bindings, shared state clientsrc/router.ts: client routersrc/style.ts: CSS generatorsrc/server.ts: dev server,ServerRouter, middleware, shared state serversrc/build.ts: bundler APIsrc/desktop.ts: desktop runtime bindings exposed to TypeScriptsrc/desktop-cli.ts:elit desktopimplementationsrc/database.ts: database VM and helpersexamples/correct-config: minimal full-stack referenceexamples/full-db: larger full-stack example with database usageexamples/desktop-example.ts: desktop smoke test and runtime examplepackages/create-elit: scaffold templates used bynpm create elit@latestdocs/: the documentation site built with Elit itself
Known Boundaries
- Root
elitexport is client-oriented. Non-client features are on subpaths. - Desktop APIs only exist inside the desktop runtime.
- Client env injection is limited to
VITE_variables. - Production HTML copying is explicit. If you copy
index.html, make sure it points at the built JS file, not the dev-only/src/*.tspath.
Next Reads
examples/correct-configfor the cleanest SSR + API setupexamples/desktop-example.tsfor desktop runtime usageUSAGE_EXAMPLES.mdfor more import combinationsdocs/API.mdfor broader API detail
