FS routing, reactivity, SSR, and more.
With Elegance you ship a runtime smaller than a JPEG.
on pages with 200K+ mounted components*
Not marketing. The real mechanics, from the filesystem to the client runtime.
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 routesState 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-batchedPages 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 eliminationBuild 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 = trueWhatever 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 wiringDrop 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 isDynamicExport 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 · PATCHName 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]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 · rewritingThe 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.
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)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.
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 ↓") ) });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> ) });Stress-tested against Next.JS on a page with 200,000 mounted components.
<p> elementsEvery concept above expressed as actual files you'd ship.
// 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(); }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.
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.
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.
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 exportno black box required!