Drawing Text
The canvas is a pixel surface, not a layout engine — so text on a <canvas> works very differently from text in HTML. There is no word wrap, no line breaks, and no CSS box model. Instead you give the context a font, a string, and a single (x, y) coordinate, and it rasterizes those glyphs at that spot. That low-level control is exactly what makes canvas text powerful for charts, games, and image generation, but it also means you own alignment, wrapping, and measurement.
Filling and stroking text
There are two ways to paint text. fillText(text, x, y) paints solid glyphs using the current fillStyle, and strokeText(text, x, y) traces only the glyph outlines using strokeStyle and lineWidth. You can combine both for outlined-and-filled lettering.
const canvas = document.querySelector("#scene");
const ctx = canvas.getContext("2d");
ctx.font = "48px sans-serif";
ctx.fillStyle = "#3b82f6";
ctx.fillText("DevCraftly", 20, 60);
ctx.strokeStyle = "#111827";
ctx.lineWidth = 1;
ctx.strokeText("DevCraftly", 20, 120);
Both methods accept an optional fourth argument, maxWidth. When supplied, the canvas horizontally condenses the glyphs (it does not wrap) so the text fits within that pixel width — useful for labels that must never overflow a column.
ctx.fillText("Squeezed to fit", 10, 40, 120); // condensed into 120px
Tip: Like all canvas styling, the font and fill state is sticky. Set
ctx.fontandctx.fillStylebefore each block of text you draw, especially inside reusable functions, rather than assuming the context is in a known state.
The font property
ctx.font accepts the same shorthand syntax as the CSS font property: optional style and weight, then a required size, then a required family. If the string is invalid, the assignment is silently ignored and the previous font stays in effect — a common reason text “won’t change size.”
ctx.font = "italic bold 24px Georgia, serif";
ctx.font = "16px system-ui";
ctx.font = '600 1.25rem "Fira Code", monospace';
| Component | Example | Required? |
|---|---|---|
| style | italic | No |
| weight | bold, 600 | No |
| size | 24px, 1.25rem | Yes |
| family | Georgia, serif | Yes |
Warning: Web fonts (
@font-face) may not be loaded yet when your script first runs, so the canvas falls back to a default family. Awaitdocument.fonts.ready(ordocument.fonts.load("16px MyFont")) before drawing if you depend on a custom font.
Alignment and baseline
By default the (x, y) you pass is the position of the text’s left edge and alphabetic baseline. Two properties remap that anchor. textAlign controls horizontal anchoring relative to x, and textBaseline controls vertical anchoring relative to y. Together they let you center, right-align, or vertically middle text without manual offset math.
| Property | Values |
|---|---|
textAlign | start (default), end, left, right, center |
textBaseline | alphabetic (default), top, hanging, middle, ideographic, bottom |
ctx.font = "20px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// Perfectly centered in a 300x150 canvas:
ctx.fillText("Centered", canvas.width / 2, canvas.height / 2);
center + middle is the go-to combination for labeling the center of a shape, a chart slice, or a button. Note that start/end respect the canvas direction (LTR vs RTL), while left/right are absolute.
Measuring text for layout
Because the canvas never wraps, you need measureText(text) to do layout yourself. It returns a TextMetrics object whose width is the rendered advance width in pixels. Modern browsers also expose bounding-box metrics like actualBoundingBoxAscent and actualBoundingBoxDescent, which let you compute true glyph height.
ctx.font = "32px sans-serif";
const m = ctx.measureText("Hello");
const width = m.width;
const height = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent;
console.log(`Hello renders at ${width.toFixed(1)} x ${height.toFixed(1)} px`);
Output:
Hello renders at 78.7 x 23.0 px
Use the measured width to draw a background pill behind a label, right-align a column of numbers, or detect overflow before it happens.
Wrapping multiline text
There is no automatic word wrap, so a multiline paragraph is a loop: split into words, measure each candidate line, and break when it would exceed the maximum width. Lines are stacked manually by advancing y by a chosen line height.
<canvas id="wrap" width="320" height="200" style="border:1px solid #ccc"></canvas>
<script>
const ctx = document.getElementById("wrap").getContext("2d");
ctx.font = "18px system-ui, sans-serif";
ctx.fillStyle = "#111827";
ctx.textBaseline = "top";
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split(" ");
let line = "";
for (const word of words) {
const test = line ? `${line} ${word}` : word;
if (ctx.measureText(test).width > maxWidth && line) {
ctx.fillText(line, x, y);
line = word;
y += lineHeight;
} else {
line = test;
}
}
ctx.fillText(line, x, y); // flush the last line
}
const paragraph =
"Canvas text has no automatic word wrap, so we measure each " +
"candidate line and break it manually to fit the box.";
wrapText(ctx, paragraph, 16, 16, 288, 26);
</script>
The same measureText loop scales to alignment and effects. Here is a self-contained demo combining fill, stroke, and centered alignment:
<canvas id="title" width="320" height="160" style="border:1px solid #ccc"></canvas>
<script>
const ctx = document.getElementById("title").getContext("2d");
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "italic 700 44px Georgia, serif";
ctx.fillStyle = "#f59e0b";
ctx.fillText("Canvas", 160, 60);
ctx.lineWidth = 1.5;
ctx.strokeStyle = "#111827";
ctx.strokeText("Canvas", 160, 60);
ctx.font = "16px system-ui, sans-serif";
ctx.fillStyle = "#6b7280";
ctx.fillText("filled + stroked, centered", 160, 110);
</script>
Best Practices
- Always set
ctx.fontwith both a size and a family — an invalid string is ignored and silently keeps the old font. - Await
document.fonts.readybefore drawing if you rely on a custom web font, or your first frame will use a fallback. - Use
textAlign = "center"withtextBaseline = "middle"to anchor labels on a point without offset arithmetic. - Reach for
measureText().widthwhenever you need to wrap, right-align numbers, or size a background behind text. - Cache wrapped lines if the text and width are unchanged between frames — re-measuring every animation frame is wasteful.
- For crisp text on high-DPI screens, scale the canvas backing store by
devicePixelRatio(see Canvas setup).