Ripl (pronounced "ripple") is a library that provides a unified API for 2D graphics rendering (Canvas & SVG) in the browser, with a focus on high performance and interactive data visualization. It also includes an experimental 3D rendering package.
Working with the canvas API can be notoriously difficult as it is designed to be very low-level. Alternatively, working with SVG is rather straightforward but not without its flaws. Because these paradigms differ widely in their implementations developers often have to choose one or the other at the outset of a project. Ripl alleviates the issue by exposing a unified API and mimicking the DOM/CSSOM in as many ways as possible to make it simple for developers to interact with. Switching between Canvas and SVG is as simple as changing one line of code.
[!IMPORTANT] Ripl is currently in beta.
Spline Polyline
![]() |
3D Sphere
![]() |
Multi-Line Graph
![]() |
Bubble Scatter
![]() |
Candlestick Chart
![]() |
Jet Engine Model
![]() |
requestAnimationFrame loopgetElementById, getElementsByType, getElementsByClass, query, queryAll with selector syntaxgetBoundingBox on all shape elementsShape2DTask-based transitions with CSS-like keyframe support and custom interpolators@ripl/charts| Package | Description |
|---|---|
@ripl/web |
Main entry point for browser usage — re-exports core + canvas context with browser platform bindings |
@ripl/core |
Core rendering — elements, scene, renderer, animation, scales, math, color, interpolation, gradients, tasks |
@ripl/canvas |
Canvas 2D rendering context |
@ripl/svg |
SVG rendering context |
@ripl/charts |
Pre-built chart components with axes, legends, tooltips, crosshairs, and grids |
@ripl/3d |
3D rendering context with camera, shading, and primitive shapes (experimental) |
@ripl/webgpu |
WebGPU-accelerated 3D rendering context with hardware depth testing and WGSL shaders |
@ripl/terminal |
Terminal rendering context — braille-character output with ANSI truecolor |
@ripl/node |
Node.js runtime bindings — configures the platform factory for headless environments |
@ripl/dom |
DOM utilities used internally by browser contexts |
@ripl/utilities |
Shared typed utility functions — type guards, collection helpers, DOM helpers |
The project is structured as a Yarn 4 monorepo:
packages/
├── core/ # Core rendering library
├── canvas/ # Canvas 2D rendering context
├── svg/ # SVG context implementation
├── charts/ # Pre-built chart components
├── 3d/ # 3D rendering (experimental)
├── webgpu/ # WebGPU 3D rendering context
├── terminal/ # Terminal rendering context
├── node/ # Node.js runtime bindings
├── web/ # Main browser entry point
├── dom/ # DOM utilities
├── utilities/ # Shared typed utility functions
└── test-utils/ # Test utilities
app/ # Documentation site (VitePress) with live demos
The following is a tour of Ripl's features starting from the most basic and progressively building towards more advanced concepts.
import {
createCircle,
createContext,
} from '@ripl/web';
// Create a canvas context bound to a DOM element
const context = createContext('.mount-element');
// Create an element
const circle = createCircle({
fill: 'rgb(30, 105, 120)',
lineWidth: 4,
cx: context.width / 2,
cy: context.height / 2,
radius: context.width / 3,
});
// Render the element to the context
circle.render(context);
Built-in 2D shape primitives: arc, circle, rect, line, polyline, polygon, ellipse, text, path, image.
To modify an element simply change any of its properties and re-render it.
circle.fill = '#FF0000';
circle.cx = context.width / 3;
circle.cy = context.height / 3;
circle.render(context);
To render the same element to SVG, replace the createContext import from @ripl/web with @ripl/svg:
import {
createContext,
} from '@ripl/svg';
import {
createCircle,
} from '@ripl/web';
const context = createContext('.mount-element');
const circle = createCircle({ /* same options */ });
circle.render(context);
Render multiple elements in groups with inherited properties (like CSS) and event bubbling (like the DOM):
import {
createCircle,
createContext,
createGroup,
createRect,
} from '@ripl/web';
const context = createContext('.mount-element');
const circle = createCircle({
cx: context.width / 2,
cy: context.height / 2,
radius: context.width / 3,
});
const rect = createRect({
x: context.width / 2,
y: context.height / 2,
width: context.width / 5,
height: context.height / 5,
});
// Both children inherit fill and lineWidth from the group
const group = createGroup({
fill: 'rgb(30, 105, 120)',
lineWidth: 4,
children: [circle, rect],
});
group.render(context);
Elements can be queried using common DOM methods or CSS-like selectors:
const circles = parentGroup.getElementsByType('circle');
const shapes = parentGroup.queryAll('.shape');
const first = parentGroup.query('#child-group > .shape');
Supported selector features:
circle /* type */
#element-id /* id */
.element-class /* class */
circle[radius="5"] /* attribute */
.group-class circle /* descendant */
.group-class > circle /* direct child */
.group-class rect + circle.circle-class /* adjacent sibling */
A Scene is the top-level group bound to a rendering context. A Renderer drives the animation loop via requestAnimationFrame.
import {
createCircle,
createGroup,
createRect,
createRenderer,
createScene,
} from '@ripl/web';
const circle = createCircle({
fill: 'rgb(30, 105, 120)',
cx: 100,
cy: 100,
radius: 40,
});
const rect = createRect({
fill: 'rgb(30, 105, 120)',
x: 200,
y: 80,
width: 60,
height: 60,
});
// Scene takes a target (selector, element, or context) and options
const scene = createScene('.mount-element', {
children: [circle, rect],
});
const renderer = createRenderer(scene, {
autoStart: true,
autoStop: true,
});
// Listen for events
circle.on('click', event => console.log(event));
The renderer provides transition-based animation. Transitions are cancellable Task instances (extending Promise with AbortController integration).
import {
easeOutCubic,
} from '@ripl/web';
// Animate a single element
await renderer.transition(circle, {
duration: 1000,
ease: easeOutCubic,
state: {
fill: '#FF0000',
cx: 200,
cy: 200,
radius: 60,
},
});
// Animate multiple elements (or a whole group/scene)
await renderer.transition([circle, rect], {
duration: 1000,
ease: easeOutCubic,
state: {
fill: '#FF0000',
},
});
// Implicit keyframe offsets
await renderer.transition(circle, {
duration: 1000,
ease: easeOutCubic,
state: {
fill: [
'#FF0000', // offset ~0.33
'#00FF00', // offset ~0.66
'#0000FF', // offset 1
],
},
});
// Explicit keyframe offsets
await renderer.transition(circle, {
duration: 1000,
ease: easeOutCubic,
state: {
fill: [
{ value: '#FF0000',
offset: 0.25 },
{ value: '#0000FF',
offset: 0.8 },
],
},
});
// Custom interpolator function
await renderer.transition(circle, {
duration: 1000,
ease: easeOutCubic,
state: {
radius: t => t * 100, // 0 <= t <= 1
},
});
Every element supports CSS-like transforms:
const rect = createRect({
x: 100,
y: 100,
width: 80,
height: 80,
rotation: '45deg', // or radians as a number
transformOriginX: '50%', // or pixels as a number
transformOriginY: '50%',
translateX: 20,
translateY: 10,
transformScaleX: 1.5,
transformScaleY: 1.5,
});
Transforms can also be animated via renderer.transition.
@ripl/charts provides 18 ready-to-use, animated chart types. Each chart supports tooltips, legends, crosshairs, grids, axes, and data update animations out of the box.
| Chart | Factory |
|---|---|
| Area | createAreaChart |
| Bar | createBarChart |
| Chord | createChordChart |
| Funnel | createFunnelChart |
| Gantt | createGanttChart |
| Gauge | createGaugeChart |
| Heatmap | createHeatmapChart |
| Line | createLineChart |
| Pie / Donut | createPieChart |
| Polar Area | createPolarAreaChart |
| Radar | createRadarChart |
| Realtime | createRealtimeChart |
| Sankey | createSankeyChart |
| Scatter | createScatterChart |
| Stock (OHLC) | createStockChart |
| Sunburst | createSunburstChart |
| Treemap | createTreemapChart |
| Trend (Bar + Line) | createTrendChart |
Charts are constructed by passing a target element and an options object:
import {
createBarChart,
} from '@ripl/charts';
const chart = createBarChart('.mount-element', {
data: [
{ category: 'A',
value: 30 },
{ category: 'B',
value: 70 },
{ category: 'C',
value: 45 },
],
keyBy: 'category',
series: [
{ label: 'Values',
valueBy: item => item.value },
],
});
// Update with new data
chart.update({
data: [
{ category: 'A',
value: 50 },
{ category: 'B',
value: 20 },
{ category: 'C',
value: 80 },
],
});
Reusable chart components: ChartXAxis, ChartYAxis, Grid, Legend, Tooltip, Crosshair.
Ripl provides 11 scale types for mapping data between domains and ranges, inspired by D3. All scales expose inverse, ticks, and includes methods.
import {
scaleBand,
scaleContinuous,
scaleDiscrete,
scaleDiverging,
scaleLogarithmic,
scalePower,
scaleQuantile,
scaleQuantize,
scaleThreshold,
scaleTime,
} from '@ripl/web';
const scale = scaleContinuous([0, 25], [-100, 100]);
scale(10); // -20
scale.inverse(-20); // 10
// With clamping
const clamped = scaleContinuous([0, 25], [-100, 100], { clamp: true });
clamped(30); // 100
const scale = scaleDiscrete(['a', 'b', 'c'], [0, 50]);
scale('b'); // 25
const scale = scaleBand(['A', 'B', 'C'], [0, 300], {
innerPadding: 0.1,
outerPadding: 0.05,
});
scale('B'); // position of band B
scale.bandwidth; // width of each band
scaleDiverging — maps values below and above a midpoint to separate sub-rangesscaleLogarithmic — logarithmic mapping with configurable basescalePower — polynomial mapp