Ripple is a new open-source front-end framework taking ideas from React, SolidJS, and Svelte into a TypeScript-first, component-oriented, JSX-like compiled language with fine-grained reactivity and scoped CSS. Created by Svelte maintainer Dominic Gannaway, Ripple offers a reactivity system with automatic dependency tracking, and direct DOM updates without a virtual DOM. Ripple aims to support better debugging through AI agents.
The following code sample exemplifies many of the core Ripple features:
import { Button } from './Button.ripple';
import { track } from 'ripple';
export component TodoList({ todos, addTodo }: Props) {
<div class="container">
<h2>{'Todo List'}</h2>
<ul>
for (const todo of todos) {
<li>{todo.text}</li>
}
</ul>
if (todos.length > 0) {
<p>{todos.length}{" items"}</p>
}
<Button onClick={addTodo} label={"Add Todo"} />
</div>
<style>
.container {
text-align: center;
font-family: "Arial", sans-serif;
}
</style>
}
export component Counter() {
let count = track(0);
let double = track(() => @count * 2);
<div class='counter'>
<h2>{'Counter'}</h2>
<p>{"Count: "}{@count}</p>
<p>{"Double: "}{@double}</p>
<Button onClick={() => @count++} label={'Increment'} />
<Button onClick={() => @count = 0} label={'Reset'} />
</div>
}
With Ripple, developers write components (e.g., TodoList, Counter) that are functions that contain DOM expressions as statements in a language whose syntax is largely inspired from TypeScript and JSX. Those functions thus describe the markup (DOM), styles (CSS), and behavior of a portion of an application’s user interface.
The user interface markup is directly expressed as statements with natural interleaving of control flow (e.g., if (todos.length > 0), for (const todo of todos)) and JSX markup. Styles are scoped to the component. Behavior is driven by event handlers leveraging a fine-grained reactivity system that, as Svelte does, surgically updates the real DOM (vs. recomputing the component’s entire virtual DOM markup) in response to changes to independent and computed variables.
The track primitive describes an independent variable (e.g., count) whose value is obtained with the @ operator. Computed variables can express their dependencies to independent variables (e.g., let double = track(() => @count * 2);). Ripple’s reactive system ensures that computed variables are kept in sync with their dependencies. Ripple’s reactive system also ensures that DOM elements’ state is kept in sync with their dependencies (e.g., clicking the button will increment count, which will in turn update the textContent property of both paragraphs).
Gannaway explained on Twitter:
Ripple’s reactivity system isn’t virtual DOM and isn’t signals. It is a fine-grained, lazy evaluation system that leverages the compiler more than the runtime to make these sorts of things happen.
While Ripple does not support global state, it does support context for those pieces of application state that must be shared across components. Context, however, can only be used by components that have it in closure, and can only be set and read within a component context (that is, they cannot be set nor read within an event handler context).
import { Context } from 'ripple';
const MyContext = new Context(null);
component Child() {
// Context is read in the Child component
const value = MyContext.get();
// value is "Hello from context!"
console.log(value);
}
export component Parent() {
const value = MyContext.get();
// Context is read in the Parent component, but hasn't yet
// been set, so we fallback to the initial context value.
// So the value is `null`
console.log(value);
// Context is set in the Parent component
MyContext.set("Hello from context!");
<Child />
}
Effects can also be tied to state changes with the effect keyword:
import { track, effect } from 'ripple';
export component App() {
let count = track(0);
effect(() => {
console.log(@count);
});
<button onClick={() => @count++}>{'Increment'}</button>
}
Ripple hopes to offer a simpler mental model together with a better developer experience to web application developers (e.g., no need to useMemo, CSS is scoped by default, no extra abstraction between markup and DOM elements). The language is designed to interact with the compiler to finely understand TypeScript types and the reactive state patterns, paving the way for better autocompletion, error checking, and tooling support. The Ripple team is investigating integrating AI support directly into the dev server for proactive debugging and suggestions.
Ripple was created by Dominic Gannaway, who previously worked on React Hooks at Meta, created Lexical, authored Inferno, and was recently part of Svelte 5’s core team. While Ripple is already a few years old, it was only recently open-sourced with the MIT license. Ripple remains in early development. Contributions are welcome and should follow the contribution guidelines.