ripple

the elegant TypeScript UI framework

7,374 stars JavaScript
RAW Doc



Ripple TS

Ripple is a TypeScript-first UI framework built around .tsrx files, fine-grained
reactivity, scoped styles, and a small runtime. It pairs the authoring feel of JSX
with template-native control flow and TypeScript setup that can live right beside
the UI it feeds.

Created by @trueadm, who has contributed to
Inferno,
React,
Lexical, and
Svelte 5.

.tsrx is also a standalone language. The shared TSRX compiler stack can target
React, Preact, Solid, Vue, and Ripple. Ripple is the runtime-focused target with
track(), reactive collections, server modules, hydration, and DOM helpers.

Ripple Docs |
Ripple Playground |
TSRX Website

Features

  • Fine-grained reactivity with track() and lazy destructuring.
  • Reactive RippleArray, RippleObject, RippleMap, and RippleSet.
  • Template-native @if, @for, @switch, and @try.
  • Local TypeScript setup with JSX statement containers (@{...}).
  • Scoped <style> blocks with automatic class hashing.
  • Vite, editor, Prettier, ESLint, SSR (buffered and streaming), and hydration
    support.

Quick Start

Using CLI

bash
npx create-ripple
cd my-app
npm install
npm run dev

Using Template

bash
npx degit Ripple-TS/ripple/templates/basic my-app
cd my-app
npm install
npm run dev

Add To Existing Project

bash
npm install ripple @ripple-ts/vite-plugin

Use npm, pnpm, yarn, or bun, matching your project.

Mounting

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

mount(App, {
  props: { title: 'Hello world!' },
  target: document.getElementById('root'),
});

Core Syntax

Components

Components are ordinary TypeScript functions. Return a JSX element directly when
the component has one root, and use a JSX statement container (@{...}) when
setup statements or multiple rendered siblings belong next to the UI.

tsx
type ButtonProps = {
  text: string;
  onClick: () => void;
};

export function Button({ text, onClick }: ButtonProps) {
  return {text};
}

export function App() {
  return  console.log('Clicked!')} />;
}

Fragments are still useful when the component really returns multiple siblings,
such as markup plus a scoped <style> block.

Local TypeScript

Plain JSX children are text, elements, comments, and {...} expression
containers. When a scope needs TypeScript setup before rendering, use a JSX
statement container: @{...}. Setup comes first and the container finishes with
exactly one output node: a JSX element, JSX fragment, or JSX control-flow
expression. If the output needs text, expression containers, or multiple siblings
after setup, wrap them in a fragment.

Text such as x = 123 between tags is JSX text, not JavaScript, unless it is
inside a statement container.

tsx
import { track } from 'ripple';

export function Counter() @{
  let &[count] = track(0);
  const increment = () => count++;

  Count:{count}
}

The same rule applies in nested scopes:

tsx
export function Cart({ items }: { items: Item[] }) @{
  @{
    const subtotal = items.reduce((sum, item) => sum + item.price, 0);
    const discount =
      subtotal > 100 ? 0.1 : 0;

    <>
      Subtotal: ${subtotal}
      Save: ${(subtotal * discount).toFixed(2)}
    </>
  }
}

JavaScript comments are allowed between template children and are not rendered.

Text And Expressions

Static text is JSX text. Dynamic values use normal JSX expression containers.

tsx
export function Greeting({ name }: { name?: string }) @{
  @if (name) {
    Hello,{name}
  } @else {
    Hello, stranger
  }
}

Control Flow

Rendered control flow uses directive-prefixed expressions:

tsx
import { RippleArray, track } from 'ripple';

type Item = { id: number; name: string; done?: boolean };

export function TodoList() @{
  const items = new RippleArray({ id: 1, name: 'Plan the work' }, {
    id: 2,
    name: 'Ship the work',
  });
  let &[showDone] = track(true);
  const visibleItems = () => items.filter((item) => showDone || !item.done);

  
    @for (const item of visibleItems(); index i; key item.id) {
      <li>
        {i + 1}
        .
        {item.name}
      </li>
    } @empty {
      <li>No todos to show</li>
    }
  
}

Use ordinary return for real function exits in TypeScript setup. Use @if for
conditional rendering; direct return, continue, and break statements are not
valid inside @if template branches.

tsx
export function Dashboard({ user }: { user: User | null }) @{
  if (!user) {
    return null;
  }

  <>
    <h1>Welcome,{user.name}</h1>
    Here is your dashboard.
  </>
}

@try supports error and pending UI:

tsx
export function ProfilePanel() @{
  @try {
    
  } @pending {
    Loading...
  } @catch (error, reset) {
    
      Error:{error.message}
       reset()}>Try again
    
  }
}

Reactivity

Create state with track() and lazy destructuring. Reads of lazy bindings stay
reactive, and assignments write back to the tracked value.

tsx
import { effect, track, type Tracked } from 'ripple';

export function Counter() @{
  let &[count, trackedCount] = track(0);
  let &[double] = track(() => count * 2);
  effect(() => {
    console.log('Count changed:', count);
  });

  <>
    Count:{count}
    Double:{double}
     count++}>Increment
    <CounterValue count={trackedCount} />
  </>
}

function CounterValue({ count }: { count: Tracked<number> }) {
  return Shared value:{count.value};
}

Tracked<T> objects can also be read and written through .value, which is
useful when passing reactive values through data structures or props.

Reactive Collections

Use Ripple collections when collection operations should be reactive.

tsx
import { RippleArray, RippleMap, RippleObject, RippleSet } from 'ripple';

export function Inventory() @{
  const items = new RippleArray({ id: 1, name: 'Jacket' });
  const totals = new RippleObject({ selected: 0 });
  const prices = new RippleMap([[1, 120]]);
  const selected = new RippleSet<number>();

  <>
    
      @for (const item of items; key item.id) {
        <li>{item.name}: ${prices.get(item.id)}</li>
      }
    
     selected.add(1)}>Select first item
    
      Selected:
      {selected.size + totals.selected}
    
  </>
}

DOM Refs And Events

DOM refs use ref, and events use JSX-style event props.

tsx
import { track } from 'ripple';

export function SearchBox() @{
  let &[value] = track('');
  let input: HTMLInputElement | undefined;

  <>
    <label>
      Search
       {
          value = event.currentTarget.value;
        }}
      />
    </label>
     input?.focus()}>Focus
  </>
}

Scoped Styles

<style> blocks are static CSS and are scoped to the template. Use CSS custom
properties for runtime values.

tsx
import { track } from 'ripple';

export function Notice() @{
  let &[tone] = track('rebeccapurple');

  <>
    Scoped text
     (tone = tone === 'rebeccapurple'
        ? 'tomato'
        : 'rebeccapurple')}
    >Toggle tone
    <style>
      .notice {
        color: var(--notice-color);
        font-weight: 700;
      }
    </style>
  </>
}

Module-scope style expressions can expose scoped class names:

tsx
const styles = <style>
  .highlight {
    background: #e8f5e9;
  }
</style>;

export function Badge() {
  return New;
}

Context And Portals

tsx
import { Context, Portal, track, type Tracked } from 'ripple';

const ThemeContext = new Context>();

export function App() @{
  let &[theme, themeTracked] = track('light');
  ThemeContext.set(themeTracked);

  <>
    
     (theme = theme === 'light' ? 'dark' : 'light')}>
      Toggle theme
    
    
      Portal content
    
  </>
}

function ThemeLabel() @{
  const theme = ThemeContext.get();

  Theme:{theme.value}
}

Server Modules

Ripple supports module server in .tsrx files for server-oriented exports.
Import from server inside the same file before calling the server function.

tsx
module server {
  export async function loadMessage() {
    return 'Loaded on the server';
  }
}

import { loadMessage } from server;
import { effect, track } from 'ripple';

export function Page() @{
  let &[message] = track('Loading...');
  effect(() => {
    loadMessage().then((next) => {
      message = next;
    });
  });

  {message}
}

Editor Support

Install the
Ripple VSCode extension
for syntax highlighting, diagnostics, TypeScript integration, and completions.

Resources

Contributing

Contributions are welcome. Please see CONTRIBUTING.md.

License

MIT License - see LICENSE for details.