1Rendering Text on the Canvas
▶
The HTML Canvas API is most commonly associated with drawing shapes, paths, and images, but it also provides a powerful and flexible system for rendering text directly onto a canvas element. Unlike DOM-based text, which is laid out and styled by the browser's rendering engine, canvas text is drawn as pixels — meaning it becomes part of the bitmap and can be combined seamlessly with graphics, animations, and visual effects. Understanding how to configure and use the canvas text API opens the door to building data visualizations with labels, game HUDs, custom typography effects, chart annotations, and much more.
Text on the canvas is rendered through the 2D drawing context, the same object used to draw rectangles, arcs, and paths. This means text shares the same coordinate system, transformation matrix, and compositing rules as everything else on the canvas. It also means that properties you set on the context — such as fill color or line width — will persist and affect all subsequent drawing operations unless explicitly changed. Keeping track of context state is therefore an important discipline when mixing text with other graphics.
Setting Up the Canvas Context for Text
Before any text (or any drawing) can happen, you must obtain a reference to the canvas element and retrieve its 2D rendering context. The standard approach is to select the canvas element from the DOM and call getContext('2d') on it. The returned context object — conventionally named ctx — is the single gateway to all drawing operations, including text rendering.
<canvas id="myCanvas" width="600" height="300"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
</script>
The ctx object exposes all text-related properties (font, fillStyle, textAlign, textBaseline) and methods (fillText(), strokeText(), measureText()) that you will use throughout text rendering. Because all of these live on the same shared context object, any property change you make — for example, changing ctx.fillStyle to draw a red background rectangle — will remain in effect when you later call fillText() unless you reset it. A common best practice is to wrap distinct drawing operations in ctx.save() and ctx.restore() calls, which push and pop a snapshot of the context's state, preventing unintended side effects.
ctx.save();
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 200, 100); // red rectangle
ctx.restore(); // fillStyle is no longer red
ctx.fillStyle = 'black';
ctx.fillText('Hello, Canvas!', 20, 200); // drawn in black
Configuring the Font Property
The ctx.font property controls every typographic aspect of how characters are shaped: their typeface family, weight, style, and size. The value you assign to it follows exactly the same shorthand syntax used by the CSS font property, which makes it familiar to anyone with web development experience.
The general order of values in the shorthand is: font-style, font-variant, font-weight, font-size (required), and font-family (required). Any optional tokens that are omitted default to their initial values.
// Minimal: size and family only
ctx.font = '16px Arial';
// With weight
ctx.font = 'bold 24px Georgia';
// With style and weight
ctx.font = 'italic bold 20px Verdana';
// With small-caps variant
ctx.font = 'small-caps 18px serif';
One of the most common pitfalls is omitting the unit from the font size. Writing '16 Arial' instead of '16px Arial' results in the browser ignoring the entire assignment and falling back to the previous font value (the default is '10px sans-serif'). Always include a valid CSS length unit: px (pixels) is the most predictable for canvas work, but em, rem, and pt are also accepted.
Web-safe fonts such as Arial, Georgia, Verdana, Times New Roman, and Courier New are reliably available across platforms. You can also use custom fonts loaded via @font-face or the Google Fonts API — but be aware that a font must be fully loaded by the browser before it can be used on the canvas. Drawing text before the font file has loaded will fall back to the browser's default font. Using the CSS Font Loading API (document.fonts.ready) ensures fonts are available before you start drawing.
document.fonts.ready.then(() => {
ctx.font = 'bold 32px "Open Sans"';
ctx.fillText('Custom Font Text', 50, 100);
});
Filling Text with fillText()
The fillText() method is the primary way to draw solid text on the canvas. It fills the interior of each character's glyph with the current fillStyle, which can be a CSS color string, a CanvasGradient, or a CanvasPattern.
The method signature is:
ctx.fillText(text, x, y [, maxWidth]);
- text — the string to draw.
- x — the horizontal position of the text's anchor point (affected by
textAlign). - y — the vertical position of the text's anchor point (affected by
textBaseline). - maxWidth (optional) — a maximum pixel width; if the text would exceed this, the browser scales or condenses it to fit.
ctx.font = 'bold 36px Arial';
ctx.fillStyle = 'steelblue';
ctx.fillText('Hello, World!', 50, 100);
Setting fillStyle to a gradient creates visually striking text with no additional complexity:
const gradient = ctx.createLinearGradient(0, 0, 400, 0);
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.5, 'orange');
gradient.addColorStop(1, 'gold');
ctx.font = 'bold 48px Impact';
ctx.fillStyle = gradient;
ctx.fillText('Gradient Text', 20, 80);
The optional maxWidth parameter is a simple but useful tool for preventing text overflow in constrained layouts. If the measured width of the string exceeds the specified maximum, the browser applies a scaling transform to the glyphs horizontally, compressing them. This can produce a slightly condensed appearance, so it should be used judiciously — ideally as a safety net rather than a primary layout strategy.
ctx.font = '24px Arial';
ctx.fillStyle = 'black';
// This long string will be compressed to fit within 200px
ctx.fillText('A very long label that might overflow', 10, 50, 200);
Stroking Text with strokeText()
Where fillText() fills the body of each glyph, strokeText() draws only the outline — the stroked path along each character's edges — leaving the inside transparent. This is analogous to the difference between fillRect() and strokeRect() for shapes.
ctx.font = 'bold 60px Georgia';
ctx.strokeStyle = 'darkred';
ctx.lineWidth = 2;
ctx.strokeText('Outlined', 50, 120);
The stroke color is controlled by ctx.strokeStyle, and the thickness of the outline is controlled by ctx.lineWidth. Because the stroke is centered on the glyph's path, a very thick lineWidth will extend both inward and outward, which can obscure fine details at small font sizes.
One of the most visually effective techniques is layering fillText() and strokeText() on top of each other to produce filled characters with a visible border. The order matters: draw the stroke first (so it appears underneath) and then the fill on top, or draw the fill first and the stroke on top for an overlay effect.
ctx.font = 'bold 72px Impact';
// Draw white fill first
ctx.fillStyle = 'white';
ctx.fillText('COMICS', 40, 120);
// Draw black stroke on top as an outline
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.strokeText('COMICS', 40, 120);
Alternatively, drawing the stroke first ensures that the fill covers the inner half of the stroke line, producing a cleaner external outline:
ctx.font = 'bold 72px Impact';
ctx.strokeStyle = 'black';
ctx.lineWidth = 6; // thicker stroke, drawn first
ctx.strokeText('OUTLINED', 40, 120);
ctx.fillStyle = 'yellow';
ctx.fillText('OUTLINED', 40, 120); // fill covers the inner portion
Controlling Text Alignment
The ctx.textAlign property determines how the text string is positioned horizontally relative to the x coordinate passed to fillText() or strokeText(). It accepts five values:
| Value | Behavior | Typical Use Case |
|---|---|---|
'left' |
The left edge of the text sits at the x coordinate (default). | Standard left-to-right body text placement. |
'right' |
The right edge of the text sits at the x coordinate. | Right-aligned labels, legends. |
'center' |
The text is centered horizontally around the x coordinate. | Centered headings, data labels on bar charts. |
'start' |
Same as 'left' for LTR text; same as 'right' for RTL text. |
Internationalized applications. |
'end' |
Same as 'right' for LTR text; same as 'left' for RTL text. |
Internationalized applications. |
The distinction between 'left'/'right' and 'start'/'end' is subtle but important for global applications. In a right-to-left writing system (such as Arabic or Hebrew), 'start' will automatically anchor text to the right, matching the natural reading direction, without any extra code changes. For most English-language canvas work, 'left', 'right', and 'center' are the practical choices.
A very common use of 'center' alignment is placing a label directly above the center of a bar in a bar chart:
function drawBar(ctx, x, barWidth, barHeight, canvasHeight, label) {
const barX = x;
const barY = canvasHeight - barHeight;
// Draw the bar
ctx.fillStyle = 'steelblue';
ctx.fillRect(barX, barY, barWidth, barHeight);
// Draw the centered label above the bar
ctx.font = '14px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.fillText(label, barX + barWidth / 2, barY - 8);
}
By setting textAlign to 'center' and passing barX + barWidth / 2 as the x coordinate, the label is perfectly centered over the bar regardless of its pixel width.
Setting the Text Baseline
The ctx.textBaseline property controls how the text is aligned vertically relative to the y coordinate. This is the canvas equivalent of the CSS vertical-align concept, and choosing the wrong baseline can result in text that appears subtly (or dramatically) misplaced.
| Value | Description | Best For |
|---|---|---|
'alphabetic' |
The baseline on which most Latin letters "sit" (default). Descenders like g, p, and y extend below it. | General Latin text, consistent with typography norms. |
'top' |
The top of the em square (roughly the tallest ascender) aligns with y. | Placing text so its top edge is at an exact pixel row. |
'middle' |
The middle of the em square aligns with y. | Vertically centering text within a bounding box or next to an icon. |
'bottom' |
The bottom of the em square aligns with y. | Placing text so its bottom edge is at an exact pixel row. |
'hanging' |
Used in Indic scripts where glyphs hang from a top line. | Devanagari and similar scripts. |
'ideographic' |
Bottom of ideographic character box; used for CJK scripts. | Chinese, Japanese, Korean text. |
For most canvas drawing tasks involving Latin text, the two baselines you will use most often are 'alphabetic' (the default) and 'middle'. Using 'middle' is especially useful when you want to center a text label vertically within a drawn shape, such as a button rectangle or a pie chart slice:
const rectX = 100, rectY = 60, rectW = 200, rectH = 50;
// Draw a button shape
ctx.fillStyle = '#4CAF50';
ctx.fillRect(rectX, rectY, rectW, rectH);
// Center the label inside the button
ctx.font = 'bold 18px Arial';
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Click Me', rectX + rectW / 2, rectY + rectH / 2);
With both textAlign set to 'center' and textBaseline set to 'middle', the text is positioned at the exact geometric center of the rectangle — a pattern used constantly in canvas-based UI components and data visualizations.
Measuring Text Width with measureText()
Because the canvas does not automatically wrap or reflow text the way the browser's layout engine does for HTML, you are responsible for calculating whether text fits within available space. The ctx.measureText() method is the essential tool for this job. It takes a string as its argument and returns a TextMetrics object that describes the dimensional properties of that string as it would be rendered with the current font settings.
ctx.font = 'bold 24px Arial';
const metrics = ctx.measureText('Hello, Canvas!');
console.log(metrics.width); // e.g., 187.5 (pixels)
The most widely supported property on the TextMetrics object is width. Modern browsers also expose additional properties such as actualBoundingBoxAscent, actualBoundingBoxDescent, fontBoundingBoxAscent, and fontBoundingBoxDescent, which allow you to calculate the precise height of a text block — essential for exact vertical centering or collision detection with other drawn elements.
A critical rule: always set ctx.font before calling measureText(). The returned width is computed based on whatever font is currently active. Measuring with the wrong font and then switching fonts before drawing will produce a mismatch between the measured and rendered widths.
// Wrong: measure before setting font
const badMetrics = ctx.measureText('Test'); // uses previous/default font
ctx.font = 'bold 24px Arial';
ctx.fillText('Test', 50, 50); // rendered width differs from badMetrics.width
// Correct: set font first
ctx.font = 'bold 24px Arial';
const goodMetrics = ctx.measureText('Test'); // accurate for this font
ctx.fillText('Test', 50, 50); // rendered width matches goodMetrics.width
One of the most important practical applications of measureText() is building a manual text-wrapping function. Since the canvas provides no built-in line breaking, you must write logic that splits text into words, measures each word, and wraps to a new line when the accumulated width exceeds the available area.
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split(' ');
let line = '';
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && i > 0) {
ctx.fillText(line.trim(), x, y);
line = words[i] + ' ';
y += lineHeight;
} else {
line = testLine;
}
}
// Draw the last remaining line
ctx.fillText(line.trim(), x, y);
}
// Usage
ctx.font = '16px Arial';
ctx.fillStyle = 'black';
wrapText(
ctx,
'This is a longer paragraph of text that should wrap across multiple lines on the canvas.',
20, // x
40, // y
300, // maxWidth
24 // lineHeight in pixels
);
In this function, each word is tentatively added to the current line and measured. If the test line's width exceeds maxWidth, the current line is drawn and a new line begins with the word that caused the overflow. The y coordinate is incremented by lineHeight for each new line, manually replicating the behavior that the browser provides automatically for HTML text. The lineHeight value is typically set to 1.2–1.5 times the font size for comfortable reading.
measureText() is also useful for right-aligning or centering text without relying on textAlign, for detecting whether a user's mouse click falls within a rendered text string, and for precisely positioning decorative elements (such as underlines or highlight boxes) relative to known text dimensions. The combination of font configuration, fill and stroke methods, alignment controls, and text measurement gives you everything needed to implement sophisticated typographic layouts directly on the canvas.