Skip to content

trueadm/ripple

Repository files navigation

Ripple - the elegant TypeScript UI framework

CI Discord Open in StackBlitz

RippleJS

Currently, this project is still in early development, and should not be used in production.

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 JS/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: track and @ 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 .ripple file 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 trueadm/ripple/templates/basic my-app
cd my-app
npm install && npm run dev

Add to Existing Project

npm install ripple vite-plugin-ripple

→ Full Installation Guide

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

→ Editor Setup Guide

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!")} />
}

→ Component Guide

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

→ Reactivity Guide

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

→ Control Flow Guide

DOM Refs

Capture DOM elements with the {ref fn} syntax:

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

→ DOM Refs Guide

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

→ Events Guide

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

→ Styling Guide

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

→ State Management Guide

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

→ 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

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 39