onefold

A 3kb reactive UI framework with signals, templates, router, stores, SSR, and enterprise-grade microfrontend security. Zero dependencies. TypeScript-first.

3kbGzipped
0Dependencies
TypeScriptFirst-class
243Tests

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

FunctionReturnsDescription
createSignal(initial)Signal<T>Create a reactive signal with initial value
signal()TRead value and subscribe to changes
signal.set(value)voidSet new value, notify subscribers
signal.set(fn)voidUpdate from previous value
signal.peek()TRead without creating subscription
createEffect(fn)voidRun side effects on dependency change
createComputed(fn)() => TCached derived computation
batch(fn)voidBatch 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() },
]}

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]
}))
MethodDescription
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
PropertyTypeDescription
data()T | undefinedResolved value
loading()booleanTrue while fetching
error()unknownRejection reason if failed
refetch()voidForce re-fetch
dispose()voidUnsubscribe 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

RuleDescription
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

LayerWhat it does
trustedOriginsWhitelist — 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
blockAllEmergency 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

ModeCSS IsolationJS IsolationUse Case
'none'NoneNoneTrusted first-party remotes
'shadow'Full (Shadow DOM)NoneCSS conflicts between teams
'iframe'FullFull (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

DirectionIsolation: none/shadowIsolation: iframe
Host → RemoteProps (function args)Props serialized into iframe srcdoc
Remote → HostCallback propswindow.parent.postMessage()
Remote ↔ RemoteCustomEvent on documentpostMessage 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

AttackWithout SRIWith SRI
CDN compromise (attacker modifies widget.js)Runs malicious codeBlocked — hash mismatch
Man-in-the-middle (intercepts and modifies)Runs modified codeBlocked — hash mismatch
Stale cache (CDN serves old version)Runs old codeBlocked — hash differs
Accidental deploy (wrong file uploaded)Runs wrong codeBlocked — hash mismatch

Hash Delivery Patterns

PatternWho generatesHow host gets itBest for
Remote-generatedRemote team CIPR, Slack, config service APIIndependent teams
Host-verifiedHost team locallyDownload from registry, hash locallyCentralized governance
Pipeline-automatedCI/CD pipelineConfig 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).

SituationHash validityWhat happens
Remote file unchangedValid foreverLoads successfully
Remote deploys new versionImmediately invalidHash mismatch → onError fires
Rollback to previous versionValid againMatches 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

FeatureWhat it does
preloadRemote(url)Prefetch a remote on hover — instant loading when user clicks
fallback: () => Spinner()Show immediate feedback while remote loads
timeout: 10000Kill hanging loads after 10s — page stays responsive
Module cachingOnce 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

FunctionDescription
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

OptionTypeDefaultDescription
urlstringURL to the remote ES module
exportNamestring'default'Named export to call
isolate'none' | 'shadow' | 'iframe''none'Isolation level
integritystringSRI hash (sha256/384/512)
permissionsstring[]['dom']dom, storage, network, navigation, clipboard
fallback() => NodeLoading state UI
onError(err) => NodeError state UI
timeoutnumber10000Max load time (ms)

configureSecurity Options

OptionTypeDefaultDescription
trustedOriginsstring[][] (allow all)Whitelist of allowed origins
requireIntegritybooleanfalseBlock remotes without SRI hash
timeoutnumber10000Global timeout for all remotes
blockAllbooleanfalseKill 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();

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>&lt;script&gt;alert("xss")&lt;/script&gt;</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.

PermissionGrants 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
TemplateIncludes
baseSignals, html, css, mount, dev server
spaBase + Router, Store, Lazy loading
fullstackSPA + Forms, i18n, HTTP client, Auth, SSR
microfrontendFullstack + 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
OptionDescription
--template <name>Template to use (base, spa, fullstack, microfrontend)
--yesSkip prompts, use defaults
--helpShow help message