onefold
A 3kb reactive UI framework with signals, templates, router, stores, SSR, and enterprise-grade microfrontend security. Zero dependencies. TypeScript-first.
Why onefold? It ships everything you need — signals, templates, routing, state, forms, i18n, theming, accessibility, SSR, and microfrontend security — in a single 3kb package with zero dependencies. No VDOM, no compiler, no polyfills. Built on modern web standards (ES2022, Shadow DOM, Web Crypto).
Installation
# npm
npm install onefold
# pnpm
pnpm add onefold
# yarn
yarn add onefold
Or scaffold a new project with the CLI:
npm create onefold@latest my-app
Quick Start
import { createSignal, html, mount } from 'onefold';
function Counter(): Node {
const count = createSignal(0);
return html`
<div>
<h1>Count: ${() => count()}</h1>
<button onclick=${() => count.set(n => n + 1)}>Increment</button>
</div>
`;
}
mount(Counter(), document.getElementById('app')!);
That's it. No build step required for development — just a modern browser and an ES module bundler for production.
Signals
Signals are the reactive primitive in onefold. They hold a value and automatically notify subscribers when it changes.
createSignal
import { createSignal, createEffect, createComputed, batch } from 'onefold';
const count = createSignal(0);
count() // read current value
count.set(5) // write a new value
count.set(n => n + 1) // update from previous
count.peek() // read without subscribing
createEffect
Run side effects whenever dependencies change. Dependencies are tracked automatically.
createEffect(() => {
console.log('Count changed:', count());
});
// Logs immediately, then again on every count change
createComputed
Create derived values that only recompute when their dependencies change.
const double = createComputed(() => count() * 2);
double() // always 2x count, cached until count changes
batch
Group multiple signal updates into a single flush to avoid intermediate renders.
const a = createSignal(0);
const b = createSignal(0);
batch(() => {
a.set(1);
b.set(2);
}); // effects run once, not twice
API Reference
| Function | Returns | Description |
|---|---|---|
createSignal(initial) | Signal<T> | Create a reactive signal with initial value |
signal() | T | Read value and subscribe to changes |
signal.set(value) | void | Set new value, notify subscribers |
signal.set(fn) | void | Update from previous value |
signal.peek() | T | Read without creating subscription |
createEffect(fn) | void | Run side effects on dependency change |
createComputed(fn) | () => T | Cached derived computation |
batch(fn) | void | Batch updates, single notification flush |
Templates (html)
The html tagged template literal creates real DOM nodes — no virtual DOM, no diffing. Reactive expressions (functions) are automatically tracked and updated in place.
Basic Usage
import { html } from 'onefold';
// Static content
html`<div class="card">Hello World</div>`
// Reactive text
html`<span>${() => count()}</span>`
// Reactive attributes
html`<div class=${() => active() ? 'active' : ''}>...</div>`
// Static attributes
html`<div class=${cls}>...</div>`
Styles
// Style as string
html`<div style="color: red; font-size: 16px;">...</div>`
// Style as object (reactive-friendly)
html`<div style=${{ color: 'red', fontSize: '16px' }}>...</div>`
Events
// Inline handler
html`<button onclick=${() => count.set(n => n + 1)}>Click</button>`
// Named handler
const handleClick = (e: Event) => { /* ... */ };
html`<button onclick=${handleClick}>Click</button>`
Reactive Lists
const items = createSignal(['Apple', 'Banana', 'Cherry']);
html`<ul>
${() => items().map(item => html`<li>${item}</li>`)}
</ul>`
Refs
html`<input ref=${(el) => el.focus()} />`
Directives
// Use registered directives with d- prefix
html`<div d-tooltip="Hello!">Hover me</div>`
Scoped CSS (css)
The css tagged template creates scoped stylesheets that won't leak into other components.
import { css, html, cssValue } from 'onefold';
const styles = css`.card {
background: white;
border-radius: 8px;
padding: 16px;
}
.title {
font-size: 18px;
font-weight: bold;
}`;
// Apply scoped styles using the generated scope class
html`<div class=${styles.scope}>
<div class="card">
<h2 class="title">Scoped!</h2>
</div>
</div>`
cssValue — Safe User Input in CSS
// Sanitizes user input to prevent CSS injection
const userColor = 'red; background: url(evil)';
css`.card { background: ${cssValue(userColor)}; }`
// Only "red" is applied — injection is stripped
Mounting (mount)
Attach a component tree to the DOM.
import { mount } from 'onefold';
const app = App();
mount(app, document.getElementById('app')!);
Note: mount replaces the container's content. If you need to append instead, use container.appendChild(node) directly with the result of html\`...\`.
Router
Client-side routing with nested routes, dynamic parameters, and programmatic navigation.
import { Router, navigate, Link } from 'onefold';
const App = Router([
{ path: '/', view: () => Home() },
{ path: '/users/:id', view: (params) => User(params) },
{ path: '/settings', view: (p, outlet) => Layout(outlet), children: [
{ path: '/profile', view: () => Profile() },
{ path: '/billing', view: () => Billing() },
]},
], NotFound);
Nested Routes
Child routes render into the parent's outlet parameter. This enables layout patterns where the shell stays rendered while inner content changes.
// Parent receives the outlet — renders the matched child
{ path: '/dashboard', view: (params, outlet) => html`
<div class="layout">
<nav>...</nav>
<main>${outlet}</main>
</div>
`, children: [
{ path: '/overview', view: () => Overview() },
{ path: '/analytics', view: () => Analytics() },
]}
Link
Declarative navigation links with active state tracking.
import { Link, currentRoute } from 'onefold';
// Link(path, label, classGetter?)
Link('/about', 'About', () => currentRoute() === '/about' ? 'active' : '')
Dynamic Params
Route parameters are passed to view functions as a params object.
// Route definition
{ path: '/users/:id', view: (params) => UserProfile(params) }
// Component receives params
function UserProfile(params: { id: string }) {
const user = createResource(
() => params.id,
(id) => fetch(`/api/users/${id}`).then(r => r.json())
);
return html`<h1>${() => user.data()?.name}</h1>`;
}
Store
A reactive state container for application-wide state. Simpler than Redux, more structured than individual signals.
import { createStore } from 'onefold';
const store = createStore({ user: null, items: [] });
// Read the entire state
store()
// Partial merge update
store.update({ user: userData })
// Functional update from previous state
store.update(prev => ({
items: [...prev.items, newItem]
}))
| Method | Description |
|---|---|
store() | Read current state (reactive) |
store.update(partial) | Shallow merge partial state |
store.update(fn) | Update from previous state |
Persisted Signals
Signals that automatically sync with localStorage. Survive page reloads.
import { createPersisted } from 'onefold';
const prefs = createPersisted('user-prefs', { theme: 'dark', fontSize: 14 });
// Read
prefs() // { theme: 'dark', fontSize: 14 }
// Write — automatically persisted
prefs.set({ theme: 'light', fontSize: 16 });
// Remove from storage
prefs.clear();
Tip: Values are JSON-serialized. The second argument is the default value used when nothing is found in storage.
Resource
Declarative async data fetching tied to reactive sources. Automatically refetches when dependencies change.
import { createSignal, createResource } from 'onefold';
const userId = createSignal(1);
const user = createResource(
userId, // source signal — refetches when it changes
(id) => fetch(`/api/users/${id}`).then(r => r.json())
);
user.data() // T | undefined
user.loading() // boolean
user.error() // unknown | undefined
user.refetch() // manually trigger refetch
user.dispose() // cleanup subscriptions
| Property | Type | Description |
|---|---|---|
data() | T | undefined | Resolved value |
loading() | boolean | True while fetching |
error() | unknown | Rejection reason if failed |
refetch() | void | Force re-fetch |
dispose() | void | Unsubscribe from source |
HTTP Client
A typed HTTP client with interceptors, timeout, and base URL support.
import { createHttpClient } from 'onefold';
const http = createHttpClient({
baseUrl: '/api',
headers: { 'X-App': 'onefold' },
interceptors: [authInterceptor],
timeout: 5000,
});
// GET
const users = await http.get<User[]>('/users');
// POST
await http.post('/users', { name: 'Alice' });
// PUT, DELETE, PATCH
await http.put('/users/1', updatedUser);
await http.delete('/users/1');
await http.patch('/users/1', { active: false });
Interceptors
Interceptors modify requests and responses globally — useful for auth tokens, logging, and error handling.
const authInterceptor = {
request: (config) => ({
...config,
headers: { ...config.headers, Authorization: `Bearer ${token()}` }
}),
response: (res) => res,
error: (err) => {
if (err.status === 401) navigate('/login');
throw err;
}
};
Forms
Reactive form management with built-in validation.
import { createForm, required, email, minLength } from 'onefold';
const form = createForm({
email: { initial: '', rules: [required(), email()] },
password: { initial: '', rules: [required(), minLength(8)] },
});
// Reactive field state
form.fields.email.value() // current value
form.fields.email.error() // validation error string or null
form.fields.email.touched() // has been focused+blurred
// Form-level state
form.valid() // all fields pass validation
form.dirty() // any field changed from initial
// Submit
form.submit((values) => {
console.log(values); // { email: '...', password: '...' }
});
// Bind to inputs
html`<input
type="email"
oninput=${form.fields.email.handle}
/>
<span class="error">${() => form.fields.email.error()}</span>`
Validation Rules
| Rule | Description |
|---|---|
required() | Field must not be empty |
email() | Must match email format |
minLength(n) | Minimum character count |
maxLength(n) | Maximum character count |
pattern(regex) | Must match regex |
min(n) | Minimum numeric value |
max(n) | Maximum numeric value |
custom(fn) | Custom validator: (value) => string | null |
// Custom validator example
const strongPassword = custom((val) => {
if (!/[A-Z]/.test(val)) return 'Must contain uppercase letter';
if (!/[0-9]/.test(val)) return 'Must contain a number';
return null;
});
Microfrontends
Enterprise-grade microfrontend architecture with built-in security, isolation modes, SRI integrity checks, and cross-framework support.
A microfrontend in onefold is an ES module hosted on any URL that exports a function returning a DOM Node. The host loads it at runtime via loadRemote().
// Remote widget (billing.cdn.com/widget.js)
import { createSignal, html, css } from 'onefold';
export default function BillingWidget(props: { accountId: string }): Node {
const usage = createSignal(0);
return html`<div>Account: ${props.accountId} — Usage: ${() => usage()}%</div>`;
}
configureSecurity
Set global security policies once at app startup, before loading any remotes:
import { configureSecurity } from 'onefold';
configureSecurity({
// Only these origins are allowed. Everything else is blocked.
trustedOrigins: ['https://cdn.billing.com', 'https://cdn.analytics.com'],
// Require SRI hash for every remote (production)
requireIntegrity: true,
// Maximum time to wait for a remote to load
timeout: 10000,
// Emergency kill switch — blocks ALL remotes instantly
blockAll: false,
});
Security Layers
| Layer | What it does |
|---|---|
trustedOrigins | Whitelist — blocks any URL not from an approved origin, synchronously before any network request |
integrity (SRI) | Verifies remote code hash before execution — detects tampering in transit |
isolate: 'shadow' | Closed Shadow DOM — remote CSS can't reach host, host can't inspect remote internals |
isolate: 'iframe' | Full sandbox — separate JS context, no access to host window/document/cookies |
credentials: 'omit' | Hardcoded — remotes never receive host's cookies or auth headers |
blockAll | Emergency kill switch — disables all remote loading instantly |
clearRemoteCache() | Invalidate cached modules if a compromise is detected |
loadRemote
Load a remote module and render it as a local component.
import { loadRemote } from 'onefold';
const BillingWidget = loadRemote({
url: 'https://cdn.billing.com/widget.js',
isolate: 'shadow', // 'none' | 'shadow' | 'iframe'
integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
permissions: ['network', 'storage'],
fallback: () => Spinner(),
onError: (err) => html`<div class="error">${err.message}</div>`,
});
// Use like any component, pass props
html`${BillingWidget({ accountId: '123', onSave: handleSave })}`
preloadRemote
import { preloadRemote } from 'onefold';
// Prefetch on hover for instant mount
html`<button
onmouseenter=${() => preloadRemote('https://cdn.billing.com/widget.js')}
onclick=${showBilling}
>Open Billing</button>`
clearRemoteCache
import { clearRemoteCache } from 'onefold';
clearRemoteCache(); // clear all
clearRemoteCache('https://cdn.billing.com/widget.js'); // clear specific
Isolation Modes
| Mode | CSS Isolation | JS Isolation | Use Case |
|---|---|---|---|
'none' | None | None | Trusted first-party remotes |
'shadow' | Full (Shadow DOM) | None | CSS conflicts between teams |
'iframe' | Full | Full (sandbox) | Untrusted or third-party code |
Security note: Use 'iframe' isolation for any untrusted or third-party code. Shadow DOM only isolates CSS — JavaScript still runs in the main thread with full DOM access.
Communication
Host → Remote (Props)
Pass data from host to remote as function arguments:
// Host passes data as props
html`${BillingWidget({ user: currentUser(), token: authToken(), locale: 'en' })}`
// Remote receives props
export default function Widget(props: { user: User; token: string; locale: string }): Node {
// props.user, props.token, props.locale are available
return html`<div>Hello ${props.user.name}</div>`;
}
Remote → Host (Callback Props)
Pass callback functions as props — the remote calls them to send data back to the host:
// HOST — passes callbacks as props
const selectedPlan = createSignal('free');
html`${BillingWidget({
currentPlan: selectedPlan(),
onPlanChange: (plan: string) => selectedPlan.set(plan),
onPayment: (amount: number) => processPayment(amount),
})}`
// REMOTE — calls the callback to notify the host
export default function BillingWidget(props: {
currentPlan: string;
onPlanChange: (plan: string) => void;
onPayment: (amount: number) => void;
}): Node {
const upgrade = () => {
props.onPlanChange('pro'); // sends data TO the host
props.onPayment(49); // sends data TO the host
};
return html`<button onclick=${upgrade}>Upgrade to Pro</button>`;
}
iframe Communication (postMessage)
When using isolate: 'iframe', the remote runs in a separate JS context and can't receive function props. Use postMessage instead:
// HOST — listens for messages from the iframe
window.addEventListener('message', (e) => {
if (e.data?.type === 'billing:plan-changed') {
selectedPlan.set(e.data.plan);
}
});
// REMOTE (inside iframe) — posts messages to host
const upgrade = () => {
window.parent.postMessage({ type: 'billing:plan-changed', plan: 'pro' }, '*');
};
Communication Summary
| Direction | Isolation: none/shadow | Isolation: iframe |
|---|---|---|
| Host → Remote | Props (function args) | Props serialized into iframe srcdoc |
| Remote → Host | Callback props | window.parent.postMessage() |
| Remote ↔ Remote | CustomEvent on document | postMessage via host relay |
SRI Integrity
Subresource Integrity verification ensures remotes haven't been tampered with. The hash is stored in the host's source code, not in the remote file — so an attacker can't modify both.
How SRI Verification Works
┌──────────────────────────────────────────────────────────────────┐
│ BUILD & DEPLOY TIME │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 1. Remote team builds: npm run build → dist/billing.js │
│ 2. Generate SHA-384 hash of the built file │
│ 3. Remote team sends hash to host team (PR / config service) │
│ 4. Remote deploys billing.js to CDN │
│ 5. Host team puts hash in THEIR code: │
│ loadRemote({ integrity: 'sha384-ABC...' }) │
│ │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ RUNTIME (User's browser) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 6. Host calls loadRemote({ integrity: 'sha384-...' }) │
│ 7. fetch(url) → gets remote JS as plain text │
│ 8. crypto.subtle.digest('SHA-384', bytes) → compute hash │
│ 9. COMPARE: computed === expected? │
│ │
│ ✅ MATCH → import module → execute │
│ ❌ MISMATCH → throw error → module NEVER runs │
│ │
└──────────────────────────────────────────────────────────────────┘
Generate the Hash
# openssl
cat dist/billing.js | openssl dgst -sha384 -binary | openssl base64 -A
# shasum
shasum -b -a 384 dist/billing.js | awk '{print $1}' | xxd -r -p | base64
# Node.js
node -e "
const fs = require('fs');
const crypto = require('crypto');
const source = fs.readFileSync('dist/billing.js');
const hash = crypto.createHash('sha384').update(source).digest('base64');
console.log('sha384-' + hash);
"
Use in the Host
const BillingWidget = loadRemote({
url: 'https://cdn.billing.com/widget.js',
integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux...',
onError: (err) => {
if (err.message.includes('Integrity check FAILED')) {
return html`<div>Widget updated — please refresh</div>`;
}
return html`<div>Error: ${err.message}</div>`;
},
});
What SRI Protects Against
| Attack | Without SRI | With SRI |
|---|---|---|
| CDN compromise (attacker modifies widget.js) | Runs malicious code | Blocked — hash mismatch |
| Man-in-the-middle (intercepts and modifies) | Runs modified code | Blocked — hash mismatch |
| Stale cache (CDN serves old version) | Runs old code | Blocked — hash differs |
| Accidental deploy (wrong file uploaded) | Runs wrong code | Blocked — hash mismatch |
Hash Delivery Patterns
| Pattern | Who generates | How host gets it | Best for |
|---|---|---|---|
| Remote-generated | Remote team CI | PR, Slack, config service API | Independent teams |
| Host-verified | Host team locally | Download from registry, hash locally | Centralized governance |
| Pipeline-automated | CI/CD pipeline | Config service (fetched at runtime) | Enterprise zero-touch |
Hash Validity
The hash is valid forever for that exact file — it only becomes invalid when the remote team deploys a new version (different bytes → different hash).
| Situation | Hash validity | What happens |
|---|---|---|
| Remote file unchanged | Valid forever | Loads successfully |
| Remote deploys new version | Immediately invalid | Hash mismatch → onError fires |
| Rollback to previous version | Valid again | Matches old hash → loads |
Best Practice: Versioned URLs
// Option 1: Version in URL (most predictable)
loadRemote({ url: 'https://cdn/billing-v1.2.0.js', integrity: 'sha384-ABC...' });
// Option 2: Config service (zero-downtime updates)
const config = await fetch('https://config.service/remotes.json');
const { url, integrity } = config.billing.latest;
loadRemote({ url, integrity });
// Host fetches hash at runtime — updates without redeploying host
Enforce SRI globally: Set requireIntegrity: true in configureSecurity() to block any loadRemote call that doesn't provide an integrity hash. This prevents developers from accidentally shipping a remote without verification.
Independent Deployment
Each microfrontend is built and deployed independently. The host discovers remotes at runtime via URL — no rebuild needed when a remote updates.
# Team A deploys billing widget
npm run build:billing # → dist/billing.js
aws s3 cp dist/billing.js s3://billing-cdn/widget.js
# Team B deploys analytics widget
npm run build:analytics # → dist/analytics.js
aws s3 cp dist/analytics.js s3://analytics-cdn/widget.js
# Host just references the URLs — no rebuild needed
configureSecurity({ trustedOrigins: ['https://billing-cdn', 'https://analytics-cdn'] });
Why duplication doesn't matter: onefold is 3kb per remote. Five teams each bundling their own copy costs 15kb total. React would cost 210kb. The entire rationale for Module Federation's complexity disappears at this size.
Recommended Project Structure
my-platform/
src/
host/
main.ts Shell — loads remote widgets
remotes/
billing/
index.ts Self-contained (own signals, scoped CSS)
analytics/
index.ts Self-contained
shared/
types.ts Type contracts between host and remotes
index.html Host HTML shell
style.css Host global styles
build.mjs Builds everything (supports --target for individual)
dev.mjs Dev servers — host :3000, remotes :3001
Performance Features
| Feature | What it does |
|---|---|
preloadRemote(url) | Prefetch a remote on hover — instant loading when user clicks |
fallback: () => Spinner() | Show immediate feedback while remote loads |
timeout: 10000 | Kill hanging loads after 10s — page stays responsive |
| Module caching | Once loaded, a remote is cached — subsequent calls are instant |
clearRemoteCache(url) | Force re-fetch after deploy or compromise |
Error Handling
Each remote has its own error boundary — one failing remote never crashes the host:
const Widget = loadRemote({
url: 'https://billing.cdn.com/widget.js',
fallback: () => html`<div class="spinner">Loading...</div>`,
onError: (err) => html`<div class="error">
<p>Failed: ${err.message}</p>
<button onclick=${() => { clearRemoteCache(url); location.reload(); }}>Retry</button>
</div>`,
});
Quick Start
# Scaffold a microfrontend project
npm create onefold@latest my-platform --template microfrontend
cd my-platform
npm install
npm run dev # Host on :3000, Remotes on :3001 (livereload)
npm run build # Build everything → dist/
Cross-Framework Loading
Load components built with any framework — React, Vue, or plain HTML via iframe.
import { embedForeign, loadRemote } from 'onefold';
// React component in onefold
const ReactWidget = embedForeign({
render: (el) => {
const root = ReactDOM.createRoot(el);
root.render(React.createElement(BillingApp));
return root;
},
unrender: (root) => root.unmount(),
});
// Vue component in onefold
const VueWidget = embedForeign({
render: (el) => {
const app = createApp(AnalyticsApp);
app.mount(el);
return app;
},
unrender: (app) => app.unmount(),
});
// Any framework via iframe (full isolation)
const LegacyApp = loadRemote({
url: 'https://legacy.example.com/app.js',
isolate: 'iframe',
});
Microfrontend API Reference
| Function | Description |
|---|---|
configureSecurity(config) | Set trusted origins, SRI requirements, timeout, kill switch |
loadRemote(options) | Load a remote ES module as a component |
preloadRemote(url) | Prefetch without mounting (hover optimization) |
clearRemoteCache(url?) | Invalidate cached modules (no arg = clear all) |
loadRemote Options
| Option | Type | Default | Description |
|---|---|---|---|
url | string | — | URL to the remote ES module |
exportName | string | 'default' | Named export to call |
isolate | 'none' | 'shadow' | 'iframe' | 'none' | Isolation level |
integrity | string | — | SRI hash (sha256/384/512) |
permissions | string[] | ['dom'] | dom, storage, network, navigation, clipboard |
fallback | () => Node | — | Loading state UI |
onError | (err) => Node | — | Error state UI |
timeout | number | 10000 | Max load time (ms) |
configureSecurity Options
| Option | Type | Default | Description |
|---|---|---|---|
trustedOrigins | string[] | [] (allow all) | Whitelist of allowed origins |
requireIntegrity | boolean | false | Block remotes without SRI hash |
timeout | number | 10000 | Global timeout for all remotes |
blockAll | boolean | false | Kill switch — block everything |
Async
Suspense
Wrap async component loading with fallback and error handling.
import { Suspense } from 'onefold';
Suspense(
async () => {
const data = await fetch('/api/dashboard').then(r => r.json());
return html`<div>${data.title}</div>`;
},
{
fallback: () => Spinner(),
onError: (err) => ErrorBox(err),
}
);
SuspenseAll
Wait for multiple async operations before rendering.
import { SuspenseAll } from 'onefold';
SuspenseAll(
[
async () => { const users = await fetchUsers(); return UserList(users); },
async () => { const stats = await fetchStats(); return StatBar(stats); },
],
{ fallback: () => Skeleton() }
);
Lazy Loading
Code-split components that load on demand.
import { lazy } from 'onefold';
const Dashboard = lazy(
() => import('./pages/Dashboard'),
() => Spinner() // shown while loading
);
// Use in router
{ path: '/dashboard', view: () => Dashboard() }
Error Boundaries
Catch rendering errors and display fallback UI.
Suspense(
() => RiskyComponent(),
{
onError: (error) => html`
<div class="error-boundary">
<h3>Something went wrong</h3>
<pre>${error.message}</pre>
<button onclick=${() => location.reload()}>Reload</button>
</div>
`,
}
);
Streaming
WebSocket
Reactive WebSocket wrapper with auto-reconnect and typed messages.
import { createWebSocket } from 'onefold';
interface ChatMsg { user: string; text: string; ts: number; }
const ws = createWebSocket<ChatMsg>('wss://chat.app/room/42');
ws.data() // ChatMsg[] — all received messages
ws.latest() // ChatMsg | undefined — most recent
ws.status() // 'connecting' | 'open' | 'closed' | 'error'
ws.send({ user: 'Alice', text: 'Hello!', ts: Date.now() });
ws.close();
// Reactive UI
html`<ul>${() => ws.data().map(m => html`<li><b>${m.user}</b>: ${m.text}</li>`)}</ul>`
Server-Sent Events
Reactive SSE stream for one-way server push.
import { createEventSource } from 'onefold';
interface Notification { id: string; message: string; }
const sse = createEventSource<Notification>('/api/notifications/stream');
sse.data() // Notification[] — all events
sse.latest() // Notification | undefined
sse.status() // 'connecting' | 'open' | 'closed'
sse.close();
// Show latest notification
html`<div class="toast">${() => sse.latest()?.message}</div>`
Internationalization
createI18n
import { createI18n } from 'onefold';
const i18n = createI18n({
defaultLocale: 'en',
messages: {
en: {
greeting: 'Hello {name}',
items: '{count} items in cart',
},
es: {
greeting: 'Hola {name}',
items: '{count} artículos en el carrito',
},
}
});
Locale Switching
// Get current locale
i18n.locale() // 'en'
// Switch locale (reactive — UI updates automatically)
i18n.setLocale('es');
// Add messages at runtime
i18n.addMessages('fr', {
greeting: 'Bonjour {name}',
items: '{count} articles dans le panier',
});
Interpolation
// Simple interpolation
i18n.t('greeting', { name: 'World' }) // "Hello World"
// Use in templates (reactive)
html`<h1>${() => i18n.t('greeting', { name: userName() })}</h1>`
html`<p>${() => i18n.t('items', { count: cart().length })}</p>`
Theming
createTheme
import { createTheme } from 'onefold';
const theme = createTheme({
light: { bg: '#ffffff', text: '#1a1a1a', accent: '#6366f1' },
dark: { bg: '#0f1117', text: '#e4e7ef', accent: '#818cf8' },
}, 'light'); // default theme
// Read current theme name
theme.current() // 'light'
// Switch theme
theme.set('dark');
CSS Custom Properties
Theme values are applied as CSS custom properties on :root.
/* Automatically injected by createTheme */
:root {
--bg: #ffffff;
--text: #1a1a1a;
--accent: #6366f1;
}
/* Use in your CSS */
.card {
background: var(--bg);
color: var(--text);
border: 1px solid var(--accent);
}
Toggle
// Toggle between themes
theme.toggle(); // light → dark → light
// Use in a button
html`<button onclick=${() => theme.toggle()}>
${() => theme.current() === 'dark' ? '☀️ Light' : '🌙 Dark'}
</button>`
Accessibility
FocusTrap
Trap keyboard focus within a container — essential for modals and dialogs.
import { FocusTrap } from 'onefold';
const trap = FocusTrap(modalElement);
trap.activate(); // trap focus inside modal
trap.deactivate(); // release focus
announce
Announce messages to screen readers via ARIA live regions.
import { announce } from 'onefold';
announce('Item deleted successfully'); // polite
announce('Error: form is invalid', 'assertive'); // urgent
useKeyboard
Declarative keyboard shortcut handling.
import { useKeyboard } from 'onefold';
const kb = useKeyboard({
'Ctrl+S': () => save(),
'Ctrl+Z': () => undo(),
'Escape': () => closeModal(),
'Ctrl+K': () => openSearch(),
});
// Cleanup when component unmounts
kb.destroy();
SkipLink
Provide a skip-to-content link for keyboard users.
import { SkipLink } from 'onefold';
// Renders a visually hidden link that appears on focus
SkipLink('#main-content');
// In your layout:
html`
${SkipLink('#main-content')}
<nav>...</nav>
<main id="main-content">...</main>
`
Transitions
Transition
Animate elements entering and leaving the DOM.
import { Transition } from 'onefold';
// Named transition (CSS classes)
Transition(() => currentView(), {
name: 'fade',
duration: 300,
});
// Applies: .fade-enter-from, .fade-enter-to, .fade-leave-to
animateEnter / animateLeave
Inline animation definitions without CSS classes.
// Inline style transitions
Transition(() => currentView(), {
duration: 300,
enterFrom: { opacity: '0', transform: 'translateY(10px)' },
enterTo: { opacity: '1', transform: 'translateY(0)' },
leaveTo: { opacity: '0', transform: 'translateY(-10px)' },
});
Dependency Injection
createToken
Type-safe dependency injection for services, making components testable and decoupled.
import { createToken, provide, inject } from 'onefold';
// Define a typed token
interface AuthApi {
user: () => User | null;
login: (creds: Credentials) => Promise<void>;
logout: () => void;
}
const AuthService = createToken<AuthApi>('auth');
provide / inject
// Provide implementation
provide(AuthService, {
user: createSignal(null),
login: async (creds) => { /* ... */ },
logout: () => { /* ... */ },
});
// Inject in any component
function UserMenu() {
const auth = inject(AuthService);
return html`<span>${() => auth.user()?.name ?? 'Guest'}</span>`;
}
runWithProviders
Override providers for testing or isolated contexts.
import { runWithProviders } from 'onefold';
// In tests — provide mock implementations
const mockAuth: AuthApi = {
user: createSignal({ name: 'Test User' }),
login: async () => {},
logout: () => {},
};
runWithProviders(
[[AuthService, mockAuth]],
() => UserMenu() // uses mock auth
);
Security
RBAC Guards
Role-based access control for routes and UI elements.
import { setPermissions, guard, hasPermission, guardedNode } from 'onefold';
// Set user permissions (usually after login)
setPermissions(createSignal(new Set(['admin', 'billing:read', 'users:write'])));
// Guard routes
Router([
{ path: '/admin', view: guard(['admin'], AdminPage, AccessDenied) },
{ path: '/billing', view: guard(['billing:read'], BillingPage, AccessDenied) },
]);
// Check permissions imperatively
hasPermission('admin') // true
hasPermission('billing:write') // false
// Guard individual UI nodes
guardedNode(['admin'], () => html`<button>Delete All</button>`)
XSS Prevention
The html template automatically escapes text content. Dynamic values inserted as text are never interpreted as HTML.
const userInput = '<script>alert("xss")</script>';
// Safe — renders as escaped text, not executed
html`<div>${userInput}</div>`
// Output: <div><script>alert("xss")</script></div>
Sanitization
import { sanitize, cssValue } from 'onefold';
// Sanitize HTML if you must insert raw HTML
const clean = sanitize(untrustedHtml);
// Strips scripts, event handlers, dangerous attrs
// Sanitize CSS values
const safeColor = cssValue(userProvidedColor);
// Prevents CSS injection attacks
Trusted Types
onefold integrates with the browser's Trusted Types API for defense-in-depth against DOM XSS.
// When Trusted Types policy is active,
// all DOM writes go through the framework's policy
// No raw string innerHTML assignments are allowed
// CSP header to enforce:
// Content-Security-Policy: require-trusted-types-for 'script'
Best practice: Always use the html template for rendering. Avoid innerHTML directly. The framework's template engine creates DOM nodes without string parsing, eliminating injection vectors.
Performance
VirtualList
Efficiently render thousands of items by only rendering those visible in the viewport.
import { VirtualList, createSignal } from 'onefold';
const items = createSignal(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })));
VirtualList({
items: items,
itemHeight: 40, // px per row
height: 600, // container height in px
overscan: 6, // extra rows rendered above/below viewport
renderRow: (item, index) => html`
<div class="row">
<span>#${index}</span>
<span>${item.name}</span>
</div>
`,
});
Code Splitting (lazy)
Split your app into chunks that load on demand. Combined with the router, only visited pages are downloaded.
import { lazy } from 'onefold';
const Admin = lazy(() => import('./pages/Admin'), () => Spinner());
const Analytics = lazy(() => import('./pages/Analytics'), () => Spinner());
Router([
{ path: '/admin', view: () => Admin() },
{ path: '/analytics', view: () => Analytics() },
]);
Interop
wrapImperative
Wrap imperative libraries (Chart.js, D3, MapboxGL) as reactive components.
import { wrapImperative, createSignal } from 'onefold';
const data = createSignal([12, 19, 3, 5, 2]);
const ChartWidget = wrapImperative({
tag: 'canvas',
mount: (el) => new Chart(el, {
type: 'bar',
data: { datasets: [{ data: data() }] },
}),
update: (chart) => {
chart.data.datasets[0].data = data();
chart.update();
},
destroy: (chart) => chart.destroy(),
watch: () => data(), // triggers update when data changes
});
embedForeign
Embed React, Vue, or any framework's components inside onefold.
import { embedForeign } from 'onefold';
// React component
const ReactChart = embedForeign({
render: (el) => {
const root = ReactDOM.createRoot(el);
root.render(React.createElement(ChartApp, { data: data() }));
return root;
},
unrender: (root) => root.unmount(),
});
// Vue component
const VueWidget = embedForeign({
render: (el) => {
const app = createApp(AnalyticsWidget);
app.mount(el);
return app;
},
unrender: (app) => app.unmount(),
});
Plugins
createPluginHost
Extensible plugin system with sandboxed permissions and lifecycle hooks.
import { createPluginHost } from 'onefold';
const plugins = createPluginHost();
plugins.register({
name: 'analytics',
version: '1.0.0',
permissions: ['observe', 'network'],
setup: (ctx) => {
ctx.on('navigate', (route) => trackPageView(route));
ctx.on('error', (err) => reportError(err));
},
});
plugins.start(); // activate all registered plugins
Permissions
Plugins declare what they need. The host enforces boundaries.
| Permission | Grants Access To |
|---|---|
'observe' | Read-only access to app events (navigate, render, error) |
'network' | Outbound HTTP requests |
'storage' | localStorage / sessionStorage |
'dom' | Direct DOM manipulation |
'state' | Read/write app state |
Lifecycle
plugins.register({
name: 'logger',
version: '1.0.0',
permissions: ['observe'],
setup: (ctx) => { /* called on start */ },
teardown: () => { /* called on stop/unregister */ },
});
plugins.stop('logger'); // stop specific plugin
plugins.unregister('logger'); // remove entirely
plugins.stopAll(); // stop all plugins
Observability
createObserver
Built-in observability for monitoring render performance, errors, and custom metrics.
import { createObserver } from 'onefold';
const obs = createObserver();
// Listen to framework events
obs.on('error', (e) => sendToSentry(e.error));
obs.on('render', (e) => perfMonitor.record(e.component, e.duration));
obs.on('navigate', (e) => analytics.track('page_view', e.path));
Metrics
// Track render performance
obs.trackRender('UserList', () => renderUserList());
// Record custom metrics
obs.metric('api_latency', 120, { endpoint: '/users' });
obs.metric('cache_hit_rate', 0.87);
Logging
// Structured logging
obs.log('info', 'User logged in', { userId: '123' });
obs.log('warn', 'Slow render detected', { component: 'Table', ms: 200 });
obs.log('error', 'Failed to fetch', { url: '/api/users' });
Component Metadata
component()
Annotate components with metadata for the registry and devtools.
import { component } from 'onefold';
const UserCard = component({
name: 'UserCard',
version: '1.2.0',
description: 'Displays user profile card',
props: ['user', 'compact'],
}, (props) => {
return html`<div class="user-card">${props.user.name}</div>`;
});
Registry
import { getComponentRegistry } from 'onefold';
const registry = getComponentRegistry();
registry.list() // all registered components
registry.get('UserCard') // metadata for specific component
Manifest Export
Export a JSON manifest of all components — useful for design systems and documentation tools.
import { exportManifest } from 'onefold';
const manifest = exportManifest();
// { components: [{ name, version, description, props }] }
// Write to file in build step
fs.writeFileSync('component-manifest.json', JSON.stringify(manifest));
SSR
renderToString
Server-side render components to HTML strings for fast initial page loads.
import { renderToString } from 'onefold';
const html = renderToString(() => App());
// Returns: '<div class="app">...</div>'
// Use in Express/Node server
app.get('/', (req, res) => {
const body = renderToString(() => App());
res.send(`<!DOCTYPE html><html><body>${body}</body></html>`);
});
renderToStringAsync
Async SSR — waits for data fetching before rendering.
import { renderToStringAsync } from 'onefold';
const html = await renderToStringAsync(async () => {
const data = await fetchInitialData();
return App({ data });
});
// Full SSR with data pre-fetching
app.get('*', async (req, res) => {
const body = await renderToStringAsync(async () => {
const user = await getUser(req.cookies.token);
return App({ user, route: req.path });
});
res.send(shell(body));
});
DevTools
enableDevtools
Enable runtime devtools for debugging signal graphs, render performance, and component trees.
import { enableDevtools, disableDevtools } from 'onefold';
// Enable in development only
if (import.meta.env?.DEV) {
enableDevtools();
}
// Exposes window.__NANOFRAME_DEVTOOLS__ with:
// - signals: active signal graph
// - renders: render performance timeline
// - components: mounted component tree
// - stores: all active stores and their state
disableDevtools
// Disable devtools (for production builds)
disableDevtools();
// Removes window.__NANOFRAME_DEVTOOLS__ and stops tracking
Tip: The devtools have zero overhead when disabled. Tree-shaking removes all devtools code in production builds when guarded by import.meta.env.DEV.
Utilities
Common formatting and timing utilities included in the framework.
formatDate
import { formatDate } from 'onefold';
formatDate(new Date(), 'short') // "Jul 14, 2026"
formatDate(new Date(), 'long') // "July 14, 2026, 3:45 PM"
formatDate(new Date(), 'iso') // "2026-07-14"
timeAgo
import { timeAgo } from 'onefold';
timeAgo(new Date(Date.now() - 60000)) // "1 minute ago"
timeAgo(new Date(Date.now() - 3600000)) // "1 hour ago"
timeAgo(new Date(Date.now() - 86400000)) // "1 day ago"
formatCurrency
import { formatCurrency } from 'onefold';
formatCurrency(49.99, 'USD') // "$49.99"
formatCurrency(1234.5, 'EUR') // "€1,234.50"
formatCurrency(9999, 'JPY') // "¥9,999"
String Utilities
import { truncate, slugify, pluralize } from 'onefold';
truncate('A very long string that needs trimming', 20)
// "A very long string t…"
slugify('Hello World! How are you?')
// "hello-world-how-are-you"
pluralize(0, 'item') // "0 items"
pluralize(1, 'item') // "1 item"
pluralize(5, 'item') // "5 items"
debounce / throttle
import { debounce, throttle } from 'onefold';
// Debounce: wait until user stops typing
const search = debounce((query: string) => {
fetchResults(query);
}, 300);
// Throttle: limit to once per interval
const handleScroll = throttle(() => {
updateScrollPosition();
}, 100);
html`<input oninput=${(e) => search(e.target.value)} />`
pipe
Compose functions left-to-right for readable data transformations.
import { pipe } from 'onefold';
const result = pipe(
' Hello World ',
(s) => s.trim(),
(s) => s.toLowerCase(),
(s) => s.replace(/\s+/g, '-'),
);
// "hello-world"
// Useful for data processing pipelines
const processedUsers = pipe(
rawUsers,
(users) => users.filter(u => u.active),
(users) => users.sort((a, b) => a.name.localeCompare(b.name)),
(users) => users.slice(0, 10),
);
Extensions
registerDirective
Create custom template directives that attach behavior to elements.
import { registerDirective } from 'onefold';
// Register a tooltip directive
registerDirective('tooltip', (el, value) => {
const tip = document.createElement('div');
tip.className = 'tooltip';
tip.textContent = value;
el.addEventListener('mouseenter', () => document.body.appendChild(tip));
el.addEventListener('mouseleave', () => tip.remove());
});
// Usage in templates
html`<button d-tooltip="Save changes">💾</button>`
// Register a click-outside directive
registerDirective('click-outside', (el, handler) => {
document.addEventListener('click', (e) => {
if (!el.contains(e.target)) handler();
});
});
setEffectHook
Override the default effect runner for profiling or debugging.
import { setEffectHook } from 'onefold';
// Profile all effects
setEffectHook((label, fn) => {
console.time(label);
fn();
console.timeEnd(label);
});
// Or send to performance monitoring
setEffectHook((label, fn) => {
const start = performance.now();
fn();
const duration = performance.now() - start;
if (duration > 16) {
console.warn(`Slow effect: ${label} took ${duration.toFixed(1)}ms`);
}
});
CLI (create-onefold)
Scaffold new projects with pre-configured templates.
Templates
# Base template — minimal setup
npm create onefold@latest my-app --template base
# SPA — routing, state, CSS scoping
npm create onefold@latest my-app --template spa
# Fullstack — routing, auth, forms, i18n, SSR
npm create onefold@latest my-app --template fullstack
# Microfrontend — host + remotes, security, isolation
npm create onefold@latest my-app --template microfrontend
| Template | Includes |
|---|---|
base | Signals, html, css, mount, dev server |
spa | Base + Router, Store, Lazy loading |
fullstack | SPA + Forms, i18n, HTTP client, Auth, SSR |
microfrontend | Fullstack + Host/Remote scaffold, Security config, Build scripts |
Options
# Interactive mode (prompts for options)
npm create onefold@latest
# Specify project name
npm create onefold@latest my-project
# Skip prompts with defaults
npm create onefold@latest my-project --template spa --yes
# Show help
npm create onefold@latest --help
| Option | Description |
|---|---|
--template <name> | Template to use (base, spa, fullstack, microfrontend) |
--yes | Skip prompts, use defaults |
--help | Show help message |