Getting Started
Vielzeug — a German-inspired name meaning roughly "many things" — is a collection of focused TypeScript packages. Each one solves a single problem. You pick what you need, import only what you use, and the rest never ships.
Zero external dependencies. Full tree-shaking. TypeScript-first throughout.
Try before you install
The REPL lets you run any package in the browser without installing anything.
End-to-end showcase
Looking for a full, multi-package example app? See Showcases and open the Kanban demo on StackBlitz.
What are you building?
Jump to the packages most relevant to your work.
| I'm building… | Start with |
|---|---|
| Reactive vanilla TS/JS | Ripple → Arsenal |
| Custom UI components or a design system | Ore → Refine → Ripple |
| A form-heavy application | Forge → Spell → Ripple |
| A full SPA | Wayfinder → Courier → Ripple |
| Complex async or event-driven logic | Flux → Clockwork → Ripple |
| Just utility functions | Arsenal |
| Real-time search UI (combobox, command palette) | Scout → Sourcerer |
| App with undo/redo (editor, design tool) | Ledger → Ripple |
| Global keyboard shortcuts or command palette | Keymap → Ledger |
Core
The packages most projects reach for first.
Ripple
Fine-grained reactive state — signals, computed values, effects, stores, and reactive scopes.
import { signal, computed, effect } from '@vielzeug/ripple';
const count = signal(0);
const doubled = computed(() => count.value * 2);
effect(() => console.log(doubled.value)); // runs whenever count changes
count.value++; // logs: 2Most other packages build on Ripple. Learn it first.
Ore
Custom element authoring — reactive templates, signals, slots, and automatic lifecycle management.
import { define, html } from '@vielzeug/ore';
import { signal } from '@vielzeug/ripple';
define('my-counter', () => {
const count = signal(0);
return html`<button @click=${() => count.value++}>Clicked ${count} times</button>`;
});Arsenal
100+ tree-shakeable utilities — array, object, string, async, math, cache, and more. Nothing you don't import, nothing you pay for.
import { debounce, groupBy, clamp, isEqual } from '@vielzeug/arsenal';
const search = debounce(fetchResults, 300);
const byRole = groupBy(users, (u) => u.role);
const clamped = clamp(value, 0, 100);Common
Packages for the tasks that come up in most real applications.
Refine
Accessible, themeable web components built on Ore — buttons, inputs, modals, and more.
<ore-button variant="solid" color="primary">Save</ore-button>
<ore-input label="Email" type="email"></ore-input>Drop them straight into any project. No framework required.
Forge
Immutable typed form state with focused nested fields, whole-value validation, and deterministic submission.
import { createForm } from '@vielzeug/forge';
const form = createForm({
initialValues: { email: '', age: 0 },
validate: (value) => ({
fields: {
age: value.age >= 18 ? undefined : 'Must be an adult',
email: value.email.includes('@') ? undefined : 'Invalid email',
},
}),
});
form.field('email').set('ada@example.com');
await form.submit((value) => fetch('/api/users', { method: 'POST', body: JSON.stringify(value) }));Pairs with Spell — one schema for both form and API validation.
Spell
Schema-first validation and parsing — runtime type checking, coercion, and custom refinements.
import { s } from '@vielzeug/spell';
const schema = s.object({
name: s.string().min(2).max(50),
email: s.string().email(),
age: s.number().min(18).optional(),
});
const result = schema.safeParse(input);
if (!result.success) console.log(result.error.issues);Courier
HTTP client with explicit cached reads, direct mutations, SSE streaming, and interceptors.
import { createCourier } from '@vielzeug/courier';
const courier = createCourier({ baseUrl: '/api', query: { staleTime: 5_000 } });
const key = ['user', id] as const;
await courier.queries.fetch({
key,
fetch: ({ signal }) => courier.get<User>(`/users/${id}`, { signal }),
});
await courier.patch(`/users/${id}`, { body: { name: 'Alice' } });
courier.queries.invalidate(key);Vault
Storage adapter for IndexedDB and localStorage — TTL expiration, reactive signals, schema validation, and a query builder.
import { table } from '@vielzeug/vault';
import { createLocalStorage } from '@vielzeug/vault/local-storage';
type User = { id: string; name: string; role: string };
const db = createLocalStorage({ name: 'myapp', schema: { users: table<User>('id') } });
await db.put('users', { id: '1', name: 'Alice', role: 'admin' });
const admins = await db.query('users').equals('role', 'admin').toArray();Wayfinder
Typed client-side router — guards, middleware, history management, and nested routes.
import { createMemoryHistory, createRouter } from '@vielzeug/wayfinder';
const router = createRouter({
history: createMemoryHistory('/'),
routes: {
home: { component: HomePage, path: '/' },
userDetail: {
component: UserPage,
data: async ({ params }) => fetchUser(params.id),
path: '/users/:id',
},
},
notFound: { component: NotFoundPage },
});
await router.ready;
router.subscribe((state) => render(state.matches.at(-1)?.component));Specialized
Reach for these when the problem calls for them.
Clockwork
Typed finite state machines — pure transitions, plain readonly actor snapshots, owned actors, timers, and async invokes.
import { defineMachine } from '@vielzeug/clockwork';
type AuthEvent = { type: 'LOGIN'; token: string } | { type: 'LOGOUT' };
const authMachine = defineMachine<{ token: string }, AuthEvent>()({
initial: 'idle',
context: { token: '' },
states: {
idle: {
on: {
LOGIN: { reduce: ({ event }) => ({ token: event.token }), target: 'active' },
},
},
active: { on: { LOGOUT: { reduce: () => ({ token: '' }), target: 'idle' } } },
},
});
const auth = authMachine.createActor();
auth.send({ type: 'LOGIN', token: 'abc123' });
console.log(auth.snapshot.state); // 'active'
auth.dispose();Flux
Composable push streams with explicit buffering and adapters for Vielzeug primitives.
import { debounce, filter, map, pipe } from '@vielzeug/flux';
import { createChannel } from '@vielzeug/flux/subjects';
const subject = createChannel<string>();
pipe(
subject.stream,
filter((query) => query.length > 1),
debounce({ for: 300 }),
map((query) => query.toLowerCase().trim()),
).subscribe(console.log);
subject.send('hello'); // after 300 ms: helloWard
Role-based access control — typed permissions, wildcard patterns, and composable predicates.
import { createWard, owns } from '@vielzeug/ward';
const ward = createWard([
{ role: 'admin', resource: 'posts', action: '*', effect: 'allow' },
{ role: 'user', resource: 'posts', action: 'create', effect: 'allow' },
{ role: 'user', resource: 'posts', action: 'update', effect: 'allow', when: owns('authorId') },
]);
if (!ward.can(currentUser, 'posts', 'delete')) throw new ForbiddenError();Conduit
Lightweight dependency injection — singletons, transient instances, factories, and named scopes.
import { createContainer, token } from '@vielzeug/conduit';
const LoggerToken = token<Logger>('Logger');
const ApiToken = token<ApiService>('ApiService');
const container = createContainer();
container.factory(LoggerToken, [], () => new ConsoleLogger());
container.factory(ApiToken, [LoggerToken], (logger) => new ApiService(logger));
const api = await container.resolve(ApiToken);Other Packages
| Package | What it does |
|---|---|
| Prism | Reactive SVG charts — line, bar, area, pie, and sparkline with signal-driven updates |
| Orbit | Floating element positioning for tooltips, dropdowns, menus, and popovers |
| Scroll | Virtual list engine with variable-height rows, smooth scrolling, and zero layout thrash |
| Dnd | Framework-agnostic drag-and-drop with sortable lists, file-drop zones, and MIME filtering |
| Scout | Trigram fuzzy search with per-field weights, reactive query state, and match highlighting |
| Sourcerer | Typed data-source adapter for pagination, filtering, sorting, search, and infinite scroll |
| Pulse | Typed WebSocket client with channel multiplexing, presence tracking, and auto-reconnect |
| Lingua | Typed translations, pluralization, lazy locale catalogs, and SSR support |
| Herald | Typed event bus — pub/sub with namespaces, wildcards, and once-listeners |
| Keymap | Headless keyboard shortcut manager with chord sequences, modifier aliases, and context guards |
| Ledger | Async undo/redo command history with Ripple signals for reactive canUndo/canRedo state |
| Rune | Structured logging with scoped loggers, pluggable transports, and log levels |
| Familiar | Typed Web Worker pool with task queuing, streaming, and AbortSignal cancellation |
| Tempo | Date and time utilities — timezone conversion, DST-safe arithmetic, and Intl formatting |
| Coins | Bigint-based monetary arithmetic with currency formatting and rounding policies |
| Assay | Framework-agnostic DOM testing primitives — scoped queries, event dispatch, and async waiting |
Packages That Work Well Together
| Combination | Why |
|---|---|
| Ripple + Ore | Ore templates are powered by Ripple signals — same reactive primitives, zero glue |
| Spell + Forge | Pass a Spell schema as a field validator — one schema for both form and API |
| Courier + Ripple | Fetch with caching, push results into a signal for reactive rendering |
| Vault + Courier | Persist query results in IndexedDB for offline-capable apps |
| Ward + Wayfinder | Check permissions in router middleware before the route handler runs |
| Conduit + Rune | Register a scoped logger per service in your DI container |
| Sourcerer + Courier | Use Courier as transport inside createPageSource for paginated collections with caching |
| Sourcerer + Wayfinder | Sync source query state (page, filters, sort) with the URL so links stay shareable |
| Scroll + Orbit | Render a virtualised list inside an Orbit-positioned dropdown for high-count comboboxes |
| Dnd + Scroll | Combine sortable drag handles with a virtual list for large reorderable datasets |
| Clockwork + Ripple | Clockwork state and context are signals — bind them directly to effects or UI templates |
| Clockwork + Ward | Call Ward predicates inside Clockwork guards to block unauthorized transitions |
| Clockwork + Herald | Publish state-change events to decouple multiple machines from each other |
| Flux + Ripple | fromSignal() / toSignal() bridge signals and streams — Ripple for state, Flux for pipelines |
| Flux + Herald | fromBus() / toBus() turn a Herald bus into a Flux stream and back |
| Flux + Courier | fromQuery() adapts Courier cache entries into stream pipelines |
| Flux + Pulse | fromPulse() / fromRoomPresence() convert Pulse WebSocket events and room presence into composable Flux streams |
| Scout + Ripple | createReactiveSearch() wraps the index in Ripple signals — query and results are reactive computed values |
| Scout + Sourcerer | toSearchMatcher() wires a Scout index into createLocalSource as its matcher |
| Keymap + Ledger | Wire ctrl+z / ctrl+shift+z to ledger.undo() / ledger.redo() with no boilerplate |
| Keymap + Herald | Publish shortcut events to a bus instead of calling handlers directly — decouples keyboard from logic |
| Ledger + Ripple | canUndo, canRedo, and isProcessing are Ripple Computed values — bind directly to UI templates |
Philosophy
One problem per package. Each package has a tight scope and does that one thing well. You pull in exactly what you need.
TypeScript first. Types are not bolted on. Everything is designed around inference — you rarely write a type annotation and you never reach for as any.
No magic. No proxies chasing object mutations, no decorators, no global singletons. If you want to know what a function does, reading it is enough.
Zero surprises. APIs follow consistent conventions: create* for factories, on* for subscriptions, safeParse for fallible operations. Learn one package and the next one feels familiar.
Install a Package
Every package is independent. Install only what you need:
pnpm add @vielzeug/ripplenpm install @vielzeug/rippleyarn add @vielzeug/ripple