Skip to content

Ripple-TS/ripple

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,943 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Ripple - the elegant TypeScript UI framework

CI Discord Open in StackBlitz

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 .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.

.tsrx is 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

Features

  • Fine-grained Reactivity: track with lazy destructuring for a unique reactivity system
  • 🔥 Performance: Industry-leading rendering speed, bundle size, and memory usage
  • 📦 Reactive Collections: RippleArray, RippleObject, RippleMap, RippleSet imported from 'ripple' with full reactivity
  • 🎯 TypeScript First: Complete type safety with the default .tsrx extension
  • 🛠️ Developer Tools: VSCode extension, Prettier, and ESLint support
  • 🎨 Scoped Styling: Function-local CSS with automatic scoping

🚀 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, or bun package managers.

→ Full Installation Guide

Mounting Your App

// index.ts
import { mount } from 'ripple';
import { App } from './App.tsrx';

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

→ Editor Setup Guide

Core Concepts

Components

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.

→ Component Guide

Reactivity

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>;
}

→ Reactivity Guide

Transporting Reactivity

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

Effects & Side Effects

import { track, effect } from 'ripple';

export function App() {
  let &[count] = track(0);

  effect(() => {
    console.log('Count changed:', count);
  });

  return <button onClick={() => count++}>"Increment"</button>;
}

→ Effects & Reactivity Guide

Control Flow

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>;
}

→ Control Flow Guide

DOM Refs

Capture DOM elements with the ref={fn} syntax:

export function App() {
  return <div ref={(node) => console.log(node)}>"Hello"</div>;
}

→ DOM Refs Guide

Events

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>;
}

→ Events Guide

Styling

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>
  </>;
}

→ Styling Guide

Advanced Features

Context API

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>;
}

→ State Management Guide

Portals

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>;
}

→ Portal & Component Guide

Resources

Contributing

Contributions are welcome! Please see our contributing guidelines.

License

MIT License - see LICENSE for details.

About

the elegant TypeScript UI framework

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors