DEV Community

Cover image for Getting Started with Onefold: The Complete, Lightweight Reactive UI Framework
Md. Zahirul Haque
Md. Zahirul Haque

Posted on

Getting Started with Onefold: The Complete, Lightweight Reactive UI Framework

If you've ever started a new project and spent half the day wiring up a router, a state manager, a form library, and an HTTP client before writing a single line of actual app code — this one's for you.

Onefold is a TypeScript-first UI framework that ships everything you need in a single package. No virtual DOM, no compiler, zero dependencies, and the core is under 6kb gzipped.

In this article, I'll walk you through the core concepts and build a simple app from scratch so you can see what working with Onefold feels like.

Why Onefold?

Why choose Onefold over traditional setups? Here's the pitch in 30 seconds:

  • Fine-grained reactivity : Signal updates touch only the exact DOM node that depends on them. No diffing, no reconciliation. Updates are O(1).
  • Real DOM : No virtual DOM abstraction. The html tagged template builds actual DOM nodes with reactive bindings.
  • Secure by default : Dynamic text safely binds to textContent instead of risking innerHTML. This structural design completely eliminates cross-site scripting (XSS) risks without needing eval(), keeping you 100% CSP-compliant.
  • Complete toolkit : Routing, state management, forms with validation, HTTP client, i18n, theming, SSR, accessibility, microfrontend support... all included.
  • Tiny : Core is ~6kb gzipped. Full bundle with all features is ~16kb.
  • Zero dependencies : The entire framework has 0 runtime dependencies.

Setting Up Your First Project

The fastest way to get started:

npm create onefold@latest my-app
cd my-app
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

This scaffolds a SPA with routing, a dev server with live reload, and a production build script. Open http://localhost:3000 and you're running.

The generated project structure looks like:

my-app/
├── src/
│   ├── main.ts           # Entry point + router setup
│   ├── components/       # Reusable UI components
│   ├── routes/           # Page components
│   └── state/            # Signals & stores
├── public/
│   └── images/logo.svg   # Static assets
├── index.html
├── style.css             # Global styles
├── build.mjs             # Production build (esbuild)
├── dev.mjs               # Dev server + livereload
├── tsconfig.json
└── package.json
Enter fullscreen mode Exit fullscreen mode

Available templates: base, spa, fullstack, and microfrontend.

Core Concept #1: Signals

Signals are the heart of Onefold's reactivity. A signal is a reactive value — when it changes, anything that depends on it updates automatically.

import { createSignal } from 'onefold';

const count = createSignal(0);

// Read
count();          // 0

// Write
count.set(5);
count.set(n => n + 1);  // updater function

// Read without tracking (won't subscribe effects)
count.peek();
Enter fullscreen mode Exit fullscreen mode

You also get computed signals (derived values) and effects (side effects that re-run on change):

import { createSignal, createComputed, createEffect } from 'onefold';

const firstName = createSignal('Jane');
const lastName = createSignal('Doe');

// Automatically recomputes when firstName or lastName change
const fullName = createComputed(() => `${firstName()} ${lastName()}`);

// Runs whenever fullName changes
createEffect(() => {
  console.log(`Name is now: ${fullName()}`);
});
Enter fullscreen mode Exit fullscreen mode

Core Concept #2: Templates

UI is built with the html tagged template literal. It produces real DOM nodes — no compilation step needed.

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)}>-</button>
      <button onclick=${() => count.set(n => n + 1)}>+</button>
    </div>
  `;
}

mount(Counter(), document.getElementById('app')!);
Enter fullscreen mode Exit fullscreen mode

The #1 Rule: Wrap Signal Reads in () =>

This is the most important thing to remember:

// WRONG — renders once, never updates
html`<p>${count()}</p>`

// CORRECT — updates reactively when count changes
html`<p>${() => count()}</p>`
Enter fullscreen mode Exit fullscreen mode

The () => wrapper creates a reactive binding. Without it, you just interpolate the current value once and it's static forever.

Core Concept #3: Components

Components are just plain functions that return Node. No classes, no decorators, no special registration.

import { html } from 'onefold';

function Greeting(name: string): Node {
  return html`<p>Hello, ${name}!</p>`;
}

function App(): Node {
  return html`
    <div>
      ${Greeting('World')}
    </div>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Building a Todo App

Let's put it all together with a practical example. We'll build a reactive todo list with filtering.

State Management

// src/state/todos.ts
import { createSignal, createComputed } from 'onefold';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

type Filter = 'all' | 'active' | 'completed';

export const todos = createSignal<Todo[]>([]);
export const filter = createSignal<Filter>('all');

export const filteredTodos = createComputed(() => {
  const items = todos();
  const mode = filter();
  switch (mode) {
    case 'active': return items.filter(t => !t.completed);
    case 'completed': return items.filter(t => t.completed);
    default: return items;
  }
});

export const activeCount = createComputed(
  () => todos().filter(t => !t.completed).length
);

export function addTodo(text: string): void {
  const trimmed = text.trim();
  if (!trimmed) return;
  todos.set(prev => [...prev, {
    id: crypto.randomUUID(),
    text: trimmed,
    completed: false,
  }]);
}

export function toggleTodo(id: string): void {
  todos.set(prev =>
    prev.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
  );
}

export function removeTodo(id: string): void {
  todos.set(prev => prev.filter(t => t.id !== id));
}
Enter fullscreen mode Exit fullscreen mode

Input Component

// src/components/TodoInput.ts
import { createSignal, html } from 'onefold';
import { addTodo } from '../state/todos';

export function TodoInput(): Node {
  const input = createSignal('');

  const handleSubmit = (e: Event) => {
    e.preventDefault();
    addTodo(input());
    input.set('');
  };

  return html`
    <form onsubmit=${handleSubmit}>
      <input
        type="text"
        placeholder="What needs to be done?"
        value=${() => input()}
        oninput=${(e: Event) => input.set((e.target as HTMLInputElement).value)}
      />
      <button type="submit" disabled=${() => !input().trim()}>Add</button>
    </form>
  `;
}
Enter fullscreen mode Exit fullscreen mode

List Component

// src/components/TodoList.ts
import { html } from 'onefold';
import { filteredTodos, toggleTodo, removeTodo } from '../state/todos';

export function TodoList(): Node {
  return html`
    <ul>
      ${() => filteredTodos().map(todo => html`
        <li class=${todo.completed ? 'completed' : ''}>
          <input
            type="checkbox"
            checked=${todo.completed}
            onchange=${() => toggleTodo(todo.id)}
          />
          <span>${todo.text}</span>
          <button onclick=${() => removeTodo(todo.id)}>x</button>
        </li>
      `)}
    </ul>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Filter Component

// src/components/TodoFilter.ts
import { html } from 'onefold';
import { filter, activeCount } from '../state/todos';

export function TodoFilter(): Node {
  return html`
    <div class="filters">
      <button
        class=${() => filter() === 'all' ? 'active' : ''}
        onclick=${() => filter.set('all')}
      >All</button>
      <button
        class=${() => filter() === 'active' ? 'active' : ''}
        onclick=${() => filter.set('active')}
      >Active</button>
      <button
        class=${() => filter() === 'completed' ? 'active' : ''}
        onclick=${() => filter.set('completed')}
      >Completed</button>
      <span>${() => activeCount()} items left</span>
    </div>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Putting It Together

// src/main.ts
import { html, mount, css } from 'onefold';
import { TodoInput } from './components/TodoInput';
import { TodoList } from './components/TodoList';
import { TodoFilter } from './components/TodoFilter';

const styles = css`
  .todo-app {
    max-width: 520px;
    margin: 60px auto;
    background: #ffffff;
    border-radius: 16px;
    padding: 40px 36px;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  }
  h1 {
    font-size: 32px;
    font-weight: 700;
    color: #1e293b;
    margin-bottom: 24px;
  }
  form {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
  }
  input[type="text"] {
    flex: 1;
    padding: 12px 16px;
    font-size: 15px;
    border: 1.5px solid #e2e8f0;
    border-radius: 10px;
    outline: none;
    transition: border-color 0.2s;
  }
  input[type="text"]:focus {
    border-color: #3b82f6;
  }
  button {
    padding: 10px 20px;
    background: #3b82f6;
    color: white;
    border: none;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s;
  }
  button:hover { background: #2563eb; }
  button:disabled { background: #cbd5e1; cursor: not-allowed; }
  .filters {
    display: flex;
    gap: 6px;
    align-items: center;
    margin-bottom: 20px;
  }
  .filters button {
    padding: 7px 16px;
    background: white;
    color: #64748b;
    border: 1.5px solid #e2e8f0;
    border-radius: 8px;
    font-size: 13px;
  }
  .filters button:hover { border-color: #3b82f6; color: #3b82f6; }
  .filters button.active { background: #3b82f6; color: white; border-color: #3b82f6; }
  .filters span { margin-left: auto; font-size: 13px; color: #94a3b8; }
  ul {
    list-style: none;
    padding: 0;
    margin: 0;
  }
  li {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 8px;
    border-bottom: 1px solid #f1f5f9;
    font-size: 15px;
  }
  li:last-child { border-bottom: none; }
  li:hover { background: #f8fafc; border-radius: 8px; }
  li.completed span { text-decoration: line-through; color: #94a3b8; }
  li input[type="checkbox"] { width: 18px; height: 18px; accent-color: #3b82f6; }
  li span { flex: 1; }
  li button {
    padding: 4px 10px;
    background: none;
    color: #ef4444;
    border: none;
    font-size: 14px;
    opacity: 0;
    transition: opacity 0.15s;
  }
  li:hover button { opacity: 1; }
  li button:hover { background: #fef2f2; border-radius: 6px; }
`;

function App(): Node {
  return html`
    <div class=${styles.scope}>
      <div class="todo-app">
        <h1>Todos</h1>
        ${TodoInput()}
        ${TodoFilter()}
        ${TodoList()}
      </div>
    </div>
  `;
}

mount(App(), document.getElementById('app')!);
Enter fullscreen mode Exit fullscreen mode

That's it. No boilerplate, no providers wrapping your app, and the scoped CSS keeps all the todo styling contained — nothing bleeds into the rest of the page.

Todo Preview

Routing

Onefold includes a client-side router out of the box:

import { Router, navigate, currentRoute, Link, html, mount } from 'onefold';

function Nav(): Node {
  return html`
    <nav>
      ${Link('/', 'Home', () => currentRoute() === '/' ? 'active' : '')}
      ${Link('/about', 'About', () => currentRoute() === '/about' ? 'active' : '')}
    </nav>
  `;
}

function Home(): Node { return html`<h1>Home</h1>`; }
function About(): Node { return html`<h1>About</h1>`; }
function NotFound(): Node { return html`<h1>404 — Not Found</h1>`; }

function App(): Node {
  return html`
    <div>
      ${Nav()}
      ${Router([
        { path: '/', view: () => Home() },
        { path: '/about', view: () => About() },
        { path: '/users/:id', view: (params) => UserProfile(params.id) },
      ], () => NotFound())}
    </div>
  `;
}

mount(App(), document.getElementById('app')!);
Enter fullscreen mode Exit fullscreen mode

It supports dynamic params (:id), nested routes with layouts, hash-based routing for static hosting, and programmatic navigation via navigate('/path').

What Else Is Included?

This is where Onefold really differentiates itself. All of these are part of the same package, available via tree-shakeable sub-path imports:

// Forms with validation
import { createForm, required, email, minLength } from 'onefold/form';

// HTTP client with interceptors
import { createHttpClient } from 'onefold/http';

// Internationalization
import { createI18n } from 'onefold/i18n';

// Persisted state (localStorage/sessionStorage)
import { createPersisted } from 'onefold/persist';

// Theming with CSS variables
import { createTheme } from 'onefold/theme';

// Server-side rendering
import { renderHTML } from 'onefold/ssr';

// WebSocket & SSE
import { createWebSocket, createEventSource } from 'onefold/stream';

// Microfrontend loading with security
import { loadRemote } from 'onefold/remote';

// Accessibility utilities
import { FocusTrap, announce } from 'onefold/a11y';

// Scoped CSS
import { css } from 'onefold';

// Virtual list for large datasets
import { VirtualList } from 'onefold/virtual-list';

// Lazy loading + code splitting
import { lazy } from 'onefold';
import { Suspense } from 'onefold/suspense';
Enter fullscreen mode Exit fullscreen mode

You only bundle for what you import. The core (signals + templates + routing + store) is ~6kb. Import additional modules as needed, each tree-shakes independently.

Scoped CSS

Components can have scoped styles that don't leak to the rest of the page. The css tagged template returns an object with a .scope property — a unique class name (like nf-0, nf-1) that you apply to your root element. All selectors in your CSS block get automatically prefixed with that scope class:

import { html, css } from 'onefold';

const styles = css`
  .card { padding: 16px; border: 1px solid #ddd; border-radius: 8px; }
  .card h2 { margin: 0 0 8px; }
  button { padding: 8px 16px; border-radius: 6px; cursor: pointer; }
`;

function Card(title: string, content: string): Node {
  return html`
    <div class=${styles.scope}>
      <div class="card">
        <h2>${title}</h2>
        <p>${content}</p>
        <button>Action</button>
      </div>
    </div>
  `;
}
Enter fullscreen mode Exit fullscreen mode

The .card, h2, and button styles here are fully scoped — they won't affect any other .card or button on the page. Under the hood, Onefold generates something like .nf-0 .card { ... } and injects a <style> into <head> (deduplicated — identical templates reuse the same scope).

Microfrontends — Built In

This is one of Onefold's standout features. If your organization has multiple teams shipping different parts of a frontend, Onefold handles remote module loading with built-in security controls:

import { html, mount } from 'onefold';
import { loadRemote, configureSecurity, preloadRemote } from 'onefold/remote';

// At app startup: define what origins you trust
configureSecurity({
  trustedOrigins: ['https://widgets.company.com'],
  requireIntegrity: true,
  timeout: 10000,
});

// Load a remote widget — returns a component function
const BillingWidget = loadRemote({
  url: 'https://widgets.company.com/billing.js',
  integrity: 'sha384-abc123...',
  isolate: 'shadow',  // 'none' | 'shadow' | 'iframe'
  fallback: () => html`<p>Loading billing...</p>`,
  onError: (err) => html`<p>Widget unavailable: ${err.message}</p>`,
});

function Dashboard(): Node {
  return html`
    <div>
      <h1>Dashboard</h1>
      ${BillingWidget({ accountId: 'ACCT-7291' })}
    </div>
  `;
}

mount(Dashboard(), document.getElementById('app')!);
Enter fullscreen mode Exit fullscreen mode

Key features:

  • Origin allowlisting : only modules from explicitly trusted origins can load
  • Subresource Integrity (SRI) : optionally require hash verification
  • CSS isolation : Shadow DOM isolates remote widget styles from the host
  • Graceful fallbacks : loading states and error handling built in
  • Prefetching : preloadRemote(url) lets you prefetch on hover for faster interaction
  • Independent deployment : each remote is a standalone ES module, deployable by a different team on a different schedule

The create-onefold CLI even has a microfrontend template that scaffolds a full host shell + remote widgets setup:

npm create onefold@latest my-platform --template microfrontend
Enter fullscreen mode Exit fullscreen mode

CDN Usage (No Build Step)

You can use Onefold directly in the browser without any build tooling:

<!DOCTYPE html>
<html>
<body>
  <div id="app"></div>
  <script type="module">
    import { createSignal, html, mount } from 'https://unpkg.com/onefold';

    function App() {
      const count = createSignal(0);
      return html`
        <button onclick=${() => count.set(n => n + 1)}>
          Clicked ${() => count()} times
        </button>
      `;
    }

    mount(App(), document.getElementById('app'));
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

No npm, no bundler, just a <script type="module"> tag.

How Does It Compare?

Onefold React Svelte Solid
Virtual DOM No Yes No No
Compiler required No No* Yes Yes
Bundle size (core) ~6kb ~44kb ~2kb ~7kb
Reactivity Fine-grained signals Re-render entire component Compiler-assisted Fine-grained signals
Runtime dependencies 0 3 0 0
Built-in router Yes No No No
Built-in forms Yes No No No
Built-in HTTP client Yes No No No
Built-in i18n Yes No No No
Built-in microfrontends Yes No No No
SSR support Yes Yes Yes Yes

*React technically doesn't require a compiler, but JSX practically does.

The trade-off is clear: Onefold gives you a complete toolkit with zero config in exchange for being a newer, less battle-tested ecosystem.

Deployment

The production build outputs static files to dist/:

npm run build
Enter fullscreen mode Exit fullscreen mode

Deploy the dist/ folder anywhere: Vercel, Netlify, GitHub Pages, S3, any static host. For hash-based routing on static hosts, add one line before your Router:

import { configureRouter } from 'onefold';
configureRouter({ hash: true });
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

Building features should be the fun part, but modern web development often forces you to waste hours stitching together five different libraries just to get started. Onefold flips the script, giving you everything you need right out of the box, without the bloated footprint.

The core concept is incredibly straightforward:

  1. Signals manage your reactive state naturally.
  2. html templates build real DOM elements with built-in reactive bindings.
  3. Components are just plain functions that return a Node
  4. Wrap signal reads in () => to make them reactive in templates

Want to take it for a spin? Kick off a project in seconds:

npm create onefold@latest my-app
Enter fullscreen mode Exit fullscreen mode

Links:


Onefold is MIT licensed and open source. It's at version 0.1.5 — early but functional, with a growing set of examples and documentation.

Top comments (0)