Skip to main content

How to use custom font for HTML5 canvas?

How to draw external font on html5 canvas?

If you want to use custom font for Konva.Text you just need to:

  1. Add font style to your page
  2. Set fontFamily attribute to required font-face when font is loaded

But there is one important thing here. When you set font for DOM elements (like div or span) browsers will automatically update that elements when font is loaded. But it doesn't work the same for canvas text. You need to redraw canvas again.

Note: For older browsers that don't support the native Font Loading API, you can use a width measurement approach - measure text width with fallback font, then periodically check if the custom font width differs (indicating it's loaded).

Loading font

You may use this async function that combines the native Font Loading API with a reliable width-measurement fallback and timing guard:

const loadedFonts = {};

function measureFont(fontName, fallbackFont, fontStyle = 'normal', fontWeight = '400') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const sampleText = 'The quick brown fox 0123456789';
ctx.font = `${fontStyle} ${fontWeight} 16px '${fontName}', ${fallbackFont}`;
return ctx.measureText(sampleText).width;
}

function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function loadFont(fontName, fontStyle = 'normal', fontWeight = '400') {
if (loadedFonts[fontName]) return;

const hasFontsLoadSupport = !!(document.fonts && document.fonts.load);
const arialWidth = measureFont('Arial', 'Arial', fontStyle, fontWeight);

if (hasFontsLoadSupport) {
try {
await document.fonts.load(`${fontStyle} ${fontWeight} 16px '${fontName}'`);
const newWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const shouldTrustChanges = arialWidth !== newWidth;
if (shouldTrustChanges) {
// Small guard delay to avoid rare race when metrics are not ready yet
await delay(60);
loadedFonts[fontName] = true;
return;
}
} catch (e) {
// ignore and fallback to polling
}
}

const timesWidth = measureFont('Times', 'Times', fontStyle, fontWeight);
const lastWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const waitTime = 60;
const timeout = 6000; // do not wait more than 6 seconds
const attemptsNumber = Math.ceil(timeout / waitTime);
for (let i = 0; i < attemptsNumber; i++) {
const newWidthArial = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const newWidthTimes = measureFont(fontName, 'Times', fontStyle, fontWeight);
const somethingChanged =
newWidthArial !== lastWidth ||
newWidthArial !== arialWidth ||
newWidthTimes !== timesWidth;
if (somethingChanged) {
await delay(60);
loadedFonts[fontName] = true;
return;
}
await delay(waitTime);
}
console.warn(
`Timeout for loading font "${fontName}". Is it a correct font family?`
);
}
import Konva from 'konva';

const loadedFonts = {};

function measureFont(fontName, fallbackFont, fontStyle = 'normal', fontWeight = '400') {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const sampleText = 'The quick brown fox 0123456789';
  ctx.font = `${fontStyle} ${fontWeight} 16px '${fontName}', ${fallbackFont}`;
  return ctx.measureText(sampleText).width;
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function loadFont(fontName, fontStyle = 'normal', fontWeight = '400') {
  if (loadedFonts[fontName]) return;

  const hasFontsLoadSupport = !!(document.fonts && document.fonts.load);
  const arialWidth = measureFont('Arial', 'Arial', fontStyle, fontWeight);

  if (hasFontsLoadSupport) {
    try {
      await document.fonts.load(`${fontStyle} ${fontWeight} 16px '${fontName}'`);
      const newWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
      const shouldTrustChanges = arialWidth !== newWidth;
      if (shouldTrustChanges) {
        await delay(60);
        loadedFonts[fontName] = true;
        return;
      }
    } catch (e) {}
  }

  const timesWidth = measureFont('Times', 'Times', fontStyle, fontWeight);
  const lastWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
  const waitTime = 60;
  const timeout = 6000;
  const attemptsNumber = Math.ceil(timeout / waitTime);
  for (let i = 0; i < attemptsNumber; i++) {
    const newWidthArial = measureFont(fontName, 'Arial', fontStyle, fontWeight);
    const newWidthTimes = measureFont(fontName, 'Times', fontStyle, fontWeight);
    const somethingChanged =
      newWidthArial !== lastWidth ||
      newWidthArial !== arialWidth ||
      newWidthTimes !== timesWidth;
    if (somethingChanged) {
      await delay(60);
      loadedFonts[fontName] = true;
      return;
    }
    await delay(waitTime);
  }
  console.warn(`Timeout for loading font "${fontName}".`);
}

// Load the font using a stylesheet link

const fontLink = document.createElement('link');
fontLink.href = 'https://fonts.googleapis.com/css2?family=Kavivanar&display=swap';
fontLink.rel = 'stylesheet';
document.head.appendChild(fontLink);

// Build stage immediately with fallback font

var width = window.innerWidth;
var height = window.innerHeight;

var stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

var layer = new Konva.Layer();
stage.add(layer);

var text = new Konva.Text({
  x: 50,
  y: 50,
  fontSize: 40,
  text: 'A text with custom font.',
  width: 250,
  fontFamily: 'Arial'
});

layer.add(text);

// Then wait for font to load and apply it

loadFont('Kavivanar', 'normal', '400').then(() => {
  text.fontFamily('Kavivanar');
});