Flash
Fine-grained reactive JSX framework with zero VDOM overhead
Flash is a lightweight, performant JSX framework built on signals for fine-grained reactivity. Write familiar JSX, get blazing fast updates with direct DOM manipulation. First class enter-exit animations and rollbacks.
Features
Client-Side
- Fine-grained reactivity - Powered by Preact Signals, updates only what changed
- No Virtual DOM - Direct DOM manipulation for maximum performance
- Flexible animations - Use any library (Framer Motion, GSAP, etc.) or vanilla CSS/classes
- DOM resurrection - Automatic animation reversal for rapid toggling
- Keyed list rendering - SolidJS-style efficient list updates (insert/update/delete/move)
- View Transitions API - Smooth page transitions out of the box
- Frame scheduler - Prevent layout thrashing with read/update/render phases
- FLIP animations - Built-in utilities for performant layout animations
- Auto-animate - Automatic layout animations (in progress)
- First-class exit animations -
onBeforeExitpauses unmounting (save data, animations, etc.) - Context API - Share state across component trees
- Built-in Router - File-based routing with lazy loading (work in progress)
- Tiny bundle size - No compiler required, minimal runtime
- Reactive props - Props can be signals for automatic updates
Server-Side
- Server-Side Rendering - Three rendering strategies (sync, async, streaming)
- Parallel async resolution - Resolves async components efficiently
- Pre-rendering & caching - Built-in static site generation
- Progressive enhancement - Stream HTML for better perceived performance
- Automatic XSS protection - HTML escaping by default
Installation
npm install @mateosuarezdev/flash @preact/signals-coreConfigure your tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@mateosuarezdev/flash/runtime"
}
}Quick Start
import { render, onMount } from "@mateosuarezdev/flash";
import { signal } from "@preact/signals-core";
const count = signal(0);
function Counter() {
onMount(() => {
console.log("Counter mounted!");
});
return (
<div>
<h1>Count: {() => count.value}</h1>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
render(<Counter />, document.getElementById("app"));Core Concepts
Reactive Boundaries
Wrap expressions in functions to create reactive boundaries that update automatically when signals change:
import { signal } from "@preact/signals-core";
const name = signal("World");
function Greeting() {
return (
<div>
{/* This updates when name changes */}
<h1>Hello {() => name.value}!</h1>
{/* Conditional rendering */}
{() =>
name.value === "World" ? (
<p>Welcome!</p>
) : (
<p>Hello, {() => name.value}!</p>
)
}
</div>
);
}Reactive Props
Props can be functions that automatically update:
const isDark = signal(false)
<button
className={() => isDark.value ? 'dark' : 'light'}
disabled={() => !isDark.value}
>
Toggle
</button>Keyed Lists
Use the key prop for efficient list rendering:
const items = signal([
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Cherry" },
]);
function List() {
return (
<ul>
{() => items.value.map((item) => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Flash efficiently handles:
- Inserts - New items are rendered and inserted
- Removals - Deleted items are unmounted and removed
- Reordering - DOM nodes are moved to match new order
- Updates - Existing items are reused (no re-render)
Lifecycle Hooks
onMount
Runs after the component is mounted to the DOM:
function Component() {
let ref: HTMLDivElement;
onMount(() => {
console.log("Element:", ref);
// Fetch data, start animations, etc.
});
return <div ref={(el) => (ref = el)}>Content</div>;
}onUnmount
Runs when the component is removed from the DOM:
function Component() {
onUnmount(() => {
console.log("Cleaning up...");
// Cancel subscriptions, clear timers, etc.
});
return <div>Content</div>;
}onBeforeExit
Runs before unmounting - a first-class feature, not a workaround. You can pause the entire unmount process to:
- Play exit animations
- Save form data or state
- Confirm user actions
- Clean up async operations
- Anything you need before removal
The unmount tree is held until your async callback completes:
import { animate } from "framer-motion";
function FadeBox() {
let ref: HTMLElement;
onBeforeExit(async (token) => {
// Play exit animation
const animation = animate(ref, { opacity: 0 }, { duration: 0.3 });
// Handle cancellation (e.g., rapid toggling)
token.onCancel(() => {
animation.stop();
animate(ref, { opacity: 1 }, { duration: 0.3 });
});
await animation.finished;
// Component won't unmount until animation completes
});
return <div ref={(el) => (ref = el)}>Fading content</div>;
}Save data before unmounting:
function Form() {
const formData = signal({ name: "", email: "" });
onBeforeExit(async () => {
// Save to localStorage or API
await saveFormData(formData.value);
console.log("Data saved before unmount!");
});
return <form>...</form>;
}Cancellation Example:
const show = signal(true)
// Rapid toggling: true → false → true
// Flash will cancel the exit animation and reuse the DOM!
<button onClick={() => show.value = !show.value}>
Toggle
</button>
{() => show.value && <FadeBox />}Context API
Share state across component trees without prop drilling:
import { createContext, useContext } from "@mateosuarezdev/flash";
const ThemeContext = createContext({ theme: "light" });
function App() {
ThemeContext.provide({ theme: "dark" });
return <Child />;
}
function Child() {
const theme = useContext(ThemeContext);
console.log(theme); // { theme: 'dark' }
return <div>Theme: {theme.theme}</div>;
}Router (Work in Progress)
Flash includes a built-in router with reactive pathname tracking and custom URL change events:
import {
Router,
pathname,
push,
replace,
back,
onUrlChange,
} from "@mateosuarezdev/flash/router";
function App() {
return (
<Router>
<nav>
<a
href="/"
onClick={(e) => {
e.preventDefault();
push("/");
}}
>
Home
</a>
<a
href="/about"
onClick={(e) => {
e.preventDefault();
push("/about");
}}
>
About
</a>
</nav>
{/* Reactive routing based on pathname signal */}
{() => {
switch (pathname.value) {
case "/":
return <Home />;
case "/about":
return <About />;
default:
return <NotFound />;
}
}}
</Router>
);
}Core Router Features
Reactive Pathname Tracking:
import { pathname } from "@mateosuarezdev/flash/router";
// pathname is a signal that updates on navigation
function NavBar() {
return (
<nav>
<a class={() => (pathname.value === "/" ? "active" : "")} href="/">
Home
</a>
</nav>
);
}Programmatic Navigation:
import { push, replace, back } from "@mateosuarezdev/flash/router";
// Navigate to a new route
push("/dashboard");
// Replace current route (no history entry)
replace("/login");
// Go back in history
back();URL Change Events:
import { onUrlChange } from "@mateosuarezdev/flash/router";
function Component() {
onUrlChange((event) => {
if (event) {
console.log("Navigation:", event.action); // 'pushState' | 'replaceState' | 'popstate' | 'beforeunload'
console.log("From:", event.oldURL?.pathname);
console.log("To:", event.newURL?.pathname);
// Prevent navigation by calling event.preventDefault()
// Great for unsaved changes warnings
}
});
return <div>Content</div>;
}Custom UrlChangeEvent:
The router automatically intercepts and dispatches custom urlchangeevent for all navigation:
pushState- New history entryreplaceState- Replace current entrypopstate- Back/forward navigationbeforeunload- Page close/reload
Events can be prevented to block navigation (useful for form guards, unsaved changes, etc.)
Current Features:
- Reactive pathname signal
- Programmatic navigation (push, replace, back)
- Custom URL change events with prevention
- History state tracking
- Lifecycle integration (auto-cleanup with onUnmount)
Planned Features:
- File-based routing with automatic route generation
- Lazy loading and code splitting helpers
- Integrated View Transitions API support
- Nested routes and layouts
- Route guards and middleware
- Path parameter extraction
- Link component with active state
Note: The router is currently in active development. The current implementation provides low-level primitives for building routing solutions!
Animations & Performance
Flash is designed to be a batteries-included framework with powerful animation and performance utilities built right in.
Flexible Animation Options
Flash gives you complete freedom to animate however you want:
1. Use Any Animation Library
import { animate } from "framer-motion";
import { animate as animateJsAnimate } from "animejs";
function Component() {
let ref: HTMLElement;
onMount(() => {
// Framer Motion
animate(ref, { x: 100 }, { duration: 0.3 });
// Anime.js, GSAP, Motion One, or any library!
});
onBeforeExit(async (token) => {
const animation = animate(ref, { opacity: 0 }, { duration: 0.3 });
token.onCancel(() => {
animation.stop();
animate(ref, { opacity: 1 }, { duration: 0.3 });
});
await animation.finished;
});
return <div ref={(el) => (ref = el)}>Animated</div>;
}2. Vanilla CSS Transitions
function Component() {
let ref: HTMLElement;
onMount(() => {
ref.style.transition = "opacity 300ms";
ref.style.opacity = "0";
setTimeout(() => (ref.style.opacity = "1"), 10);
});
onBeforeExit(async () => {
ref.style.opacity = "0";
await new Promise((resolve) => setTimeout(resolve, 300));
});
return <div ref={(el) => (ref = el)}>CSS Animated</div>;
}3. Toggle CSS Classes
function Component() {
let ref: HTMLElement;
onMount(() => {
requestAnimationFrame(() => {
ref.classList.add("enter-active");
setTimeout(() => ref.classList.remove("enter-active"), 300);
});
});
onBeforeExit(async () => {
ref.classList.add("exit-active");
await new Promise((resolve) => setTimeout(resolve, 300));
});
return (
<div ref={(el) => (ref = el)} class="animated">
Content
</div>
);
}Built-in Performance Utilities
Frame Scheduler (Prevent Layout Thrashing)
Inspired by Framer Motion's frame loop, Flash includes a high-performance scheduler that prevents layout thrashing by separating read/update/render phases:
import { frame } from "@mateosuarezdev/flash";
// Simple usage
frame.read(() => {
const height = element.offsetHeight; // DOM reads
});
frame.update(() => {
position += velocity; // Calculations
});
frame.render(() => {
element.style.transform = `translateY(${position}px)`; // DOM writes
});
// Chained operations with type-safe data flow
frame.chain({
read: () => element.offsetHeight,
update: (height) => height * 2,
render: (doubled) => (element.style.height = `${doubled}px`),
});
// Keep-alive for continuous animations
const animate = frame.render(() => {
element.style.transform = `rotate(${rotation}deg)`;
}, true); // true = runs every frame
// Cancel when done
frame.cancel(animate);FLIP Animations
Built-in FLIP (First, Last, Invert, Play) utilities for performant layout animations:
import { flip, flipGroup } from "@mateosuarezdev/flash";
// Animate a single element
flip(
element,
() => {
// Make DOM changes
element.classList.add("expanded");
element.style.width = "400px";
},
{
duration: 300,
easing: "ease-out-cubic",
},
);
// Animate list reordering
const items = document.querySelectorAll(".item");
flipGroup(
items,
() => {
// Reorder items
container.appendChild(items[2]);
},
{
duration: 400,
easing: "ease-out-cubic",
},
);Auto-Animate (Work in Progress)
Automatically animate layout changes (inspired by Framer Motion's layout animations):
<div autoanimate>
{/* Children automatically animate when added/removed/reordered */}
{() => items.value.map((item) => <div key={item.id}>{item.name}</div>)}
</div>Note: Auto-animate is currently in development and will provide automatic FLIP animations for layout changes without manual setup.
View Transitions API
Built-in support for the browser's View Transitions API:
import { startViewTransition } from '@mateosuarezdev/flash'
const expanded = signal(false)
<button onClick={() => {
startViewTransition(() => {
expanded.value = !expanded.value
})
}}>
Toggle
</button>
<div
className={() => expanded.value ? 'expanded' : 'collapsed'}
viewTransitionName="container"
>
Content
</div>Performance Best Practices
Use the Frame Scheduler:
// ❌ Bad: Layout thrashing
const height = element.offsetHeight; // Read
element.style.height = `${height * 2}px`; // Write
const width = element.offsetWidth; // Read (forces reflow!)
element.style.width = `${width * 2}px`; // Write
// ✅ Good: Batched reads and writes
frame.chain({
read: () => ({
height: element.offsetHeight,
width: element.offsetWidth,
}),
render: ({ height, width }) => {
element.style.height = `${height * 2}px`;
element.style.width = `${width * 2}px`;
},
});Use FLIP for Layout Changes:
// ❌ Bad: Animating layout properties directly
element.animate({ width: "400px", height: "300px" }, { duration: 300 });
// ✅ Good: Use FLIP to transform instead
flip(
element,
() => {
element.style.width = "400px";
element.style.height = "300px";
},
{ duration: 300 },
);Advanced Examples
Counter with Computed Values
import { signal, computed } from "@preact/signals-core";
const count = signal(0);
const double = computed(() => count.value * 2);
function Counter() {
return (
<div>
<p>Count: {() => count.value}</p>
<p>Double: {() => double.value}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}Dynamic List with Add/Remove
import { signal } from "@preact/signals-core";
const items = signal([
{ id: 1, name: "Task 1" },
{ id: 2, name: "Task 2" },
]);
let nextId = 3;
function TodoList() {
const addItem = () => {
items.value = [...items.value, { id: nextId++, name: `Task ${nextId}` }];
};
const removeItem = (id: number) => {
items.value = items.value.filter((item) => item.id !== id);
};
return (
<div>
<button onClick={addItem}>Add Task</button>
<ul>
{() =>
items.value.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => removeItem(item.id)}>Delete</button>
</li>
))
}
</ul>
</div>
);
}Nested Reactive Updates
const user = signal({ name: "John", age: 25 });
function Profile() {
return (
<div>
<h1>{() => user.value.name}</h1>
<p>Age: {() => user.value.age}</p>
<button
onClick={() => {
user.value = { ...user.value, age: user.value.age + 1 };
}}
>
Birthday
</button>
</div>
);
}Conditional Rendering with Animations
import { animate } from "framer-motion";
const show = signal(true);
function AnimatedBox() {
let ref: HTMLElement;
onMount(() => {
animate(ref, { opacity: [0, 1], y: [-20, 0] }, { duration: 0.3 });
});
onBeforeExit(async (token) => {
const animation = animate(ref, { opacity: 0, y: -20 }, { duration: 0.3 });
token.onCancel(() => {
animation.stop();
animate(ref, { opacity: 1, y: 0 }, { duration: 0.3 });
});
await animation.finished;
});
return <div ref={(el) => (ref = el)}>Animated content</div>;
}
function App() {
return (
<div>
<button onClick={() => (show.value = !show.value)}>Toggle</button>
{() => show.value && <AnimatedBox />}
</div>
);
}Server-Side Rendering
Flash provides three rendering strategies for different use cases:
renderToString() - Synchronous
Fast synchronous rendering for static content:
import { renderToString } from '@mateosuarezdev/flash/server'
const html = renderToString(<App />)
// Returns: Complete HTML string (no async support)renderToStringAsync() - Complete HTML
Waits for all async components, perfect for SEO and pre-rendering:
import { renderToStringAsync } from '@mateosuarezdev/flash/server'
const html = await renderToStringAsync(<App />)
// Returns: Complete HTML with all async resolvedrenderToStream() - Progressive Enhancement
Stream HTML for better perceived performance:
import { renderToStream } from '@mateosuarezdev/flash/server'
const stream = renderToStream(<App />)
for await (const chunk of stream) {
response.write(chunk)
}
// Streams: Initial HTML + progressive updatesPre-rendering & Caching
import { prerenderer } from "@mateosuarezdev/flash/server/prerender";
// Save pre-rendered HTML
await prerenderer.save("/", html);
// Load from cache
const cached = await prerenderer.load("/");
if (cached) return new Response(cached);Learn more: Check out the Server Architecture Guide for detailed information about:
- Rendering strategies comparison
- Async component resolution
- Streaming architecture
- Caching and pre-rendering
- Security best practices
API Reference
Core
render(element, container)- Mount your app to the DOMFragment- Render multiple children without a wrapper
Lifecycle
onMount(callback)- Run after component mountsonUnmount(callback)- Run when component unmountsonBeforeExit(callback)- Run before unmounting (pauses unmount tree for animations, data saving, etc.)
Context
createContext(defaultValue)- Create a contextuseContext(context)- Consume context value
Animations & Performance
startViewTransition(callback)- Trigger View Transition APIframe.read(callback)- Schedule DOM reads (measurements)frame.update(callback)- Schedule calculationsframe.render(callback)- Schedule DOM writes (mutations)frame.chain({ read, update, render })- Chain operations with data flowflip(element, applyChanges, options)- FLIP animation for single elementflipGroup(elements, applyChanges, options)- FLIP animation for groupsflipMove(element, newParent, options)- Animate element to new containerautoAnimate(element, options)- Enable auto layout animations (WIP)
Special Props
ref={(el) => ...}- Get reference to DOM elementkey={value}- Unique identifier for list itemsviewTransitionName={name}- Named view transition targetautoanimate={true}- Enable auto-layout animations (WIP)
Performance Tips
- Use signals at module level for shared state
- Wrap dynamic expressions in functions
{() => signal.value}not{signal.value} - Always use keys for list items
- Minimize reactive boundaries - only wrap what needs to update
- Use computed signals for derived state
- Use
framefor DOM operations - Prevent layout thrashing by batching reads/writes - Use FLIP for layout animations - Animate transforms instead of layout properties
- Leverage DOM resurrection - Flash automatically reuses DOM for rapid toggles
Comparison to Other Frameworks
| Feature | Flash | React | SolidJS | Vue |
|---|---|---|---|---|
| Reactivity | Signals | VDOM | Signals | Proxies |
| Bundle Size | ~10KB | ~40KB | ~7KB | ~30KB |
| Fine-grained Updates | ||||
| Keyed Lists | ||||
| Built-in FLIP Utils | ||||
| Frame Scheduler | ||||
| DOM Resurrection | ||||
| Animation Flexibility | Any lib | Any lib | Any lib | Any lib |
| SSR Support | ||||
| SSR Streaming | ||||
| No Compiler |
FAQ
Q: Do I need a compiler?
A: No! Flash works with standard JSX transformation. Just configure jsxImportSource.
Q: Can I use TypeScript? A: Yes! Flash is written in TypeScript with full type support.
Q: How does reactivity work?
A: Flash uses Preact Signals. When you wrap an expression in a function {() => signal.value}, Flash creates a reactive boundary that auto-updates when the signal changes.
Q: What about SSR? A: Yes! Flash has full SSR support with three rendering strategies (sync, async, streaming). See the Server Architecture Guide.
Q: Why functions for reactive values? A: Functions create clear boundaries for reactivity and work without a compiler. It's explicit and simple.
Q: What is DOM resurrection? A: When a component is exiting (playing exit animation) but gets toggled back on, Flash cancels the animation and reuses the existing DOM instead of creating a new one. This provides smooth animation reversals without any setup.
Examples
Check out the examples directory for more demos:
- Basic counter and computed values
- Enter/exit animations with cancellation
- View Transitions API integration
- Keyed list rendering with add/remove
- Context API usage
- Reactive props and class names
Architecture
Want to understand how Flash works under the hood?
Client Architecture Guide - Deep dive into:
- JSX transformation flow
- VNode types and rendering pipeline
- Reactivity system internals
- Content replacement strategies (resurrection, text optimization)
- Keyed list reconciliation algorithm
Server Architecture Guide - Deep dive into:
- Three rendering strategies (sync, async, streaming)
- Async component resolution (parallel execution)
- Streaming architecture and progressive enhancement
- Pre-rendering and caching system
- Security best practices (XSS protection)
Contributing
Flash is in active development. Contributions are welcome!
License
MIT Mateo Suarez
Built with Flash - Fine-grained reactivity meets familiar JSX