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 UI file
extension. Components are ordinary TypeScript functions that return native TSRX
expressions, so setup code stays familiar while template bodies can use inline
control flow.
.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
- ⚡ Fine-grained Reactivity:
trackwith lazy destructuring for a unique reactivity system - 🔥 Performance: Industry-leading rendering speed, bundle size, and memory usage
- 📦 Reactive Collections:
RippleArray,RippleObject,RippleMap,RippleSetimported from'ripple'with full reactivity - 🎯 TypeScript First: Complete type safety with the default
.tsrxextension - 🛠️ Developer Tools: VSCode extension, Prettier, and ESLint support
- 🎨 Scoped Styling: Function-local CSS with automatic scoping
npx create-ripple
cd my-app
npm install && npm run devnpx degit Ripple-TS/ripple/templates/basic my-app
cd my-app
npm install && npm run devnpm install ripple @ripple-ts/vite-pluginNote: 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:
- Syntax highlighting
- TypeScript integration
- Real-time diagnostics
- IntelliSense autocomplete
Define components as ordinary functions that return native TSRX:
function Button(props: { text: string; onClick: () => void }) {
return <button onClick={props.onClick}>{props.text}</button>;
}
export function App() {
return <Button text="Click me" onClick={() => console.log('Clicked!')} />;
}
Direct calls keep ordinary helper semantics. A PascalCase helper such as
StatusCode() or FormatName() is left as a normal function when called
directly; component compilation applies to functions used as components or render
entries, and to functions that return native TSRX without being directly called.
Create reactive state with track and use lazy destructuring (&[]) to access
the value directly:
import { track } from 'ripple';
export function App() {
let &[count] = track(0);
return <div>
<p>"Count: "{count}</p>
<button onClick={() => count++}>"Increment"</button>
</div>;
}
You can also pass around the tracked value object from the second argument:
import { track } from 'ripple';
export function App() {
let &[count, trackedCount] = track(0);
return <>
<div>{count}</div>
<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 function App() {
const count = track(0);
return <>
<div>{count.value}</div>
<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 function App() {
let &[count] = track(0);
let &[double] = track(() => count * 2);
let &[quadruple] = track(() => double * 2);
return <div>
<p>"Count: "{count}</p>
<p>"Double: "{double}</p>
<p>"Quadruple: "{quadruple}</p>
<button onClick={() => count++}>"Increment"</button>
</div>;
}
Reactive collections with full reactivity:
import { RippleArray, RippleObject, RippleMap, RippleSet } from 'ripple';
export function 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
return <div>
<p>"Items: "{items.join(', ')}</p>
<p>"Object: a="{obj.a}", b="{obj.b}", c="{obj.c}</p>
<button onClick={() => items.push(items.length + 1)}>"Add Item"</button>
<button onClick={() => (obj.c = (obj.c ?? 0) + 1)}>"Increment c"</button>
</div>;
}
Pass the tracked ref (second element) across function boundaries:
import { track } from 'ripple';
function createDouble(&[count]) {
return track(() => count * 2);
}
export function App() {
let &[count, countTracked] = track(0);
const &[double] = createDouble(countTracked);
return <div>
<p>"Double: "{double}</p>
<button onClick={() => count++}>"Increment"</button>
</div>;
}
→ Transporting Reactivity Guide
import { track, effect } from 'ripple';
export function App() {
let &[count] = track(0);
effect(() => {
console.log('Count changed:', count);
});
return <button onClick={() => count++}>"Increment"</button>;
}
Conditionals:
import { track } from 'ripple';
export function App() {
let &[condition] = track(true);
return <div>
if (condition) {
<div>"True"</div>
} else {
<div>"False"</div>
}
<button onClick={() => (condition = !condition)}>"Toggle"</button>
</div>;
}
Loops:
import { RippleArray } from 'ripple';
export function App() {
const items = new RippleArray({ id: 1, name: 'Item 1' }, {
id: 2,
name: 'Item 2',
}, { id: 3, name: 'Item 3' });
return <div>
for (const item of items; index i; key item.id) {
<div>{item.name}" (index: "{i}")"</div>
}
<button
onClick={() => items.push({
id: items.length + 1,
name: `Item ${items.length + 1}`,
})}
>
"Add Item"
</button>
</div>;
}
Error Boundaries:
function ComponentThatMayFail(props: { shouldFail: boolean }) {
if (props.shouldFail) {
throw new Error('Component failed!');
}
return <div>"Component working fine"</div>;
}
import { track } from 'ripple';
export function App() {
let &[shouldFail] = track(false);
return <div>
try {
<ComponentThatMayFail {shouldFail} />
} catch (e) {
<div>"Error: "{e.message}</div>
}
<button onClick={() => (shouldFail = !shouldFail)}>"Toggle Error"</button>
</div>;
}
Capture DOM elements with the ref={fn} syntax:
export function App() {
return <div ref={(node) => console.log(node)}>"Hello"</div>;
}
Use React-style event handlers:
import { track } from 'ripple';
export function App() {
let &[value] = track('');
return <div>
<button onClick={() => console.log('Clicked')}>"Click"</button>
<input onInput={(e) => (value = e.target.value)} />
<p>"You typed: "{value}</p>
</div>;
}
Scoped CSS:
export function App() {
return <>
<div class="container">"Content"</div>
<style>
.container {
padding: 1rem;
background: lightblue;
border-radius: 8px;
}
</style>
</>;
}
<style> blocks contain static CSS. TSRX template rules for JavaScript statements
and expressions do not apply inside them. For dynamic values, set CSS custom
properties on elements and read them with var(...) from static CSS.
Dynamic CSS values:
import { track } from 'ripple';
export function App() {
let &[color] = track('red');
return <>
<div class="notice" style={{ '--notice-color': color }}>"Styled text"</div>
<button onClick={() => (color = color === 'red' ? 'blue' : 'red')}>
"Toggle Color"
</button>
<style>
.notice {
color: var(--notice-color);
font-weight: bold;
}
</style>
</>;
}
Share state across the component tree:
import { Context, track } from 'ripple';
const ThemeContext = new Context();
function Child() {
const &[theme] = ThemeContext.get();
return <div>"Theme: "{theme}</div>;
}
export function App() {
let &[theme, themeTracked] = track('light');
ThemeContext.set(themeTracked);
return <div>
<Child />
<button onClick={() => (theme = theme === 'light' ? 'dark' : 'light')}>
"Toggle Theme"
</button>
</div>;
}
Render content outside the component hierarchy:
import { Portal, track } from 'ripple';
export function App() {
let &[showModal] = track(false);
return <div>
<button onClick={() => (showModal = !showModal)}>"Toggle Modal"</button>
if (showModal) {
<Portal target={document.body}>
<div class="modal">
<p>"Modal content"</p>
<button onClick={() => (showModal = false)}>"Close"</button>
</div>
</Portal>
}
</div>;
}
- 📚 Full Documentation - Complete guide and API reference
- 🎮 Interactive Playground - Try Ripple in your browser
- 🧩 TSRX Website - Author
.tsrxonce, compile to React, Solid, or Ripple - 🐛 GitHub Issues - Report bugs or request features
- 💬 Discord Community - Get help and discuss Ripple
- 📦 npm Package - Install from npm
Contributions are welcome! Please see our contributing guidelines.
MIT License - see LICENSE for details.