A signals-based frontend framework with no virtual DOM, zero external dependencies, and TypeScript-first APIs.
- No Virtual DOM — Direct, surgical DOM updates via Signals and Effects
- Zero-Flicker Architecture — Components render only when async data is fully loaded
- Fine-Grained Reactivity — Automatic dependency tracking, no manual subscriptions
- JSX Syntax — Familiar developer experience with build-time optimization
- Plugin System — First-class
AppBuilder.use()API for router, modals, queries and more - Type-Safe by Default — Full TypeScript, strict mode, no
anyin public APIs - Zero External Dependencies — Every package has zero runtime deps
| Package | Version | Size (gzip) | Tests | Description |
|---|---|---|---|---|
| liteforge | 0.7.0 | ~18kb | — | Umbrella package — re-exports @liteforge/core + @liteforge/runtime |
| @liteforge/core | 0.1.0 | ~6kb | 89 | Reactive primitives: signal, computed, effect, batch |
| @liteforge/runtime | 0.6.2 | ~12kb | 285 | Components, lifecycle, control flow, plugin system |
| @liteforge/store | 0.1.0 | ~5kb | 128 | State management with registry and time-travel |
| @liteforge/router | 0.8.0 | ~20kb | 424 | Routing with guards, lazy loading, typed routes, view transitions |
| @liteforge/query | 3.0.0 | ~5kb | 78 | Data fetching with caching and mutations |
| @liteforge/form | 0.1.0 | ~4kb | 54 | Form management with Zod validation |
| @liteforge/table | 0.2.0 | ~8kb | 72 | Data tables with sorting, filtering, pagination |
| @liteforge/calendar | 0.3.0 | ~22kb | 202 | Scheduling calendar with drag & drop and 4 views |
| @liteforge/client | 3.0.0 | ~8kb | 76 | TypeScript-first HTTP client with interceptors and CRUD resources |
| @liteforge/modal | 3.1.0 | ~4kb | 30 | Modal system with focus trap, transitions, and promise presets |
| @liteforge/toast | 2.0.0 | ~3kb | 39 | Imperative toast notifications with four variants |
| @liteforge/tooltip | 0.2.1 | ~2kb | 28 | Portal-based tooltips with auto-positioning and delay |
| @liteforge/i18n | 2.1.0 | ~3kb | 53 | Signals-based i18n with lazy locales, interpolation, pluralization, typed keys |
| @liteforge/vite-plugin | 0.4.3 | ~15kb | 350 | JSX transform and build optimization |
| @liteforge/devtools | 3.0.0 | ~16kb | 51 | Debug panel with 5 tabs and time-travel |
1,959 tests across all packages.
liteforge — umbrella (re-exports core + runtime)
│
core (no deps)
├── runtime — components, JSX, control flow, plugin system
│
plugins (each installed via .use()):
├── store — global state
├── router — client-side routing
├── query — data fetching
├── form — form management
├── table — data tables
├── calendar — scheduling calendar
├── client — HTTP client
├── modal — modal system
├── toast — toast notifications
├── tooltip — tooltip system
└── i18n — internationalization
vite-plugin — standalone build transform
devtools — depends on core + store
npm install liteforge @liteforge/vite-pluginvite.config.ts
import { defineConfig } from 'vite'
import liteforge from '@liteforge/vite-plugin'
export default defineConfig({
plugins: [liteforge()]
})main.tsx
import { createApp } from 'liteforge';
import { routerPlugin } from '@liteforge/router';
import { App } from './App.js';
await createApp({ root: App, target: '#app' })
.use(routerPlugin({ routes: [...] }))
.mount();import { signal, computed, effect } from 'liteforge'
const count = signal(0)
const doubled = computed(() => count() * 2)
effect(() => {
console.log(`Count: ${count()}, doubled: ${doubled()}`)
})
count.set(5) // → "Count: 5, doubled: 10"
count.update(n => n + 1)import { createComponent } from 'liteforge'
const UserProfile = createComponent({
async load({ props }) {
const user = await fetch(`/api/users/${props.userId}`).then(r => r.json())
return { user }
},
placeholder: () => <div class="skeleton" />,
component: ({ data }) => (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
),
})Plugins are installed via AppBuilder.use() before mounting. Each plugin provides typed services that are accessible via use() inside components.
import { createApp } from 'liteforge';
import { routerPlugin } from '@liteforge/router';
import { modalPlugin } from '@liteforge/modal';
import { queryPlugin } from '@liteforge/query';
import { i18nPlugin } from '@liteforge/i18n';
import { devtoolsPlugin } from '@liteforge/devtools';
await createApp({ root: App, target: '#app' })
.use(routerPlugin({ routes: [...] }))
.use(modalPlugin())
.use(queryPlugin())
.use(i18nPlugin({ defaultLocale: 'en', load: (locale) => import(`./locales/${locale}.js`) }))
.use(devtoolsPlugin())
.mount();Inside components, access plugins via the typed use() function:
const MyPage = createComponent({
component({ use }) {
const router = use('router') // typed as Router
const modal = use('modal') // typed as ModalApi
return (
<button onclick={() => router.push('/home')}>Go home</button>
)
}
})import { routerPlugin } from '@liteforge/router'
.use(routerPlugin({
history: 'browser', // 'browser' | 'hash' | 'memory'
scrollBehavior: 'top',
routes: [
{ path: '/', component: Home },
{ path: '/users/:id', component: () => import('./UserDetail.js') },
{
path: '/admin',
component: AdminLayout,
guard: 'auth',
children: [{ path: '/', component: Dashboard }],
},
],
}))import { defineStore } from '@liteforge/store'
const userStore = defineStore('users', {
state: { currentUser: null, list: [] },
getters: (state) => ({
isLoggedIn: () => state.currentUser() !== null,
}),
actions: (state) => ({
async fetchUsers() {
state.list.set(await fetch('/api/users').then(r => r.json()))
},
}),
})import { createQuery, createMutation } from '@liteforge/query'
const users = createQuery({
key: 'users',
fn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000,
})
users.data() // Signal<User[]>
users.isLoading() // Signal<boolean>
users.refetch()
const addUser = createMutation({
fn: (data) => api.createUser(data),
invalidate: ['users'],
})import { createClient } from '@liteforge/client'
const client = createClient({ baseUrl: 'https://api.example.com' })
// Low-level
const todo = await client.get<Todo>('/todos/1')
// Resource-based CRUD
const posts = client.resource<Post>('posts')
await posts.getList({ page: 1, pageSize: 20 })
await posts.getOne(42)
await posts.create({ title: 'Hello', body: '...' })
await posts.update(42, { title: 'Updated' })
await posts.delete(42)
// Interceptors
client.addInterceptor({
onRequest: (config) => ({ ...config, headers: { ...config.headers, Authorization: `Bearer ${token}` } }),
onResponseError: (error) => { if (error.status === 401) redirect('/login'); throw error; },
})import { createModal, confirm, alert } from '@liteforge/modal'
// Declarative
const dialog = createModal({
title: 'Edit User',
content: () => <EditUserForm />,
})
dialog.open()
// Promise presets
const confirmed = await confirm({ title: 'Delete?', message: 'This cannot be undone.' })
await alert({ title: 'Done', message: 'User deleted.' })import { toast } from '@liteforge/toast'
toast.success('Saved successfully')
toast.error('Something went wrong')
toast.warning('Unsaved changes')
toast.info('New version available')
// With options
toast.success('User created', { duration: 4000, position: 'bottom-right' })import { tooltip } from '@liteforge/tooltip'
// Imperative — attach to any element
const cleanup = tooltip(buttonEl, {
content: 'Save changes',
position: 'top', // 'top' | 'bottom' | 'left' | 'right'
delay: 300, // ms before showing
showWhen: () => !isMobile(), // reactive guard
})
// Declarative — JSX component
import { Tooltip } from '@liteforge/tooltip'
<Tooltip content="Save changes" position="top">
<button>Save</button>
</Tooltip>npm install @liteforge/i18n// main.tsx
import { i18nPlugin } from '@liteforge/i18n';
await createApp({ root: App, target: '#app' })
.use(i18nPlugin({
defaultLocale: 'en',
fallbackLocale: 'en', // used for missing keys
load: async (locale) => {
const mod = await import(`./locales/${locale}.js`);
return mod.default; // plain TranslationTree object
},
persist: true, // saves locale to localStorage
storageKey: 'my-locale', // default: 'lf-locale'
}))
.mount();// locales/en.ts
export default {
greeting: 'Hello, {name}!',
nav: { home: 'Home', settings: 'Settings' },
items: '{count} item | {count} items', // 2-part: singular | plural
messages: 'No messages | {count} message | {count} messages', // 3-part: zero | one | many
} satisfies TranslationTree;// Inside a component
const MyPage = createComponent({
component({ use }) {
const { t, locale, setLocale } = use('i18n');
return (
<div>
<p>{() => t('greeting', { name: 'World' })}</p>
<p>{() => t('items', { count: count() }, count())}</p>
<p>{() => t('nav.home')}</p>
<button onclick={() => setLocale('de')}>🇩🇪 Deutsch</button>
<button onclick={() => setLocale('en')}>🇬🇧 English</button>
</div>
);
},
});Key properties:
- No re-render — only the text nodes that call
t()update on locale switch - Fallback locale — missing keys in the current locale transparently fall back
- Async plugin install — initial locale is fully loaded before the app mounts (no flash of untranslated keys)
- Dot-notation keys —
t('nav.home')traverses nested objects - Pipe pluralization —
2-part(singular|plural) or3-part(zero|one|many)
import { Show, For, Switch, Match } from 'liteforge'
<Show when={() => user()}>
<UserCard user={user} />
</Show>
<For each={() => items()}>
{(item) => <li>{item.name}</li>}
</For>
<Switch fallback={<NotFound />}>
<Match when={() => status() === 'loading'}>Loading...</Match>
<Match when={() => status() === 'error'}>Error!</Match>
</Switch># Install dependencies
pnpm install
# Build all packages
pnpm build:packages
# Run all tests (1,796 tests)
pnpm test
# Type check all packages
pnpm typecheck:all
# Run starter demo app
pnpm --filter starter dev
# Run docs app
pnpm --filter docs dev| Setup | Size (gzip) |
|---|---|
| Minimal (core only) | ~6kb |
| Core + Runtime | ~18kb |
| Core + Runtime + Store + Router | ~43kb |
| Full stack (+ query + client + modal + toast + tooltip) | ~65kb |
LiteForge is in active development. APIs may change between minor versions. I use it in my own production projects, but if you adopt it today, expect some rough edges.
LiteForge is a personal framework born from real frustration with React's re-rendering model and Vue's adapter overhead. I built it because I wanted a tool that works the way I think — signals that directly update the DOM, no virtual DOM diffing, no magic.
I'm actively building real applications on top of it (scheduling software, business tools, DevOps dashboards), so the framework evolves based on actual needs, not theoretical purity.
What works well today: Core reactivity, routing, state management, forms, data tables, calendar, HTTP client, modals, i18n, and a full plugin system — all battle-tested through my own projects.
What's still maturing: Documentation, edge cases in complex layouts, and the ecosystem around it.
If you find it useful, feel free to use it. If you find a bug, I'd appreciate an issue. PRs are welcome but please open an issue first.
I want to be transparent: LiteForge was developed with significant AI assistance. I used Claude (Anthropic) as a development partner throughout the entire process — from architecture decisions to implementation, testing, and documentation.
What that means in practice:
- I designed the API, made all architecture decisions, and defined what the framework should do
- AI helped write implementation code, tests, and documentation based on my specifications
- Every feature was reviewed, tested, and validated by me in real browser environments
- The framework reflects my opinions and preferences as a developer, not generic AI output
I believe AI-assisted development is the future of how software gets built. Being upfront about it is more honest than pretending otherwise. The code quality speaks for itself — 1,796 tests, TypeScript strict mode, zero external dependencies.
LiteForge is built and maintained by SchildW3rk — a one-person software studio from Salzburg, Austria.
MIT — see LICENSE for details.