<img src="https://github.com/Ripple-TS/ripple/raw/1.0.1/assets/ripple-mobile.png" alt="Ripple - the elegant TypeScript UI framework" />
Ripple is a TypeScript UI framework that combines the best parts of React, Solid, and Svelte. Created by @trueadm, who has contributed to Inferno, React, Lexical, and Svelte 5.
Key Philosophy: Ripple is TS-first with .tsrx as its default component file
extension. This allows seamless TypeScript integration and a unique syntax that
enhances both human and LLM developer experience.
.tsrxis also a standalone language: the same source can now compile to React, Solid, or Ripple via TSRX — a TypeScript language extension that treats Ripple as one of several target runtimes. If you want the authoring ergonomics without committing to Ripple's runtime, start there.
📚 Ripple Docs | 🎮 Ripple Playground | 🧩 TSRX Website
track with lazy destructuring for a unique
reactivity systemRippleArray, RippleObject, RippleMap,
RippleSet imported from 'ripple' with full reactivity.tsrx component
extensionnpx create-ripple
cd my-app
npm install && npm run dev
npx degit Ripple-TS/ripple/templates/basic my-app
cd my-app
npm install && npm run dev
npm install ripple @ripple-ts/vite-plugin
Note: You can use
npm,pnpm,yarn, orbunpackage managers.
// index.ts
import { mount } from 'ripple';
import { App } from './App.tsrx';
mount(App, {
props: { title: 'Hello world!' },
target: document.getElementById('root'),
});
Install the Ripple VSCode extension for:
Define components with the component keyword. Unlike React, you don't return
JSX—you write it directly:
component Button(props: { text: string, onClick: () => void }) {
<button onClick={props.onClick}>
{props.text}
</button>
}
export component App() {
<Button text="Click me" onClick={() => console.log("Clicked!")} />
}
Create reactive state with track and use lazy destructuring (&[]) to access
the value directly:
import { track } from 'ripple';
export component App() {
let &[count] = track(0);
{"Count: "}{count}
<button onClick={() => count++}>{"Increment"}</button>
}
You can also pass around the tracked value object from the second argument:
import { track } from 'ripple';
export component App() {
let &[count, trackedCount] = track(0);
{count}
<IncrementButton {trackedCount} />
}
Alternatively, you can read and write tracked values directly using the .value
property on the Tracked<V> object:
import { track } from 'ripple';
export component App() {
const count = track(0);
{count.value}
<button onClick={() => count.value++}>{"Increment"}</button>
}
Using &[...] is preferred in most cases for cleaner code, but .value is useful
when you need to keep the Tracked<V> object around — for example, when storing
tracked values in data structures or passing them as Tracked<T> props.
Derived values automatically update:
import { track } from 'ripple';
export component App() {
let &[count] = track(0);
let &[double] = track(() => count * 2);
let &[quadruple] = track(() => double * 2);
{"Count: "}{count}
{"Double: "}{double}
{"Quadruple: "}{quadruple}
<button onClick={() => count++}>{"Increment"}</button>
}
Reactive collections with full reactivity:
import { RippleArray, RippleObject, RippleMap, RippleSet } from 'ripple';
export component App() {
const items = new RippleArray(1, 2, 3); // RippleArray
const obj = new RippleObject({ a: 1, b: 2 }); // RippleObject
const map = new RippleMap([['k', 'v']]); // RippleMap
const set = new RippleSet([1, 2, 3]); // RippleSet
{"Items: "}{items.join(', ')}
{"Object: a="}{obj.a}{", b="}{obj.b}{", c="}{obj.c}
<button onClick={() => items.push(items.length + 1)}>{"Add Item"}</button>
<button onClick={() => obj.c = (obj.c ?? 0) + 1}>{"Increment c"}</button>
}
Pass the tracked ref (second element) across function boundaries:
import { track } from 'ripple';
function createDouble(&[count]) {
return track(() => count * 2);
}
export component App() {
let &[count, countTracked] = track(0);
const &[double] = createDouble(countTracked);
{"Double: "}{double}
<button onClick={() => count++}>{"Increment"}</button>
}
→ Transporting Reactivity Guide
import { track, effect } from 'ripple';
export component App() {
let &[count] = track(0);
effect(() => {
console.log('Count changed:', count);
});
<button onClick={() => count++}>{'Increment'}</button>
}
Conditionals:
import { track } from 'ripple';
export component App() {
let &[condition] = track(true);
if (condition) {
{'True'}
} else {
{'False'}
}
<button onClick={() => condition = !condition}>{"Toggle"}</button>
}
Loops:
import { RippleArray } from 'ripple';
export component App() {
const items = new RippleArray(
{id: 1, name: 'Item 1'},
{id: 2, name: 'Item 2'},
{id: 3, name: 'Item 3'}
);
for (const item of items; index i; key item.id) {
{item.name}{" (index: "}{i}{")"}
}
<button onClick={() => items.push({id: items.length + 1, name: `Item ${items.length + 1}`})}>{"Add Item"}</button>
}
Error Boundaries:
component ComponentThatMayFail(props: { shouldFail: boolean }) {
if (props.shouldFail) {
throw new Error('Component failed!');
{'This will never render'}
}
{"Component working fine"}
}
import { track } from 'ripple';
export component App() {
let &[shouldFail] = track(false);
try {
<ComponentThatMayFail {shouldFail} />
} catch (e) {
{'Error: ' + e.message}
}
<button onClick={() => shouldFail = !shouldFail}>{"Toggle Error"}</button>
}
Capture DOM elements with the {ref fn} syntax:
export component App() {
console.log(node)}>{"Hello"}
}
Use React-style event handlers:
import { track } from 'ripple';
export component App() {
let &[value] = track('');
<button onClick={() => console.log('Clicked')}>{'Click'}</button>
<input onInput={(e) => value = e.target.value} />
{"You typed: "}{value}
}
Scoped CSS:
export component App() {
{"Content"}
<style>
.container {
padding: 1rem;
background: lightblue;
border-radius: 8px;
}
</style>
}
Dynamic styles:
import { track } from 'ripple';
export component App() {
let &[color] = track('red');
{"Styled text"}
<button onClick={() => color = color === 'red' ? 'blue' : 'red'}>{"Toggle Color"}</button>
}
Share state across the component tree:
import { Context, track } from 'ripple';
const ThemeContext = new Context();
component Child() {
const &[theme] = ThemeContext.get();
{"Theme: " + theme}
}
export component App() {
let &[theme, themeTracked] = track('light');
ThemeContext.set(themeTracked);
<Child />
<button onClick={() => theme = theme === 'light' ? 'dark' : 'light'}>{"Toggle Theme"}</button>
}
Render content outside the component hierarchy:
import { Portal, track } from 'ripple';
export component App() {
let &[showModal] = track(false);
<button onClick={() => showModal = !showModal}>{"Toggle Modal"}</button>
if (showModal) {
<Portal target={document.body}>
{'Modal content'}
<button onClick={() => showModal = false}>{"Close"}</button>
</Portal>
}
}
.tsrx once, compile to React,
Solid, or RippleContributions are welcome! Please see our contributing guidelines.
MIT License - see LICENSE for details.
$ claude mcp add ripple \
-- python -m otcore.mcp_server <graph>