Back to Scaling Python Applications guides

Ripple UI: Building Reactive Frontends for Python Backends

Stanley Ulili
Updated on November 28, 2025

JavaScript frameworks have become essential for modern web development, with React dominating the landscape for years. Its component-based architecture and virtual DOM power countless applications, but this power comes with complexity, boilerplate code, and a steep learning curve.

Ripple UI offers a different approach by providing fine-grained reactivity, a clean syntax without JSX, and impressive performance, particularly for developers working with Python backends like FastAPI, Flask, and Django.

This guide explores Ripple's core concepts through a complete demo application, examining how it manages state with the track() function, builds dynamic components without JSX, and creates seamless communication between a Ripple frontend and a Python FastAPI backend.

What is Ripple UI and why should you care?

Many developers, especially those from backend-heavy languages, find the modern JavaScript ecosystem overwhelming with its build tools, state management libraries, and abstract concepts like the virtual DOM.

The problem with traditional frameworks

React introduces several layers of abstraction. The virtual DOM efficiently updates the user interface by re-evaluating the entire component tree to find differences, but this can lead to "re-render bloat." Developers must master complex hooks like useEffect, useCallback, and useMemo with their tricky dependency arrays to optimize performance. JSX, while popular, mixes HTML-like syntax directly into JavaScript, which isn't always the cleanest approach.

Ripple's core principles

Ripple builds on simplicity and performance, drawing inspiration from frameworks like Svelte and SolidJS while maintaining a familiar component structure.

Ripple in Action demo application showcasing various reactive components

Fine-grained reactivity is Ripple's distinguishing feature. Instead of re-rendering entire components, Ripple tracks exactly which parts of the DOM depend on specific pieces of state. When a state variable changes, only the text nodes or attributes that use it are updated. This surgical precision delivers incredible speed and efficiency compared to the broader virtual DOM approach.

Ripple is a compiled framework that generates highly optimized vanilla JavaScript that directly manipulates the DOM. Its templates use standard string literals, allowing you to write what feels like HTML with embedded logic without special syntax. Control flow uses simple JavaScript if statements and for loops, making code more readable.

The combination of a compiler and fine-grained reactivity means Ripple applications are lightweight and fast. The framework has a minimal footprint, and the resulting code is stripped of unnecessary overhead.

Integration with Python backends

Python developers building modern UIs have traditionally faced a choice between learning heavy JavaScript frameworks or settling for less dynamic server-side templating engines like Jinja2. Ripple bridges this gap as a simple, self-contained frontend library that any Python backend can easily serve. You can build API routes and business logic in FastAPI or Django while Ripple handles the user interface, communicating through standard fetch calls. This creates a clean, powerful, and maintainable full-stack architecture.

Project structure and setup

A typical Ripple and Python project consists of a FastAPI backend serving as the API and a Ripple UI frontend that consumes it.

The Python backend with FastAPI

The backend is a standard Python application built with FastAPI. The primary file defines the server and its API endpoints:

The /api/message endpoint definition in main.py

main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import logging

app = FastAPI(title="Ripple - Logging Demo API")

logger = logging.getLogger("uvicorn.error")

@app.get("/api/message")
async def api_message(request: Request) -> JSONResponse:
    return JSONResponse({
        "message": "Blending Python w/ Ripple!",
        "backend": "FastAPI",
        "frontend": "Ripple UI"
    })

@app.post("/api/signup")
async def process_user_signup(request: Request) -> JSONResponse:
    user_id = "user_183da42b"
    logger.info(f"User signup successful: {user_id}")
    return JSONResponse({"status": "Signup successful!", "user": user_id})

@app.get("/api/logs")
async def get_logs(request: Request, limit: int = 50) -> JSONResponse:
    logs = [
        {"level": "INFO", "event": "request_started", "path": "/api/logs"},
        {"level": "INFO", "event": "request_completed", "path": "/api/message"}
    ]
    return JSONResponse({"logs": logs})

These routes allow the Ripple frontend to fetch data, trigger actions, and retrieve logs.

The Ripple UI frontend

The frontend code resides in a separate frontend directory, using Vite as a development server for features like hot module reloading and API request proxying to the Python backend.

The main application logic lives in frontend/src/App.ripple.ts, which defines a class-based component containing all state, methods, and rendering logic for the user interface.

Understanding reactivity with track()

Ripple's reactivity system centers on the track() function, which replaces the entire suite of state and effect hooks found in React.

How track() works

To make data reactive, you wrap its initial value in the track() function, creating a "signal" that holds a value and notifies subscribers when that value changes:

Multiple state variables declared using track() in App.ripple.ts

App.ripple.ts
import { Component, track } from "./ripple";

interface Todo {
  id: number;
  text: string;
}

export class App implements Component {
  private countState = track(0);
  private todosState = track<Todo[]>([]);
  private todoInputState = track("");
  private messageState = track("");
  private logsState = track<any[]>([]);
  private loadingState = track(false);
}

When you call track(), it returns a tuple containing a getter function and a setter function. To read the value, you call the getter (e.g., this.countState[0]()). When Ripple's renderer executes template logic and calls this getter, it automatically creates a subscription, recording that this specific DOM part depends on this specific state.

To update the value, you call the setter with the new value (e.g., this.countState[1](newValue)). The setter notifies all subscribers, triggering them to update with the new value.

This mechanism enables fine-grained reactivity without a virtual DOM diff or manual dependency tracking. The connection between state and DOM is direct and automatic, eliminating the need for React's useEffect hook.

Building a reactive counter

Here's how a counter component demonstrates this reactivity:

App.ripple.ts
private getCount() { return this.countState[0](); }
private setCount(value: number) { this.countState[1](value); }

private increment = () => {
    this.setCount(this.getCount() + 1);
}

private reset = () => {
    this.setCount(0);
}

private renderCounterCard(): string {

    return `
        <div class="card">
            <h2>Reactive Counter</h2>
            <p>track() replaces the whole useEffect from React</p>
            <div style="text-align: center">
                <button id="increment-btn">Increment</button>
                <button id="reset-btn">Reset</button>
            </div>
        </div>
    `;
}

When the "Increment" button is clicked, the increment method updates countState. Because the counter-display and derived-value divs were rendered using getCount(), Ripple knows they're subscribed to countState and immediately updates only those two text pieces in the DOM without touching anything else.

Building dynamic UIs without JSX

Ripple deliberately moves away from JSX, resulting in a templating system that's both powerful and simple.

Control flow with plain JavaScript

Visual emphasizing Ripple's approach without .map() and JSX

Instead of learning special syntax for loops and conditionals, you use JavaScript directly. Conditionals use if statements, and lists use for...of loops. This keeps rendering logic clean and easy to follow.

The to-do list component

Here's how a to-do list component handles dynamic rendering:

App.ripple.ts
private todosState = track<Todo[]>([]);
private todoInputState = track('');

private renderTodosCard(): string {
    const todos = this.getTodos();
    const todoInputValue = this.getTodoInput();

    let todosHtml = '';
todosHtml = '<p>No todos yet. Add one above!</p>';
} else {
for (const todo of todos) {
todosHtml += `
<div class="todo-item">
<span>${this.escapeHtml(todo.text)}</span>
<button class="remove-todo-btn" data-id="${todo.id}">Remove</button>
</div>
`;
}
return ` <div class="card"> <h2>Todo List</h2> <p>Say goodbye to all that JSX Syntax</p> <input type="text" id="todo-input" value="${this.escapeHtml(todoInputValue)}" placeholder="Add a todo..." /> <button id="add-todo-btn">Add Todo</button> <button id="clear-todos-btn">Clear All</button> <div class="todo-list">${todosHtml}</div> </div> `; }

Adding, removing, and clearing to-dos simply involves updating the todosState array using its setter. Ripple's reactive system automatically and efficiently re-renders just the todo-list section whenever the array changes.

Connecting Ripple UI to Python

Ripple makes frontend and backend communication straightforward through standard API calls.

API calls and development proxy

The Ripple app runs in the browser while the Python app runs as a server. They communicate using the fetch API. During development, they run on different ports (Ripple on 5173, FastAPI on 8000). Vite's development server acts as a proxy, forwarding any request starting with /api to the Python server at http://localhost:8000 to avoid CORS issues.

Fetching data from FastAPI

When a user clicks the "Fetch from Python API" button, the following method executes:

App.ripple.ts
private fetchFromApi = async () => {
    this.setLoading(true);
    try {
const data = await response.json();
} catch (error) { console.error('Failed to fetch from API:', error); } finally { this.setLoading(false); } }

The fetched message is stored in messageState using its setter, and the UI, which subscribes to messageState, instantly updates to display the message from the Python backend.

UI showing successful response from the backend: "Response: Blending Python w/ Ripple!"

Live log viewer implementation

The live log viewer demonstrates a complete round-trip interaction. When a user clicks "Create User Signup," Ripple sends a POST request to the /api/signup endpoint. The FastAPI backend executes the corresponding function, creates a log entry, and returns a success response.

When the user clicks "Refresh Logs," Ripple sends a GET request to /api/logs:

App.ripple.ts
private refreshLogs = async () => {
    this.setLoading(true);
    try {
const data = await response.json();
} catch (error) { console.error('Failed to fetch logs:', error); } finally { this.setLoading(false); } }

FastAPI reads the latest logs and sends them back as a JSON array. Ripple updates its logsState tracked variable with this array, and the UI automatically re-renders the "Live Logs" panel to display the new entries.

Live Logs component displaying structured log entries from the Python backend

This entire interaction is managed with simple functions, tracked state, and standard API calls, showcasing clean separation of concerns.

Ripple UI vs. the competition

Framework comparison chart showing how Ripple UI compares to React, Tailwind, Bootstrap, and Shadcn

Ripple doesn't aim to replace React for massive, enterprise-scale applications but offers a compelling alternative with different priorities:

FRAMEWORK NEED JS? CURVE SPEED DESIGN
RIPPLE UI Minimal Easy ⚡️⚡️⚡️⚡️⚡️ Clean + Modern
REACT Heavy Medium ⚡️⚡️⚡️⚡️ No Default
TAILWIND CSS None Medium ⚡️⚡️⚡️⚡️ No Components
BOOTSTRAP Low Easy ⚡️⚡️⚡️ Generic Look
SHADCN React Only Hard ⚡️⚡️⚡️ Setup Heavy

Unlike React which requires a large runtime, Ripple compiles away, leaving minimal JavaScript needed to manage reactivity. The API is small and the concepts are intuitive, especially for those who prefer plain HTML and JavaScript over JSX and hooks. Its compiled, fine-grained reactivity model is one of the fastest approaches possible for web UIs. Ripple works with any CSS framework but encourages a clean, component-based design philosophy.

Final thoughts

Ripple UI is a new way to build frontends that focuses on being simple, fast, and easy for developers to use. Its track() function gives you a clear and efficient way to watch for changes in data and update the UI.

For Python developers, Ripple makes it possible to build fast, interactive, and good-looking user interfaces without having to fully learn React. Python can stay in charge of the backend, while Ripple takes care of the frontend, which makes full-stack development feel clean and organized.

Ripple UI won’t replace React in every project, but it has found an important place in the frontend world. It shows that building for the web can be quick, enjoyable, and not overly complicated. If you want a framework that stays out of your way and helps you move fast with clear code, Ripple is worth a look.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.