Skip to content
JavaScript js canvas 5 min read

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.font and ctx.fillStyle before 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';
ComponentExampleRequired?
styleitalicNo
weightbold, 600No
size24px, 1.25remYes
familyGeorgia, serifYes

Warning: Web fonts (@font-face) may not be loaded yet when your script first runs, so the canvas falls back to a default family. Await document.fonts.ready (or document.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.

PropertyValues
textAlignstart (default), end, left, right, center
textBaselinealphabetic (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.font with both a size and a family — an invalid string is ignored and silently keeps the old font.
  • Await document.fonts.ready before drawing if you rely on a custom web font, or your first frame will use a fallback.
  • Use textAlign = "center" with textBaseline = "middle" to anchor labels on a point without offset arithmetic.
  • Reach for measureText().width whenever 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).
Last updated June 1, 2026
Was this helpful?