Ripple: TypeScript UI framework analysis

Caution
"Right now, there will be plenty of bugs, things just won't work either and you'll find TODOs everywhere. At this stage, Ripple is more of an early alpha version of something that might be, rather than something you should try and adopt. If anything, maybe some of the ideas can be shared and incubated back into other frameworks. There's also a lot of similarities with Svelte 5, and that's not by accident, that's because of my recent time working on Svelte 5."

Ripple is a TypeScript UI framework created by Dominic Gannaway. It borrows ideas from React, Solid, and Svelte and is TypeScript-first, with built-in reactivity, component-based architecture, and native control flow in templates.

Architecture overview

The framework introduces a few concepts that set it apart from existing solutions. Rather than function or class components, Ripple uses a component keyword for defining reusable UI elements. The TypeScript integration is thorough: full type checking, VSCode support with diagnostics and IntelliSense.

Reactive state management

The framework implements reactivity through $ prefixed variables and object properties. This approach mirrors Svelte’s reactivity model while extending it to object properties for more complex state scenarios.

let $count = 0;
let $user = { name: 'John', age: 30 };

// Both variables and object properties are reactive
$count++; // triggers re-render
$user.age = 31; // also triggers re-render

Component definition and props

Components are defined using the component keyword with TypeScript interfaces for props. The syntax supports both traditional props and shorthand notation:

export component Button(props: { text: string, onClick: () => void }) {
  <button onClick={props.onClick}>
    {props.text}
  </button>
}

Native control flow

Ripple allows standard JavaScript control flow (if, for, try) directly in templates without special syntax or helper functions:

export component TodoList({ todos }: Props) {
  <ul>
    for (const todo of todos) { // no "key" needed!
      <li>{todo.text}</li>
    }
  </ul>

  if (todos.length === 0) {
    <p>No todos available</p>
  }
}

This eliminates the need for array methods like map() or special directives like v-for or #each.

Scoped styling and tooling

Components support scoped CSS through <style> elements. The framework includes Prettier support for .ripple modules and VSCode integration with diagnostics and IntelliSense.

export component Card({ title, content }: Props) {
  <div class="card">
    <h3>{title}</h3>
    <p>{content}</p>
  </div>

  <style>
    .card {
      border: 1px solid #ccc;
      padding: 1rem;
      border-radius: 4px;
    }
  </style>
}

Performance considerations

The framework claims fine-grain rendering with optimized performance and memory usage. The reactive system updates only affected DOM elements rather than re-rendering entire component trees.

Technical assessment

Ripple combines familiar concepts from established frameworks while introducing native JavaScript control flow in templates. The $ prefix reactivity system keeps state management clear. TypeScript integration appears thorough, with full type checking support.

The framework is still in early development. Whether it becomes production-ready, and how its ecosystem matures, remains to be seen.

Full implementation example

import { Button } from './Button.ripple';

export component TodoList({ todos, addTodo }: Props) {
  <div class="container">
    <h2>{'Todo List'}</h2>
    <ul>
      for (const todo of todos) {
        <li>{todo.text}</li>
      }
    </ul>

    if (todos.length > 0) {
      <p>{todos.length} {"items"}</p>
    }

    <Button onClick={addTodo} label={"Add Todo"} />
  </div>

  <style>
    .container {
      text-align: center;
      font-family: "Arial", sans-serif;
    }
  </style>
}

export component Counter() {
  let $count = 0;
  let $doubled = $count * 2;

  <div class='counter'>
    <h2>{'Counter'}</h2>
    <p>{`Count: ${$count}`}</p>
    <p>{`Doubled: ${$doubled}`}</p>

    <Button onClick={() => $count++} label={'Increment'} />
    <Button onClick={() => $count = 0} label={'Reset'} />
  </div>
}

Sources

Ripple GitHub Repository
Dominic Gannaway on X