react-tela
v1.0.1
Published
React renderer for Canvas
Readme
react-tela
Use React to render images, shapes and text to <canvas>
[!WARNING] This package is currently under development. Expect breaking changes.
react-tela is a React renderer that draws components to a <canvas> node.
Features
- Low-level primitives — base components expose Canvas drawing operations directly (rects, circles, arcs, paths, images, text)
- High-level abstractions — use the power of React to compose primitives into complex UIs
- Flexbox layout — optional
@react-tela/flexpackage provides a<Flex>component powered by Yoga for CSS-like layout - Unopinionated about runtime — works in web browsers, Node.js, and nx.js
- No DOM dependency — never assumes anything "outside" of the provided canvas node
Installation
npm install react-tela reactQuick Start
import React from "react";
import { Group, Rect, Text, useDimensions } from "react-tela";
function Contents() {
const dims = useDimensions();
return (
<>
<Rect width={dims.width} height={dims.height} fill="purple" alpha={0.5} />
<Text fontSize={32} fontFamily="Geist" fill="white">
Hello world!
</Text>
</>
);
}
export function App() {
return (
<Group x={5} y={15} width={180} height={30} rotate={0.1}>
<Contents />
</Group>
);
}
Rendering
Import render from react-tela/render and pass your app component along with a canvas element:
Web Browser
import React from "react";
import { render } from "react-tela/render";
import { App } from "./App";
render(<App />, document.getElementById("canvas"));Node.js
Uses @napi-rs/canvas for headless rendering:
import React from "react";
import { render } from "react-tela/render";
import config, { Canvas } from "@napi-rs/canvas";
import { App } from "./App";
const canvas = new Canvas(300, 150);
await render(<App />, canvas, config);
const buffer = canvas.toBuffer("image/png");
// … do something with the PNG buffernx.js
import React from "react";
import { render } from "react-tela/render";
import { App } from "./App";
render(<App />, screen);Gradients
The fill and stroke props on shapes and text accept gradient descriptors in addition to CSS color strings. Use the gradient hooks inside components for optimal performance — they memoize the descriptor so the underlying CanvasGradient is cached across re-renders:
import React from 'react';
import { Rect, Text, useLinearGradient, useRadialGradient, useConicGradient } from 'react-tela';
export function App() {
// Linear gradient (memoized)
const linear = useLinearGradient(0, 0, 200, 0, [
[0, 'red'],
[0.5, 'yellow'],
[1, 'blue'],
]);
// Radial gradient (memoized)
const radial = useRadialGradient(100, 100, 10, 100, 100, 100, [
[0, 'white'],
[1, 'black'],
]);
// Conic gradient (memoized)
const conic = useConicGradient(0, 100, 100, [
[0, 'red'],
[0.25, 'yellow'],
[0.5, 'green'],
[0.75, 'blue'],
[1, 'red'],
]);
// Gradient on text
const textGradient = useLinearGradient(0, 0, 300, 0, [[0, 'red'], [1, 'blue']]);
return (
<>
<Rect width={200} height={100} fill={linear} />
<Rect y={100} width={200} height={200} fill={radial} />
<Rect y={300} width={200} height={200} fill={conic} />
<Text y={500} fontSize={48} fontFamily="Geist" fill={textGradient}>Gradient Text</Text>
</>
);
}
Gradient Hooks
| Hook | Parameters | Description |
|------|-----------|-------------|
| useLinearGradient | (x0, y0, x1, y1, stops) | Memoized linear gradient between two points |
| useRadialGradient | (x0, y0, r0, x1, y1, r1, stops) | Memoized radial gradient between two circles |
| useConicGradient | (startAngle, x, y, stops) | Memoized conic (sweep) gradient around a point |
Each stops parameter is an array of [offset, color] tuples where offset is between 0 and 1.
Patterns
The <Pattern> component creates a repeating pattern that can be used as a fill or stroke on shapes and text. It works like a <Group> but renders its children into an offscreen canvas and exposes a CanvasPattern via ref:
import React, { useRef } from 'react';
import { Rect, Pattern } from 'react-tela';
export function App() {
const pattern = useRef<CanvasPattern>(null);
return (
<>
<Pattern ref={pattern} width={20} height={20} repetition="repeat">
<Rect width={10} height={10} fill="#ccc" />
<Rect x={10} y={10} width={10} height={10} fill="#ccc" />
</Pattern>
<Rect width={200} height={120} fill={pattern} />
</>
);
}
Pattern Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| repetition | 'repeat' \| 'repeat-x' \| 'repeat-y' \| 'no-repeat' | 'repeat' | How the pattern repeats |
| width | number | — | Width of the pattern tile |
| height | number | — | Height of the pattern tile |
The <Pattern> component also accepts all <Group> props. Pass the ref to fill or stroke on any shape or text component to apply the pattern.
usePattern() Hook
For image URL-based patterns, use the usePattern() hook. It loads the image asynchronously and returns a CanvasPattern (or null while loading):
import { Rect, usePattern } from 'react-tela';
function TiledBackground() {
const pattern = usePattern('https://example.com/tile.png', 'repeat');
return <Rect width={400} height={300} fill={pattern} />;
}
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| source | string | — | Image URL or path to load |
| repetition | 'repeat' \| 'repeat-x' \| 'repeat-y' \| 'no-repeat' | 'repeat' | How the pattern repeats |
Returns CanvasPattern | null — null while the image is loading.
Filters
All entities support the filter prop, which maps directly to CanvasRenderingContext2D.filter. Pass any valid CSS filter string:
import React from 'react';
import { Rect, Circle, Text } from 'react-tela';
export function App() {
return (
<>
<Rect x={10} y={20} width={80} height={80} fill="red" filter="blur(3px)" />
<Circle x={115} y={25} radius={35} fill="blue" filter="drop-shadow(4px 4px 4px rgba(0,0,0,0.5))" />
<Text x={220} y={44} fill="green" fontSize={32} fontFamily="Geist" filter="drop-shadow(3px 3px 0 rgba(0,0,0,0.7))">Hello</Text>
</>
);
}
Supported filter functions include blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), saturate(), and sepia(). Multiple filters can be chained in a single string.
Blend Modes
Set the blendMode prop on any entity to control how it composites with the content below it. This maps directly to the Canvas globalCompositeOperation property.
import React from 'react';
import { Rect, Circle } from 'react-tela';
export function App() {
return (
<>
<Rect width={360} height={120} fill="#222" />
<Rect x={10} y={10} width={80} height={100} fill="#e74c3c" />
<Rect x={50} y={10} width={80} height={100} fill="#3498db" blendMode="multiply" />
<Rect x={140} y={10} width={80} height={100} fill="#e74c3c" />
<Rect x={180} y={10} width={80} height={100} fill="#3498db" blendMode="screen" />
<Rect x={270} y={10} width={80} height={100} fill="#e74c3c" />
<Circle x={290} y={30} radius={40} fill="#3498db" blendMode="overlay" />
</>
);
}
Common values include "multiply", "screen", "overlay", "darken", "lighten", "destination-out", and more. See MDN for the full list.
Shadows
All entities support shadow props that map directly to the Canvas shadow properties:
import React from 'react';
import { Rect, Circle, Text } from 'react-tela';
export function App() {
return (
<>
<Rect x={10} y={10} width={100} height={80} fill="blue"
shadowColor="rgba(0, 0, 0, 0.5)" shadowBlur={10} shadowOffsetX={5} shadowOffsetY={5} />
<Circle x={180} y={20} radius={40} fill="red"
shadowColor="rgba(0, 0, 0, 0.6)" shadowBlur={15} shadowOffsetX={4} shadowOffsetY={4} />
<Text x={270} y={40} fill="green" fontSize={32} fontFamily="Geist"
shadowColor="rgba(0, 0, 0, 0.7)" shadowBlur={3} shadowOffsetX={2} shadowOffsetY={2}>Hello</Text>
</>
);
}
The shadowColor prop accepts any CSS color string. shadowBlur controls the blur radius, while shadowOffsetX and shadowOffsetY control the horizontal and vertical offset of the shadow.
Components
All components accept these common props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| x | number | 0 | Horizontal position |
| y | number | 0 | Vertical position |
| width | number | 0 | Width in pixels |
| height | number | 0 | Height in pixels |
| alpha | number | 1 | Opacity (0 = transparent, 1 = opaque) |
| rotate | number | 0 | Rotation in degrees |
| scaleX | number | 1 | Horizontal scale |
| scaleY | number | 1 | Vertical scale |
| blendMode | GlobalCompositeOperation | — | Composite operation (see MDN) |
| shadowColor | string | — | Shadow color (CSS color string) |
| shadowBlur | number | 0 | Shadow blur radius |
| shadowOffsetX | number | 0 | Shadow horizontal offset |
| shadowOffsetY | number | 0 | Shadow vertical offset |
Mouse and touch events are supported on all components: onClick, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onTouchStart, onTouchMove, onTouchEnd.
<Rect>
Draws a filled and/or stroked rectangle.
import React from 'react';
import { Rect } from 'react-tela';
export function App() {
return <Rect x={5} y={5} width={100} height={50} fill="red" stroke="black" lineWidth={2} />;
}
Rounded corners
Add borderRadius to render a rectangle with rounded corners (uses ctx.roundRect() under the hood):
import React from 'react';
import { Rect } from 'react-tela';
export function App() {
return <Rect x={5} y={5} width={100} height={50} fill="blue" borderRadius={10} />;
}
The borderRadius prop accepts a single number, a DOMPointInit, or an array — matching the CanvasRenderingContext2D.roundRect() spec.
<Circle>
Draws a circle. Shorthand for <Arc> with startAngle={0} and endAngle={360}.
import React from 'react';
import { Circle } from 'react-tela';
export function App() {
return <Circle x={10} y={10} radius={40} fill="green" />;
}
<Ellipse>
Draws an ellipse with separate X and Y radii.
import React from 'react';
import { Ellipse } from 'react-tela';
export function App() {
return <Ellipse x={10} y={10} radiusX={80} radiusY={40} fill="purple" stroke="white" lineWidth={2} />;
}
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| radiusX | number | — | Horizontal radius |
| radiusY | number | — | Vertical radius |
| ellipseRotation | number | 0 | Ellipse rotation in degrees |
| startAngle | number | 0 | Start angle in degrees |
| endAngle | number | 360 | End angle in degrees |
| counterclockwise | boolean | false | Draw counterclockwise |
<Arc>
Draws an arc or partial circle.
import React from 'react';
import { Arc } from 'react-tela';
export function App() {
return <Arc x={10} y={10} radius={50} startAngle={0} endAngle={180} fill="orange" />;
}
| Prop | Type | Description |
|------|------|-------------|
| radius | number | Arc radius |
| startAngle | number | Start angle in degrees |
| endAngle | number | End angle in degrees |
| counterclockwise | boolean | Draw counterclockwise |
<Path>
Draws an SVG path.
import React from 'react';
import { Path } from 'react-tela';
export function App() {
return (
<Path
x={31} y={31}
width={47.94} height={47.94}
d="M26.285,2.486l5.407,10.956c0.376,0.762,1.103,1.29,1.944,1.412l12.091,1.757c2.118,0.308,2.963,2.91,1.431,4.403l-8.749,8.528c-0.608,0.593-0.886,1.448-0.742,2.285l2.065,12.042c0.362,2.109-1.852,3.717-3.746,2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403,0l-10.814,5.685c-1.894,0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528c-1.532-1.494-0.687-4.096,1.431-4.403l12.091-1.757c0.841-0.122,1.568-0.65,1.944-1.412l5.407-10.956C22.602,0.567,25.338,0.567,26.285,2.486z"
fill="#ED8A19"
stroke="red"
lineWidth={3}
scaleX={2} scaleY={2}
/>
);
}| Prop | Type | Description |
|------|------|-------------|
| d | string | SVG path data string |
| width | number | Required. Path viewport width |
| height | number | Required. Path viewport height |

<Line>
Draws a line or polyline through a series of points.
import React from 'react';
import { Line } from 'react-tela';
export function App() {
return <Line points={[{ x: 10, y: 80 }, { x: 70, y: 10 }, { x: 140, y: 80 }]} stroke="blue" lineWidth={3} />;
}
| Prop | Type | Description |
|------|------|-------------|
| points | {x: number, y: number}[] | Array of points to connect |
Width and height are computed automatically from the bounding box of the points. All shape props are supported (stroke, lineWidth, fill, etc.).
<BezierCurve>
Draws a cubic Bézier curve between two points with two control points.

import React from 'react';
import { BezierCurve } from 'react-tela';
export function App() {
return (
<BezierCurve
x0={10} y0={10}
cp1x={40} cp1y={0}
cp2x={60} cp2y={100}
x1={90} y1={10}
stroke="blue"
lineWidth={2}
/>
);
}| Prop | Type | Description |
|------|------|-------------|
| x0, y0 | number | Start point |
| cp1x, cp1y | number | First control point |
| cp2x, cp2y | number | Second control point |
| x1, y1 | number | End point |
Width and height are computed automatically from the bounding box. All shape props are supported.
<QuadraticCurve>
Draws a quadratic Bézier curve between two points with one control point.

import React from 'react';
import { QuadraticCurve } from 'react-tela';
export function App() {
return (
<QuadraticCurve
x0={10} y0={80}
cpx={50} cpy={10}
x1={90} y1={80}
stroke="red"
lineWidth={2}
/>
);
}| Prop | Type | Description |
|------|------|-------------|
| x0, y0 | number | Start point |
| cpx, cpy | number | Control point |
| x1, y1 | number | End point |
Width and height are computed automatically from the bounding box. All shape props are supported.
<Image>
Renders an image from a URL or file path. The image loads asynchronously — the component renders at the specified (or natural) dimensions once loaded.
<Image src="/path/to/image.png" x={10} y={10} width={200} height={150} />| Prop | Type | Description |
|------|------|-------------|
| src | string | Image source (URL or file path) |
| sx, sy, sw, sh | number | Source crop rectangle (optional) |
| imageSmoothing | boolean | Enable/disable image smoothing when scaling (default: true). Set to false for pixel art. |
| imageSmoothingQuality | "low" \| "medium" \| "high" | Quality of image smoothing (default: "low") |
<Text>
Renders text.
import React from 'react';
import { Text } from 'react-tela';
export function App() {
return (
<Text x={10} y={10} fontSize={24} fontFamily="Geist" fill="#333" stroke="rgba(0,0,0,0.1)">
Hello world!
</Text>
);
}
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | string \| number | — | Text content |
| fontSize | number | 24 | Font size in pixels |
| fontFamily | string | "sans-serif" | Font family |
| fontWeight | string | "" | Font weight |
| fill | string | — | Fill color |
| stroke | string | — | Stroke color |
| lineWidth | number | — | Stroke width |
| textAlign | CanvasTextAlign | "start" | Horizontal alignment |
| textBaseline | CanvasTextBaseline | "top" | Vertical baseline |
| letterSpacing | number | — | Extra spacing between letters (px) |
| wordSpacing | number | — | Extra spacing between words (px) |
| direction | CanvasDirection | — | Text direction ("ltr", "rtl", "inherit") |
| fontKerning | CanvasFontKerning | — | Font kerning ("auto", "normal", "none") |
| fontStretch | CanvasFontStretch | — | Font stretch (e.g. "condensed", "expanded") |
| fontVariantCaps | CanvasFontVariantCaps | — | Font variant caps (e.g. "small-caps", "all-petite-caps") |
| maxWidth | number | — | Maximum width before wrapping/truncating |
| lineHeight | number | 1.2 | Line height as a multiplier of fontSize |
| overflow | 'wrap' \| 'ellipsis' \| 'clip' | 'wrap' | How to handle text exceeding maxWidth |
Multiline / Word Wrap
When maxWidth is set, text automatically wraps to fit. Use overflow to control behavior:
import React from 'react';
import { Rect, Text } from 'react-tela';
export function App() {
return (
<>
<Rect width={500} height={120} fill="#f8f8f8" />
<Text x={10} y={10} fontSize={14} fontFamily="Geist" fill="#333" maxWidth={150} lineHeight={1.4}>
This is a long paragraph that will automatically wrap to fit within the maxWidth.
</Text>
<Text x={180} y={10} fontSize={14} fontFamily="Geist" fill="#333" maxWidth={150} overflow="ellipsis">
This text will be truncated with an ellipsis when it overflows.
</Text>
<Text x={350} y={10} fontSize={14} fontFamily="Geist" fill="#333" maxWidth={150} overflow="clip">
This text will be hard-clipped at the boundary edge.
</Text>
<Text x={60} y={105} fontSize={10} fontFamily="Geist" fill="#999">wrap (default)</Text>
<Text x={235} y={105} fontSize={10} fontFamily="Geist" fill="#999">ellipsis</Text>
<Text x={415} y={105} fontSize={10} fontFamily="Geist" fill="#999">clip</Text>
</>
);
}
Explicit newline characters (\n) are always respected, even without maxWidth.
<Group>
Groups child components into a sub-canvas with its own coordinate system. Children render relative to the group's position.
import React from 'react';
import { Group, Rect, Text } from 'react-tela';
export function App() {
return (
<Group x={50} y={20} width={200} height={80} rotate={5}>
<Rect width={200} height={80} fill="purple" alpha={0.5} />
<Text fontSize={24} fontFamily="Geist" fill="white">Inside a group</Text>
</Group>
);
}
Viewport / Scrolling
A <Group> can act as a scrollable viewport into a larger content area. Set contentWidth and/or contentHeight to define the inner canvas dimensions independently from the rendered width/height, then use scrollTop/scrollLeft to control which portion of the content is visible.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| contentWidth | number | — | Width of the inner content canvas. When larger than width, enables horizontal scrolling. |
| contentHeight | number | — | Height of the inner content canvas. When larger than height, enables vertical scrolling. |
| scrollTop | number | 0 | Vertical offset into the content. Out-of-bounds values show empty space (useful for overscroll effects). |
| scrollLeft | number | 0 | Horizontal offset into the content. Out-of-bounds values show empty space (useful for overscroll effects). |
import React, { useState, useEffect } from 'react';
import { Group, Rect, Text } from 'react-tela';
export function App() {
const [scrollTop, setScrollTop] = useState(0);
// Animate scrolling
useEffect(() => {
const id = setInterval(() => {
setScrollTop((prev) => (prev + 1) % 200);
}, 16);
return () => clearInterval(id);
}, []);
return (
<Group width={200} height={100} contentHeight={300} scrollTop={scrollTop}>
<Rect width={200} height={100} fill="red" />
<Rect y={100} width={200} height={100} fill="green" />
<Rect y={200} width={200} height={100} fill="blue" />
</Group>
);
}When contentWidth/contentHeight are not set, the <Group> renders its children at full size without any viewport clipping.
Key optimization: When only scrollTop/scrollLeft change (and children haven't changed), the inner canvas is not re-rendered — only the source coordinates of the drawImage call change. This makes scrolling essentially free.
borderRadius prop
When set, the Group clips its composited output to a rounded rectangle. Accepts a single number for uniform corners or an array [tl, tr, br, bl] for per-corner radii — the same API as <RoundRect>.
<Group x={50} y={20} width={200} height={80} borderRadius={10}>
<Rect width={200} height={80} fill="purple" />
</Group>
{/* Bottom corners only */}
<Group x={50} y={120} width={200} height={80} borderRadius={[0, 0, 10, 10]}>
<Rect width={200} height={80} fill="blue" />
</Group><Canvas>
Creates a sub-canvas that you can draw to imperatively via getContext('2d'). Useful for mixing imperative canvas drawing with React components.
import React from 'react';
import { Canvas, useParent } from 'react-tela';
export function App() {
const root = useParent();
return (
<Canvas
width={120}
height={80}
ref={(ref) => {
if (!ref) return;
const ctx = ref.getContext("2d");
if (!ctx) return;
const grad = ctx.createLinearGradient(0, 0, 120, 80);
grad.addColorStop(0, "#3498db");
grad.addColorStop(1, "#9b59b6");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 120, 80);
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(60, 40, 25, 0, Math.PI * 2);
ctx.fill();
root.queueRender();
}}
/>
);
}
Shape Props
<Rect>, <Circle>, <Ellipse>, <Arc>, <Path>, <Line>, <BezierCurve>, and <QuadraticCurve> all share these shape-specific props:
| Prop | Type | Description |
|------|------|-------------|
| fill | string | Fill color |
| fillRule | CanvasFillRule | Fill rule ("nonzero" or "evenodd") |
| stroke | string | Stroke color |
| lineWidth | number | Stroke width (default: 1) |
| lineCap | CanvasLineCap | Line cap style |
| lineJoin | CanvasLineJoin | Line join style |
| lineDash | number[] | Dash pattern |
| lineDashOffset | number | Dash offset |
| miterLimit | number | Miter limit |
| clip | boolean | Clip children to this shape |
| clipRule | CanvasFillRule | Clip rule |
Hooks
useDimensions()
Returns { width, height } of the parent canvas or group.
function FullSizeRect() {
const { width, height } = useDimensions();
return <Rect width={width} height={height} fill="black" />;
}useLayout()
Returns { x, y, width, height } from the nearest LayoutContext. Used internally by components to support the <Flex> layout system.
useParent()
Returns the parent Root context. Useful for advanced use cases like accessing the underlying canvas context or queuing manual re-renders.
useTextMetrics(text, fontFamily?, fontSize?, fontWeight?)
Measures text and returns a TextMetrics object. Useful for positioning or sizing based on text content.
import React from 'react';
import { Group, Rect, Text, useDimensions, useTextMetrics } from 'react-tela';
function CenteredText({ children }: { children: string }) {
const dims = useDimensions();
const metrics = useTextMetrics(children, "Geist", 24);
return (
<Text
x={dims.width / 2 - metrics.width / 2}
y={dims.height / 2 - 12}
fontSize={24}
fontFamily="Geist"
fill="white"
>
{children}
</Text>
);
}
export function App() {
return (
<Group x={0} y={0} width={400} height={60}>
<Rect width={400} height={60} fill="#2c3e50" />
<CenteredText>Centered with useTextMetrics</CenteredText>
</Group>
);
}
Related Packages
@react-tela/flex— CSS Flexbox-like layout powered by Yoga
Exports
| Entry Point | Exports |
|------------|---------|
| react-tela | Rect, Circle, Ellipse, Arc, Path, Line, Image, Text, Group, Canvas, useDimensions, useLayout, useParent, useTextMetrics, LayoutContext |
| react-tela/render | render |
What is "tela"? 🇧🇷
The word "tela" is the Portuguese word for "canvas" or "screen".
tela [ˈtɛla] (feminine noun)
- (de pintar) canvas
- (cinema, telecommunications) screen
Since the name react-canvas was already taken, react-tela was a fun alternative.
Prior Art
A few other React renderers for <canvas> already exist, so why another?
react-tela is designed to make as few assumptions about the runtime environment as possible. Other renderers assume they are running in a web browser, or possibly Node.js. This module only interacts with the canvas node it is provided, and never makes any assumptions about anything "outside" of the node.
react-art— Appears to be an official solution (code lives in the React monorepo), but documentation and examples are basically non-existent and it does not seem to be actively maintained.react-canvas— Perfect name. Nice API. Probably one of the original React<canvas>implementations. Has not been updated in years.react-konva— Awesome and very mature. Relies onreact-dom, which is pretty large and assumes a DOM is available.
License
MIT
