v3.0 alpha
don't use in production

Framework
built to
disappear.

Development stays

FS routing, reactivity, SSR, and more.
With Elegance you ship a runtime smaller than a JPEG.

~24xfaster than Next.JS
4KB runtime
0browser deps
MITlicense

on pages with 200K+ mounted components*

THE FEATURES

What it actually
does.

Not marketing. The real mechanics, from the filesystem to the client runtime.

01

Filesystem routing

pages/ is your router. A directory with a page.ts inside is a route, full stop. Just export a default function that resolves into some element calls.

/[slug] · /[...rest] · layouts · API routes
02

Atoms

State lives in atoms. Read one inside view() and just that component re-renders when the atom changes. Just a value with a .value.

Fine-grained · no vdom · auto-batched
03

Server-first components

Pages are always server-rendered, so you can run server-side code in your page, no API required. Server code is removed from the client bundle.

SSR by default · !no-bundle · dead code elimination
04

Static or live, per page

Build output is static HTML by default. Fast, cacheable and free to host anywhere. One export flips a page to build per-request. Mix them freely across the same project; your blog can be static while your dashboard is live.

export const isDynamic = true
05

Atom hydration

Whatever state your server computed gets serialized into the page and restored on the client automatically. No need to re-fetch data you already have.

SSR state → client · zero wiring
06

Nested layouts

Drop a layout.ts into any directory and it wraps every route beneath it. Layouts, just like pages, can be dynamic or static; you can even mix and match. They render outer to inner.

Nested · independent · per-layout isDynamic
07

API routes

Export GET, POST, PUT, DELETE, or PATCH from any route.ts and that route handles the request as a plain HTTP handler.

GET · POST · PUT · DELETE · PATCH
08

Slug routes

Name a directory [id] and the segment becomes a typed parameter. [...rest] catches everything that follows. Slug values are available as props inside the page constructor in that route.

/posts/[slug] · /files/[...path]
09

Middleware

Run code before any route resolves. Auth checks, redirects, request rewriting, header injection. Middleware composes cleanly and can short-circuit the response without touching the page at all.

Auth · redirects · headers · rewriting
HOW IT FITS TOGETHER

Three ideas.
One framework.

The entire mental model. Here's all of it.

Drop a directory into pages/ with a page.ts inside. That's a route! No config, no imports, no registration step. The filesystem is the router.

Layouts stack from layout.ts files walking up the directory tree, outermost-first. Each one can independently opt into server rendering. You never touch a router config.

concept 01 | Routes
pages/ ├── page.ts → / ├── layout.ts → wraps all routes below ├── about/ │ └── page.ts → /about ├── blog/ │ ├── page.ts → /blog │ ├── layout.ts → wraps /blog/* only │ └── [slug]/ │ └── page.ts → /blog/:slug └── api/users/ └── page.ts → /api/users (HTTP handlers)
TYPESCRIPT-FIRST

Prefer JSX syntax?
Use TSX.

Elegance supports both .ts and .tsx as first-class page formats. Tag functions JSX syntax get the same atoms, same routing and identical output.

Either way, you get full TypeScript inference end-to-end: props, atoms, and return types are all checked at compile time.

.ts.tsxidentical output · fully typed
page.ts
const Card = component<{ name: string }>({ atoms: { open: false }, view: (self, { open }) => article({ class: "card" }, h2({}, self.props.name), open.value && p({}, "Details…"), button({ onclick: () => open.value = !open.value }, open.value ? "Close ↑" : "Open ↓") ) });
or
page.tsx
const Card = component<{ name: string }>({ atoms: { open: false }, view: (self, { open }) => ( <article class="card"> <h2>{self.props.name}</h2> {open.value && <p>Details…</p>} <button onclick={() => open.value = !open.value} > {open.value ? "Close ↑" : "Open ↓"} </button> </article> ) });
BENCHMARKS

Built for
absurd speed.

Stress-tested against Next.JS on a page with 200,000 mounted components.

Elegance
28,037req/srequests per second
1.78msaverage latency
3.48msp99 latency
171Btransferred · Brotli
Next.JS
9.99req/srequests per second
5.38saverage latency
8.13sp99 latency
1.03MBtransferred · GZip
vs
2803x more requests/sec · 200K <p> elements
Full methodology & raw results
REAL CODE

Small surface.
Real projects.

Every concept above expressed as actual files you'd ship.

pages/blog/page.ts
// pages/blog/page.ts // The directory name is the route. No config, no registration. const PostList = component({ atoms: { posts: [] as Post[], loading: true, }, // init runs on the server, ideal for data that should be // present before the first byte of HTML is sent init: async (_, { posts, loading }) => { posts.value = await db.query( "SELECT * FROM posts ORDER BY created_at DESC" ); loading.value = false; }, view: (_, { posts, loading }) => loading.value ? div({ class: "skeleton" }) : ul({ class: "post-list" }, ...posts.value.map(post => li({}, a({ href: `/blog/${post.slug}` }, post.title), time({ datetime: post.date }, formatDate(post.date)) ) ) ) }); export default function BlogIndex() { return PostList(); }
01
Transparent preprocessing

OXC rewrites tag calls and assigns atom IDs in one fast pass. Nothing changes about how you write TypeScript, the transformation is an implementation detail, not a mental model you carry around.

02
Atom hydration, zero wiring

Atom values are serialized to JSON during SSR and restored on the client automatically. No manual hydration calls, no mismatch warnings, no extra round-trip for state you already fetched.

03
init server-side, onMount client-side

init is awaited during SSR, the right place for data fetching. onMount fires after the DOM is live. onUnmount fires on teardown. The full lifecycle fits on an index card.

GET STARTED

Two commands,
and Voilà.

Write a page.ts. Export a default function. Elegance handles the rest.

$ npx create-elegance-app $ npx elegance# dev server, hot-reload $ npx elegance-export# static export

no black box required!