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
- Quick Start
- API Reference
- Type Definitions
- Core Architecture
- 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>;
npm install @snappjs/core
<!DOCTYPE html> <html> <head> <title>My Snapp App</title> </head> <body> <div id="app"></div> <script type="module" src="src/index.js"></script> </body> </html>
// src/index.jsx import snapp from "@snappjs/core"; const App = () => { return <h1>Hello Snapp!</h1>; }; const app = document.getElementById("app"); snapp.render(app, App());
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> </> ); }; snapp.render(document.body, Counter());
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> ); }; snapp.render(document.body, TodoApp());
Creates a DOM element, component, or fragment.
create( element: string | Component | "<>", props?: SnappProps, ...children: SnappChild[] ): Element | DocumentFragment
Usage:
// 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", callback?: (success: boolean) => void ): void
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
target |
Element | required | The DOM element to render to |
component |
Element | DocumentFragment | string | number | required | What to render |
type |
string | "replace" |
Where/how to render |
callback |
Function | optional | Called with true/false on success/failure |
Render Types:
const content = <div>New content</div>; const target = document.getElementById("app"); // Replace target's children snapp.render(target, content, "replace"); // Default // 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, "replace", (success) => { if (success) console.log("Rendered!"); });
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): void
Usage:
// 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(document.body, App(), "replace", () => { // At this point, the DOM is ready 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>[] | null
Usage:
// 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"); // null
Manage element styles programmatically.
applystyle( element: Element | Element[], styles: Record<string, string | number> ): void removestyle( element: Element | Element[], styles?: Record<string, string | number> | boolean ): void
Usage:
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[]): void
Usage:
const element = snapp.select("#myElement"); snapp.remove(element); // Remove multiple const items = snapp.selectAll(".item"); snapp.remove(items);
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 → TypeScript/Babel 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
The heart of Snapp framework containing:
Key Functions:
create(element, props, ...children)- Creates DOM elements or componentsrender(target, component, type, callback)- Renders to DOMdynamic(initialValue)- Creates reactive stateon(event, callback)- Listens for DOM eventsselect(selector)/selectAll(selector)- DOM queriesapplystyle(element, styles)- Apply CSS stylesremovestyle(element, styles)- Remove CSS stylesremove(items)- Remove elements from DOM
Internal State Management:
// Counter for unique element IDs let dataId: number = 0; // Counter for dynamic state IDs let dynamicId: number = 1; // Tracks if DOM is ready let DOMReady: boolean = false; // Tracks which dynamic values are being accessed let track_dynamic: Set<string> | null = null; // Stores all dynamic values and their subscribers const dynamicData: Record< string, { value: any; subscribe: Map<Element, number[]>; } > = {}; // Maps elements to their dependent dynamic values const dynamicDependencies = new Map<Element, SubscribeData[]>(); // Event delegation system const eventListener: Record<string, EventHandler> = {}; const elementEvent: Record<string, Record<number, EventHandler>> = {};
SVG Support:
Snapp automatically detects SVG elements and uses createElementNS() instead of createElement():
const SVG_ELEMENTS = new Set([ "svg", "circle", "ellipse", "line", "path", "polygon", "polyline", "rect", "text", "g", "defs", "filter", "image", "use", "mask", "pattern", "linearGradient", "radialGradient", "stop", "animate", "animateTransform", ... ]);
Event Delegation:
Rather than adding listeners to every element, Snapp uses event delegation:
// 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) { // Get element's ID and call its handler const elementDataId = elWithAttr.getAttribute("snapp-data"); elementEvent["click"][elementDataId](event); }
This is more efficient than listeners on every element.
Memory Management:
MutationObserver watches for removed elements:
const observer = new MutationObserver((mutations) => { mutations.forEach((element) => { element.removedNodes.forEach((node) => { // Clean up event listeners if (node.getAttribute("snapp-e-click")) { delete eventEvent["click"][elementDataId]; } // Clean up dynamic dependencies if (node.getAttribute("snapp-dynamic")) { dynamicDependencies.delete(node); } }); }); });
Comprehensive TypeScript types for:
- Component types:
SnappComponent,SnappChild,SnappProps - State types:
DynamicValue,SubscribeData - Attribute types:
HTMLAttributes,IntrinsicElements - Handler types:
EventHandler,RenderType
SubscribeData Interface:
interface SubscribeData { type: "node" | "attr" | "style"; // What's being updated temp: Function; // Function to re-execute subscribe: string[]; // Dynamic IDs this depends on node?: Text; // Text node being updated attr?: string; // Attribute name being updated prop?: string; // Style property being updated }
This tracks what type of update happens when a dynamic value changes.
Ambient type declarations for JSX:
declare global { namespace JSX { interface IntrinsicElements { // All HTML/SVG elements with their attributes div: HTMLAttributes<HTMLDivElement>; button: HTMLAttributes<HTMLButtonElement>; svg: HTMLAttributes<SVGSVGElement>; // ... etc } interface ElementAttributesProperty { props: any; } interface ElementChildrenAttribute { children: any; } } }
This enables TypeScript to recognize JSX syntax and provide autocomplete for elements and attributes.
import snapp from "./core"; export default snapp; export type { DynamicValue, SnappProps, SnappComponent, SnappChild };
Single entry point that re-exports the framework and types.
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.
We welcome contributions! Here's how to get involved:
# 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