
text-to-canvas
Render multiline plain or rich text into textboxes on HTML Canvas with automatic line wrapping.
Origins and Differences
🙌 This library would not exist were it not for all the work done by its original author, Geon George, in his canvas-txt library.
The main feature that sparked text-to-canvas is a significant update to the original code base in order to support rich text formatting, which introduced the concept of a Word specifying both text and (optional) associated CSS-based format styles. A sentence is then simply a Word[] with/out whitespace (optionally inferred).
Plain text (i.e. a string) is still supported as a convenience via the drawText(), splitText(), and textToWords() APIs.
The main differences (at v1.0.0) between canvas-txt and this library are:
The feature gap may widen with future releases of both libraries.
While there is a Node demo, it only works because the node-canvas library being used supports enough of the HTMLCanvasElement's API, not because this library formally supports Node, or node-canvas.
Features
- ✅ Rich text formatting (with the exception of words with different font sizes not yet working well in terms of text baseline alignment)
- ✅ Multiline text
- ✅ Auto line breaks
- ✅ Horizontal alignment
- ✅ Vertical alignment
- ✅ Justification
- ✅ Optimized performance with support for Web Workers and
OffscreenCanvas
Demo
See demo here.
Installation
$ npm install text-to-canvas
$ yarn add text-to-canvas
💡 If this fails with a node-pre-gyp compilation error, please see Compilation of the canvas package for help.
Compilation of canvas package
This project optionally depends on the canvas package which enables it to be used in a Node demo.
❗️ Note this is optional as text-to-canvas does not formally support this library. This is purely for casual testing and as an example of how text-to-canvas should technically work with any library that supports the HTMLCanvasElement API. text-to-canvas only officially supports HTML <canvas>.
Since this package needs to be compiled for use on the platform on which you intend to install/use it, the author must either include pre-built binaries specific to your OS when they make a release, or a new binary must be compiled by your package manager (i.e. npm) upon installation.
If you're installing on a newer Apple M1, M2, or M3 computer, or if you're using a version of Node newer than v20 (the latest LTS at time of writing), you may experience a node-pre-gyp failure because canvas doesn't provide pre-built binaries for the ARM64 architecture, only providing x86-64 (Intel x64) binaries for Node v20.
❗️ Before installing text-to-canvas, refer to the canvas compilation page for your OS/architecture, especially if you aren't on an Apple computer.
For Apple M computers (ARM64), this worked for me using HomeBrew and pyenv to install additional compiler dependencies:
$ brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman
$ pyenv install 3.12.1
$ pyenv local 3.12.1
$ npm install
Usage
Use with a bundler (Webpack, Rollup, Vite, etc) or directly in a browser is supported.
Use in Node is only supported to the extent that appropriate bundles are provided. Make sure you use a Node-base Canvas library that supports the HTMLCanvasElement API.
Bundler
Two bundles are provided for this type of target:
./dist/text-to-canvas.esm.min.js (ESM, import, ES2022+)
./dist/text-to-canvas.min.js (CJS, require(), ES2022+)
Used implicitly when using the library in a larger app bundled with a bundler like Webpack, Rollup, or Vite.
Declare a Canvas in your DOM (directly, via JSX, or other):
<canvas id="my-canvas" width="500" height="500"></canvas>
Call the drawText() API:
import { drawText, Word } from 'text-to-canvas';
const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 500, 500);
const text = 'Lorem ipsum dolor sit amet';
const text: Word[] = [
{ text: 'Lorem' },
{ text: 'ipsum', format: { fontWeight: 'bold', fontColor: 'red' } },
{ text: 'dolor', format: { fontStyle: 'italic' } },
{ text: 'sit' },
{ text: 'amet' },
];
drawText(ctx, text, {
x: 100,
y: 200,
width: 200,
height: 200,
fontSize: 24,
});
If you need to know the total render height, drawText() returns it:
const { height } = drawText(...);
⚠️ The library doesn't yet fully support varying font sizes, so you'll get best results by keeping the size consistent (via the base font size) and changing other formatting options on a per-Word basis.
Browser
One bundle is provided for this type of target:
./dist/text-to-canvas.umd.min.js (UMD, ES2022+)
Used implicitly when loading the library directly in a browser:
<body>
<canvas id="my-canvas" width="500" height="500"></canvas>
<script src="//unpkg.com/text-to-canvas"></script>
<script>
const { drawText, getTextHeight, splitText } = window.textToCanvas;
</script>
</body>
Node
Two bundles are provided for this type of target:
./dist/text-to-canvas.mjs (ESM/MJS, import, Node v22+)
./dist/text-to-canvas.cjs (CJS, require(), Node v22+)
⚠️ Other than the bundles, Node is not formally supported by this library, and neither is the node-canvas library used in the demo. Whatever "Node Canvas" library you use, make sure it supports the HTMLCanvasElement API and it should work.
Used implicitly when importing or requiring the library in your Node scripts:
import { drawText } from 'text-to-canvas';
const { drawText } = require('text-to-canvas');
See Node demo in ./src/demo/node-demo.ts for an example.
You can run this demo locally with npm run node:demo
API
drawText config

width | Required | Width of the text box. |
height | Required | Height of the text box. |
x | 0 | X position of the text box. |
y | 0 | Y position of the text box. |
align | center | Text align. Other possible values: left, right. |
vAlign | middle | Text vertical align. Other possible values: top, bottom. |
fontFamily | Arial | Base font family of the text. |
fontSize | 14 | Base font size of the text in px. |
fontStyle | '' | Base font style, same as css font-style. Examples: italic, oblique 40deg. |
fontVariant | '' | Base font variant, same as css font-variant. Examples: small-caps. |
fontWeight | '400' | Base font weight, same as css font-weight. Examples: bold, 100. |
fontColor | 'black' | Base font color, same as css color. Examples: blue, #00ff00. |
strokeColor | 'black' | Base stroke color, same as css color. Examples: blue, #00ff00. |
strokeWidth | 0 | Base stroke width. Positive number; <=0 means none. Can be fractional. ⚠️ Word splitting does not take into account the stroke, which is applied on the center of the edges of the text via the strokeText() Canvas API. Setting a thick stroke will cause it to bleed out of the text box. |
justify | false | Justify text if true. It will insert spaces between words when necessary. Ignored if textWrap != 'wrap' |
underline | false | If the text or word should be underlined. Can also be an object with customization options like color, thickness, and offset. |
strikethrough | false | If the text or word should have a strikethrough. Can also be an object with customization options like color, thickness, and offset. |
inferWhitespace | true | If whitespace in the text should be inferred. Only applies if the text given to drawText() is a Word[]. If the text is a string, this config setting is ignored. |
overflow | true | Allows the text to overflow out of the box if the box is too narrow/short to fit it all. false will clip the text to the box's boundaries. Use in conjunction with textWrap='none' to achieve a typical spreadsheet clipping effect. |
textWrap | 'wrap' | Whether the text should wrap at supported newline characters (LF, LS, or PS) as well as at the box's horizontal boundaries in order to keep as much of the text visible, or extend beyond the horizontal boundaries of the box even if it doesn't fit. This is separate from whether the text extending beyond the box's horizontal boundaries is visible (i.e. "clipped"). Use the overflow option to control clipping (e.g. clip = textWrap='none' + overflow=false. Other values: 'none' (no wrapping other than at hard breaks using \n characters). |
debug | false | Draws the border and alignment lines of the text box for debugging purposes. |
Functions
import {
drawText,
specToJson,
splitText,
splitWords,
textToWords,
wordsToJson,
getTextHeight,
getWordHeight,
getTextStyle,
getTextFormat,
} from 'text-to-canvas'
⚠️ Varying font sizes on a Word level (as given to drawText() or splitWords()) is not supported very well at this time. For best results, keep the font size consistent by relying on a single base font size as specified in the drawText() config options.
drawText(): Draws text (string or Word[]) to a given Canvas.
specToJson(): Converts a RenderSpec to a JSON string. Useful for sending it as a message through Worker.postMessage().
splitText(): Splits a given string into wrapped lines.
- This is just a convenience over
splitWords() if you aren't needing rich text. It's only real value is that it will return the input text as an array of strings according to how the text would be wrapped on Canvas.
splitWords(): Splits a given Word[] into wrapped lines.
textToWords(): Converts a string into a Word[]. Useful if you want to then apply rich formatting to certain words.
wordsToJson(): Converts a Word[] to a JSON string. Useful for sending it as a message to a Worker thread via Worker.postMessage().
getTextHeight(): Gets the measured height of a given string using a given text style.
getWordHeight(): Gets the measured height of a given Word using its text style.
getTextStyle(): Generates a CSS Font string from a given TextFormat for use with canvas.getContext('2d').font
getTextFormat(): Generates a "full" TextFormat object (all properties specified) given one with only partial properties using prescribed defaults.
TypeScript integration should provide helpful JSDocs for every function and each of its parameters to further help with their use.
Line Breaks
Newline characters (i.e. hard breaks) are supported in text and Words, but only the following characters are considered line breaks:
- Line Feed (LF):
\n
- Line Separator (LS):
\u2028
- Paragraph Separator (PS):
\u2029
🔺 Any Word that has at least one line break character in it will be treated as a single line break regardless of any other characters it contains, even if they are additional line breaks. If you generate your own Word array to provide to splitWords() or drawText(), make sure you separate all line breaks into separate words.
Text Wrapping
Text wrapping is supported via the DrawTextConfig.textWrap config option. Two modes are supported:
'wrap': (Default) Text wraps at the horizontal boundaries of the render box.
'none': Text does not wrap and will either overflow past the horizontal boundaries of the render box, or if overflow=false, get clipped at the boundaries.
💡 To achieve spreadsheet-style clipping, use textWrap='none' and overflow=false.
Examples
Web Worker and OffscreenCanvas
If you want to draw the text yourself, or even offload the work of splitting the words to a Web Worker using an OffscreenCanvas, you can use the splitWords() API directly.
This requires using wordsToJson() and specToJson() APIs to ensure all required information is properly transferred between the UI/main thread and the worker thread, particularly concerning the cached TextMetrics.
Sample code
Add a Canvas to your DOM:
<canvas id="my-canvas" width="500" height="500"></canvas>
Define a Web Worker, worker.js:
import { splitWords, specToJson } from 'text-to-canvas';
const wrapLines = ({ containerWidth, wordsStr, baseFormat }) => {
const canvas = new OffscreenCanvas(containerWidth, 100);
const context = canvas.getContext('2d');
const words = JSON.parse(wordsStr);
const spec = splitWords({
ctx: context,
words,
x: 0,
y: 0,
width: containerWidth,
align: 'left',
vAlign: 'top',
format: baseFormat,
height: 100,
});
self.postMessage({
type: 'renderSpec',
specStr: specToJson(spec),
});
};
self.onmessage = (event) => {
if (event.data.type === 'split') {
wrapLines(event.data);
}
};
export {};
Use the Worker thread to offload the work of measuring and splitting the words:
import { Word, RenderSpec, TextFormat, wordsToJson, getTextStyle } from 'text-to-canvas';
const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');
const drawWords = (baseFormat: TextFormat, spec: RenderSpec) => {
const {
lines,
height: totalHeight,
textBaseline,
textAlign,
} = spec;
ctx.save();
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = getTextStyle(baseFormat);
ctx.fillStyle = baseFormat.fontColor;
ctx.strokeStyle = baseFormat.strokeColor;
ctx.lineJoin = 'round';
const baseStrokeWidth = baseFormat.strokeWidth
lines.forEach((line) => {
line.forEach((pw) => {
if (!pw.isWhitespace) {
if (pw.format) {
ctx.save();
ctx.font = getTextStyle(pw.format);
if (pw.format.fontColor) {
ctx.fillStyle = pw.format.fontColor;
}
if (pw.format.strokeColor) {
ctx.strokeStyle = pw.format.strokeColor;
}
}
ctx.fillText(pw.word.text, pw.x, pw.y);
const lineWidth = typeof pw.format?.strokeWidth === 'number'
? pw.format.strokeWidth
: baseStrokeWidth;
if (lineWidth > 0) {
ctx.lineWidth = lineWidth;
ctx.strokeText(pw.word.text, pw.x, pw.y);
}
if (pw.format) {
ctx.restore();
}
}
});
});
};
const words: Word[] = [
{ text: 'Lorem' },
{ text: 'ipsum', format: { fontWeight: 'bold', fontColor: 'red' } },
{ text: 'dolor', format: { fontStyle: 'italic' } },
{ text: 'sit' },
{ text: 'amet' },
];
const baseFormat: TextFormat = {
fontSize: 12,
fontFamily: 'Times New Roman',
fontColor: 'black',
};
const worker = new Worker('./worker.js', { type: 'module' });
worker.postMessage({
type: 'split',
containerWidth: 500,
wordsStr: wordsToJson(words),
baseFormat,
});
worker.onmessage = (event) => {
if (event.data?.type === 'renderSpec') {
worker.terminate();
const spec: RenderSpec = JSON.parse(event.data.specStr);
drawWords(baseFormat, spec);
}
};
Help
Blurry text
If you're experiencing an issue where, "the text quality rendered on the canvas appears lower than that rendered in the DOM across various device resolutions," this may be caused by pixel density settings. See the discussion on issue #69 for a possible solution.