A lightweight JSX/TSX framework for building fast, reactive web applications with direct DOM manipulation - no virtual DOM overhead.
- What is Snapp?
- Core Concepts
- Installation & Setup
- Quick Start
- API Reference
- Type Definitions
- Core Architecture
- Coming Soon
- Contributing
Snapp is a lightweight JavaScript framework that compiles JSX/TSX directly to native DOM operations. It's designed for developers who want:
- ✅ JSX/TSX syntax you already know
- ✅ Direct DOM control without abstraction layers
- ✅ Reactive state that updates elements individually
- ✅ Zero virtual DOM - just compiled JavaScript
- ✅ TypeScript support out of the box
- ✅ Automatic memory management with built-in cleanup
| Feature | Snapp | Virtual DOM Frameworks |
|---|---|---|
| Learning Curve | Native DOM skills | New abstractions |
| Performance | Direct DOM | Reconciliation overhead |
| Debugging | Browser DevTools | Framework DevTools needed |
| Memory | Efficient cleanup | GC dependent |
// What you write:
<button onClick={() => alert("Hi")}>Click me</button>;
// Gets compiled to:
snapp.create("button", { onClick: () => alert("Hi") }, "Click me");The key difference in Snapp is how you handle reactive values:
const count = snapp.dynamic(0);
// ❌ WRONG - accesses value once at render time
<p>{count.value}</p>
// ✅ CORRECT - wrapped in arrow function for reactivity
<p>{() => count.value}</p>
// When count updates, ONLY this text updates:
count.update(5);Why arrow functions? Snapp tracks dynamic value access inside the function. When you call count.update(), it re-executes that function and updates just that specific text node, attribute, or style.
const staticText = "Hello";
const dynamicText = snapp.dynamic("World");
<div>
{staticText} {/* Static - never changes */}
{() => dynamicText.value} {/* Dynamic - updates when dynamicText changes */}
</div>;The fastest way to get started:
npm create snapp-app my-app
cd my-app
npm install
npm run devThis creates a ready-to-go project with this structure:
my-app/
├── views/ # Your JSX/TSX source files
│ ├── index.jsx # Main entry view
│ └── components/ # Reusable components
│ └── Links.jsx
├── public/ # Build output (auto-generated by snapp-kit)
│ └── index.js # Compiled JavaScript
├── style/
│ └── style.css # Your CSS
├── assets/ # Static files (images, fonts, etc.)
├── index.html # Entry HTML file
└── package.json
Open http://localhost:9000 and start building!
1. Install packages:
npm install @snappjs/core
npm install -D snapp-kit2. Create your project structure:
my-project/
├── views/
│ └── index.jsx
├── public/
├── index.html
└── package.json
3. Create index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Snapp App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="app"></div>
<script type="module" src="public/index.js"></script>
</body>
</html>4. Create views/index.jsx:
import snapp from "@snappjs/core";
const App = () => {
return <h1>Hello Snapp!</h1>;
};
const app = snapp.select("#app");
snapp.render(app, App());5. Add scripts to package.json:
{
"type": "module",
"scripts": {
"dev": "npx snapp build -W",
"build": "npx snapp build -M"
},
"dependencies": {
"@snappjs/core": "^3.0.0"
},
"devDependencies": {
"snapp-kit": "^3.1.0"
}
}6. Run the dev server:
npm run devsnapp-kit compiles views/index.jsx → public/index.js, starts a live server, and auto-refreshes on changes.
import snapp from "@snappjs/core";
const Counter = () => {
const count = snapp.dynamic(0);
return (
<>
<h2>Count: {() => count.value}</h2>
<button onClick={() => count.update(count.value + 1)}>Increment</button>
</>
);
};
const app = snapp.select("#app");
snapp.render(app, Counter());import snapp from "@snappjs/core";
const FormExample = () => {
const name = snapp.dynamic("");
return (
<div>
<input
type="text"
value={() => name.value}
onInput={(e) => name.update(e.target.value)}
placeholder="Type your name..."
/>
<p>Hello, {() => name.value || "stranger"}!</p>
</div>
);
};
const app = snapp.select("#app");
snapp.render(app, FormExample());import snapp from "@snappjs/core";
const TodoApp = () => {
const todos = snapp.dynamic([]);
const input = snapp.dynamic("");
const addTodo = () => {
const newTodos = [...todos.value, input.value];
todos.update(newTodos);
input.update("");
};
return (
<div>
<h1>Todos</h1>
<input
value={() => input.value}
onInput={(e) => input.update(e.target.value)}
placeholder="Add a todo..."
/>
<button onClick={addTodo}>Add</button>
<ul>{() => todos.value.map((todo) => <li>{todo}</li>)}</ul>
</div>
);
};
const app = snapp.select("#app");
snapp.render(app, TodoApp());import snapp from "@snappjs/core";
const Card = ({ title, children }) => {
return (
<div style={{ border: "1px solid #ccc", padding: "16px", borderRadius: "8px" }}>
<h3>{title}</h3>
{children}
</div>
);
};
const App = () => {
return (
<div>
<Card title="Welcome">
<p>This is a card component!</p>
</Card>
</div>
);
};
const app = snapp.select("#app");
snapp.render(app, App());Creates a DOM element, component, or fragment.
create(
element: string | Component | "<>",
props?: SnappProps,
...children: SnappChild[]
): Element | DocumentFragmentUsage:
// HTML element
snapp.create("div", { id: "main" }, "Hello");
// Component
const MyComponent = (props) => <div>{props.children}</div>;
snapp.create(MyComponent, {}, "Hello");
// Fragment
snapp.create("<>", null, <div>A</div>, <div>B</div>);
// In JSX, you use the angle brackets directly:
<div id="main">Hello</div>
<MyComponent>Hello</MyComponent>
<>
<div>A</div>
<div>B</div>
</>Renders a component or element to the DOM.
render(
target: Element,
component: Element | DocumentFragment | string | number,
type?: "replace" | "append" | "prepend" | "before" | "after" | null,
callback?: (success: boolean) => void
): voidParameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
target |
Element | required | The DOM element to render to |
component |
Element | DocumentFragment | string | number | required | What to render |
type |
string | null | "replace" |
Where/how to render (pass null to replace children) |
callback |
Function | optional | Called with true/false on success/failure |
Render Types:
const content = <div>New content</div>;
const target = snapp.select("#app");
// Replace target's children (recommended default)
snapp.render(target, content, null);
// Replace the target element itself
snapp.render(target, content, "replace");
// Add as first child
snapp.render(target, content, "prepend");
// Add as last child
snapp.render(target, content, "append");
// Insert before target
snapp.render(target, content, "before");
// Insert after target
snapp.render(target, content, "after");
// With callback
snapp.render(target, content, null, (success) => {
if (success) console.log("Rendered!");
});Tip: Pass
nullas the third argument to replace the element's children (most common use case). Using"replace"replaces the element itself, which removes it from the DOM.
Creates a reactive state value.
dynamic<T = any>(initialValue?: T): DynamicValue<T>Properties:
interface DynamicValue<T> {
readonly value: T; // Get current value
update: (newValue: T) => void; // Set new value
}Usage:
// Create dynamic state
const count = snapp.dynamic(0);
const user = snapp.dynamic({ name: "John", age: 30 });
const isVisible = snapp.dynamic(true);
// Use in JSX with arrow function
<div>
Count: {() => count.value}
Name: {() => user.value.name}
Visible: {() => (isVisible.value ? "Yes" : "No")}
</div>;
// Update state
count.update(5);
user.update({ name: "Jane", age: 25 });
isVisible.update(false);
// Update based on previous value
count.update(count.value + 1);Dynamic State in Different Contexts:
// Text Content
const message = snapp.dynamic("Hello");
<p>{() => message.value}</p>;
// Attributes
const id = snapp.dynamic("item-1");
<div id={() => id.value}></div>;
// Styles
const color = snapp.dynamic("blue");
<p style={{ color: () => color.value }}>Colored text</p>;
// Event Handlers (regular functions)
const handleClick = () => alert("clicked");
<button onClick={handleClick}>Click</button>;
// Conditional Rendering
const showHeader = snapp.dynamic(true);
<>{() => (showHeader.value ? <header>Title</header> : null)}</>;Listens for DOM ready events.
on(event: string, callback: () => void): voidUsage:
// Wait for DOM to be ready before accessing elements
snapp.on("DOM", () => {
const element = snapp.select("#myElement");
console.log("Element is in DOM:", element);
});
// snapp.on("DOM") is called after snapp.render() completes
const App = () => {
return <h2 id="myElement">Title</h2>;
};
snapp.render(snapp.select("#app"), App(), null, () => {
snapp.on("DOM", () => {
console.log("Now we can access #myElement");
});
});Query DOM elements using CSS selectors.
select(selector: string | string[]): Element | Element[] | null
selectAll(selector: string | string[]): NodeListOf<Element> | NodeListOf<Element>[] | nullUsage:
// Single selector
const element = snapp.select("#myId");
const element = snapp.select(".myClass");
// Multiple selectors (returns array)
const elements = snapp.select(["#id1", "#id2"]);
// Select all matching
const items = snapp.selectAll(".item");
const items = snapp.selectAll(".item, .product");
// Multiple selectors for selectAll
const results = snapp.selectAll([".class1", ".class2"]);
// Returns array of NodeLists
// Returns null if not found
const missing = snapp.select("#doesNotExist"); // nullManage element styles programmatically.
applystyle(
element: Element | Element[],
styles: Record<string, string | number>
): void
removestyle(
element: Element | Element[],
styles?: Record<string, string | number> | boolean
): voidUsage:
const box = snapp.select("#box");
// Apply styles
snapp.applystyle(box, {
backgroundColor: "blue",
padding: "20px",
"border-radius": "8px", // CSS property names with hyphens work
});
// Remove specific styles
snapp.removestyle(box, {
backgroundColor: "blue",
padding: "20px",
});
// Remove all styles
snapp.removestyle(box, true);
// Multiple elements
const boxes = snapp.selectAll(".box");
snapp.applystyle(boxes, { color: "red" });
snapp.removestyle(boxes, { color: "red" });Remove elements from the DOM.
remove(items: Element | Element[]): voidUsage:
const element = snapp.select("#myElement");
snapp.remove(element);
// Remove multiple
const items = snapp.selectAll(".item");
snapp.remove(items);Simple health check to verify Snapp is loaded correctly.
running(): string // Returns "Snapp is running!"Usage:
console.log(snapp.running()); // "Snapp is running!"type SnappChild =
| string
| number
| Element
| DocumentFragment
| SnappComponent
| SnappChild[]
| null
| undefined
| boolean;Represents anything that can be rendered as a child element.
type SnappProps = Record<string, any>;Props object for components. Can contain any key-value pairs.
type SnappComponent<P extends SnappProps = SnappProps> = (
props: P & { children?: SnappChild[] }
) => Element | DocumentFragment;A component function that takes props and returns a DOM element or fragment.
Example:
interface ButtonProps {
label: string;
onClick?: (e: Event) => void;
}
const MyButton: SnappComponent<ButtonProps> = (props) => {
return <button onClick={props.onClick}>{props.label}</button>;
};interface DynamicValue<T = any> {
readonly value: T;
update: (newValue: T) => void;
}Reactive state container that notifies subscribers when updated.
type RenderType = "before" | "prepend" | "replace" | "append" | "after";Determines where elements are rendered relative to the target.
type EventHandler = (event: Event) => void;Function called when an event fires.
Comprehensive attribute interface supporting:
- Global attributes:
id,class,style,title, etc. - Data attributes:
data-* - ARIA attributes:
aria-* - All event handlers:
onClick,onSubmit,onChange, etc.
- JSX Compilation → esbuild (via snapp-kit) compiles JSX to
snapp.create()calls - Element Creation →
snapp.create()builds native DOM elements - Dynamic Tracking → When you use
() => dynamicValue.value, Snapp tracks dependencies - Subscriptions → Each dynamic value tracks which elements depend on it
- Updates → When you call
update(), only affected text nodes/attributes/styles change - Cleanup → MutationObserver automatically cleans up when elements are removed
When you write:
const count = snapp.dynamic(0);
<p>{() => count.value}</p>;Snapp does the following:
- Start tracking: Set
track_dynamic = new Set() - Execute function: Call
() => count.value, which accesses thevaluegetter - Track access: Inside
valuegetter, add ID totrack_dynamic - Create subscription: Store the function and which dynamic values it depends on
- On update: When
count.update(5)is called, re-execute the function and update the text node
This is why arrow functions are essential - they defer execution so Snapp can track what's accessed.
Rather than adding listeners to every element, Snapp uses event delegation — a single listener per event type on the document, which is more efficient:
// Single listener per event type
document.addEventListener("click", eventTemplate);
// Template checks if target has snapp-e-click attribute
const elWithAttr = target.closest("[snapp-e-click]");
if (elWithAttr) {
const elementDataId = elWithAttr.getAttribute("snapp-data");
elementEvent["click"][elementDataId](event);
}MutationObserver watches for removed elements and automatically cleans up event listeners and dynamic subscriptions — no manual cleanup needed.
These features are planned for upcoming v3 releases:
Auto-updating DOM lists from arrays/objects, similar to .map() in React but with fine-grained DOM updates:
const items = snapp.state([
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
]);
<Render for={items} key="id">
{(item) => <li>{item.name}</li>}
</Render>
// Add, remove, update — DOM updates automatically
items.push({ id: 3, name: "Cherry" });
items.remove(1);Get a reference to a DOM element without querying:
const myDiv = snapp.ref();
<div ref={myDiv}>Hello</div>
// Access later
myDiv.current.style.color = "red";Automatically derived reactive values:
const count = snapp.dynamic(5);
const doubled = snapp.computed(() => count.value * 2);
<p>Doubled: {() => doubled.value}</p> // Updates when count changesRun side effects when dynamic values change:
const query = snapp.dynamic("");
snapp.effect([query], () => {
document.title = `Search: ${query.value}`;
});Cleaner conditional rendering:
<Show when={() => isLoggedIn.value}>
<Dashboard />
</Show>Client-side routing as a separate package:
import { Router, Route, Link } from "@snappjs/router";
const router = Router({
routes: [
{ path: "/", page: () => import("./pages/home.js") },
{ path: "/about", page: () => import("./pages/about.js") },
{ path: "/user/:id", page: () => import("./pages/user.js") },
],
});
router.mount(snapp.select("#app"));- Improved TypeScript JSX types — proper attribute types instead of
any - Error boundaries — catch rendering errors gracefully
We welcome contributions! Here's how to get involved:
# Clone the repo
git clone https://github.com/SnappJS/snapp.git
cd snapp
# Install dependencies
npm install
# Start development watch mode
npm run dev
# Build for production
npm run build- Use TypeScript for type safety
- Follow existing code patterns in
src/core.ts - Add JSDoc comments for public APIs
- Test with JSX examples
- Fork the repository
- Create a feature branch
- Make your changes with clear commit messages
- Ensure code compiles:
npm run build - Submit PR with description
MIT - See LICENSE file for details