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 a unique reactivity system - 🔥 Performance: Industry-leading rendering speed, bundle size, and memory usage
- 📦 Reactive Collections:
#ripple[...]arrays and#ripple{...}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 @ripple-ts/vite-plugin
Note: You can use
npm,pnpm,yarn, orbunpackage managers.
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 #ripple.track and access it with the @ operator:
export component App() { let count = #ripple.track(0); <div> <p>{"Count: "}{@count}</p> <button onClick={() => @count++}>{"Increment"}</button> </div> }
Derived values automatically update:
export component App() { let count = #ripple.track(0); let double = #ripple.track(() => @count * 2); let quadruple = #ripple.track(() => @double * 2); <div> <p>{"Count: "}{@count}</p> <p>{"Double: "}{@double}</p> <p>{"Quadruple: "}{@quadruple}</p> <button onClick={() => @count++}>{"Increment"}</button> </div> }
Reactive collections using the #ripple.* namespace (no imports needed):
export component App() { const items = #ripple[1, 2, 3]; // RippleArray literal const obj = #ripple{ a: 1, b: 2 }; // RippleObject literal const map = #ripple.map([['k', 'v']]); // RippleMap const set = #ripple.set([1, 2, 3]); // RippleSet <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> }
The #ripple.* namespace eliminates all reactive imports. Type #ripple. in VS
Code to see every available primitive via autocomplete.
Transporting Reactivity
Pass reactive state across function boundaries:
function createDouble(count) { return #ripple.track(() => @count * 2); } export component App() { let count = #ripple.track(0); const double = createDouble(count); <div> <p>{"Double: "}{@double}</p> <button onClick={() => @count++}>{"Increment"}</button> </div> }
→ Transporting Reactivity Guide
Effects & Side Effects
export component App() { let count = #ripple.track(0); #ripple.effect(() => { console.log('Count changed:', @count); }); <button onClick={() => @count++}>{'Increment'}</button> }
Control Flow
Conditionals:
export component App() { let condition = #ripple.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 = #ripple[ {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:
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 = #ripple.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:
export component App() { let value = #ripple.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:
export component App() { let color = #ripple.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:
const ThemeContext = #ripple.context(); component Child() { const theme = ThemeContext.get(); <div>{"Theme: " + @theme}</div> } export component App() { let theme = #ripple.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 } from 'ripple'; export component App() { let showModal = #ripple.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.