Ripple TS
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 its own .ripple file extension, allowing seamless TypeScript integration and a unique syntax that enhances both human and LLM developer experience.
๐ Full Documentation | ๐ฎ Interactive Playground
Features
- โก Fine-grained Reactivity:
trackand@syntax with signals-based reactiviโฆ
Ripple TS
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 its own .ripple file extension, allowing seamless TypeScript integration and a unique syntax that enhances both human and LLM developer experience.
๐ Full Documentation | ๐ฎ Interactive Playground
Features
- โก Fine-grained Reactivity:
trackand@syntax with signals-based reactivity - ๐ฅ Performance: Industry-leading rendering speed, bundle size, and memory usage
- ๐ฆ Reactive Collections:
#[]arrays and#{}objects with full reactivity - ๐ฏ TypeScript First: Complete type safety with
.ripplefile extension - ๐ ๏ธ Developer Tools: VSCode extension, Prettier, and ESLint support
- ๐จ Scoped Styling: Component-level CSS with automatic scoping
Note: SSR support is coming soon! Currently SPA-only.
๐ Quick Start
Using CLI (Recommended)
npx create-ripple
cd my-app
npm install && npm run dev
Using Template
npx degit Ripple-TS/ripple/templates/basic my-app
cd my-app
npm install && npm run dev
Add to Existing Project
npm install ripple vite-plugin-ripple
Mounting Your App
// index.ts
import { mount } from 'ripple';
import { App } from './App.ripple';
mount(App, {
props: { title: 'Hello world!' },
target: document.getElementById('root'),
});
๐ง VSCode Extension
Install the Ripple VSCode extension for:
- Syntax highlighting
- TypeScript integration
- Real-time diagnostics
- IntelliSense autocomplete
Core Concepts
Components
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!")} />
}
Reactivity
Create reactive state with track and access it with the @ operator:
import { track } from 'ripple';
export component App() {
let count = track(0);
<div>
<p>{"Count: "}{@count}</p>
<button onClick={() => @count++}>{"Increment"}</button>
</div>
}
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);
<div>
<p>{"Count: "}{@count}</p>
<p>{"Double: "}{@double}</p>
<p>{"Quadruple: "}{@quadruple}</p>
<button onClick={() => @count++}>{"Increment"}</button>
</div>
}
Reactive collections with shorthand syntax:
export component App() {
const items = #[1, 2, 3]; // TrackedArray
const obj = #{a: 1, b: 2}; // TrackedObject
<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>
}
Transporting Reactivity
Pass reactive state across function boundaries:
import { track } from 'ripple';
function createDouble(count) {
return track(() => @count * 2);
}
export component App() {
let count = track(0);
const double = createDouble(count);
<div>
<p>{"Double: "}{@double}</p>
<button onClick={() => @count++}>{"Increment"}</button>
</div>
}
โ Transporting Reactivity Guide
Effects & Side Effects
import { effect, track } from 'ripple';
export component App() {
let count = track(0);
effect(() => {
console.log('Count changed:', @count);
});
<button onClick={() => @count++}>{'Increment'}</button>
}
โ Effects & Reactivity Guide
Control Flow
Conditionals:
import { track } from 'ripple';
export component App() {
let condition = track(true);
<div>
if (@condition) {
<div>{'True'}</div>
} else {
<div>{'False'}</div>
}
<button onClick={() => @condition = !@condition}>{"Toggle"}</button>
</div>
}
Loops:
export component App() {
const items = #[
{id: 1, name: 'Item 1'},
{id: 2, name: 'Item 2'},
{id: 3, name: 'Item 3'}
];
<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:
import { track } from 'ripple';
component ComponentThatMayFail(props: { shouldFail: boolean }) {
if (props.shouldFail) {
throw new Error('Component failed!');
{'This will never render'}
}
<div>{"Component working fine"}</div>
}
export component App() {
let shouldFail = track(false);
<div>
try {
<ComponentThatMayFail shouldFail={@shouldFail} />
} catch (e) {
<div>{'Error: ' + e.message}</div>
}
<button onClick={() => @shouldFail = !@shouldFail}>{"Toggle Error"}</button>
</div>
}
DOM Refs
Capture DOM elements with the {ref fn} syntax:
export component App() {
<div {ref (node) => console.log(node)}>{"Hello"}</div>
}
Events
Use React-style event handlers:
import { track } from 'ripple';
export component App() {
let value = track('');
<div>
<button onClick={() => console.log('Clicked')}>{'Click'}</button>
<input onInput={(e) => @value = e.target.value} />
<p>{"You typed: "}{@value}</p>
</div>
}
Styling
Scoped CSS:
export component App() {
<div class="container">{"Content"}</div>
<style>
.container {
padding: 1rem;
background: lightblue;
border-radius: 8px;
}
</style>
}
Dynamic styles:
import { track } from 'ripple';
export component App() {
let color = track('red');
<div>
<div style={{ color: @color, fontWeight: 'bold' }}>{"Styled text"}</div>
<button onClick={() => @color = @color === 'red' ? 'blue' : 'red'}>{"Toggle Color"}</button>
</div>
}
Advanced Features
Context API
Share state across the component tree:
import { Context, track } from 'ripple';
const ThemeContext = new Context();
component Child() {
const theme = ThemeContext.get();
<div>{"Theme: " + @theme}</div>
}
export component App() {
let theme = track('light');
ThemeContext.set(theme);
<div>
<Child />
<button onClick={() => @theme = @theme === 'light' ? 'dark' : 'light'}>{"Toggle Theme"}</button>
</div>
}
Portals
Render content outside the component hierarchy:
import { Portal, track } from 'ripple';
export component App() {
let showModal = track(false);
<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>
}
Resources
- ๐ Full Documentation - Complete guide and API reference
- ๐ฎ Interactive Playground - Try Ripple in your browser
- ๐ GitHub Issues - Report bugs or request features
- ๐ฌ Discord Community - Get help and discuss Ripple
- ๐ฆ npm Package - Install from npm
Contributing
Contributions are welcome! Please see our contributing guidelines.
License
MIT License - see LICENSE for details.