Skip to main content

How to show rich html on canvas with Konva

How to show complex styles (like bold) and enable rich text editing features?

Canvas's text API is very limited. Konva.Text allows you to add many different styles, support multiline text, etc. But at the current moment it has limitations. You can't use different styles for different parts of Konva.Text. For that case you have to use several Konva.Text instances.

If you want to show complex styles on canvas, you can use render-tag — a library that renders HTML + CSS directly onto canvas using the 2D API. No SVG, no foreignObject, fully synchronous.

The idea is:

  1. Create a custom Konva.Shape with an html property
  2. Use render-tag to compute layout and draw styled text onto the canvas context
  3. The shape auto-sizes its height based on the HTML content

Instructions: Try to type and format text in the editor. The formatted text will be rendered on the canvas below it. You can drag the rendered text around.

import Konva from 'konva';
import { Factory } from 'konva/lib/Factory';
// render-tag: renders HTML+CSS onto canvas via pure 2D API
// loaded from CDN to avoid Sandpack transpilation issues
var computeLayout, drawLayout;

// Create a custom Konva shape that renders HTML via render-tag
// Use Reflect.construct to extend ES6 class from transpiled code
function RichText(config) {
  var instance = Reflect.construct(Konva.Shape, [config], RichText);
  instance._layoutResult = null;
  instance.on('htmlChange widthChange', function () {
    this._recomputeLayout();
  });
  instance._recomputeLayout();
  return instance;
}
RichText.prototype = Object.create(Konva.Shape.prototype);
RichText.prototype.constructor = RichText;
RichText.prototype.className = 'RichText';

RichText.prototype._recomputeLayout = function () {
  var html = this.html();
  var width = this.width() || 200;
  if (!html) {
    this._layoutResult = null;
    return;
  }
  this._layoutResult = computeLayout({ html: html, width: width });
};

RichText.prototype._sceneFunc = function (context) {
  if (!this._layoutResult) return;
  var width = this.width() || 200;
  drawLayout({
    layout: this._layoutResult,
    width: width,
    ctx: context._context,
    pixelRatio: 1,
  });
};

RichText.prototype._hitFunc = function (context) {
  var width = this.width() || 200;
  var height = this.height() || (this._layoutResult ? this._layoutResult.height : 0);
  context.beginPath();
  context.rect(0, 0, width, height);
  context.closePath();
  context.fillStrokeShape(this);
};

Factory.addGetterSetter(RichText, 'html', '');
Factory.addGetterSetter(RichText, 'width', 200);
Factory.addGetterSetter(RichText, 'height', 0);

// --- Toolbar + contenteditable editor ---
function execCmd(cmd, val) {
  document.execCommand(cmd, false, val || null);
  editor.focus();
}

var toolbar = document.createElement('div');
toolbar.innerHTML = [
  '<button onclick="return false" data-cmd="bold"><b>B</b></button>',
  '<button onclick="return false" data-cmd="italic"><i>I</i></button>',
  '<button onclick="return false" data-cmd="underline"><u>U</u></button>',
  '<button onclick="return false" data-cmd="formatBlock" data-val="h1">H1</button>',
  '<button onclick="return false" data-cmd="formatBlock" data-val="h2">H2</button>',
  '<button onclick="return false" data-cmd="foreColor" data-val="red" style="color:red">A</button>',
].join('');
toolbar.style.cssText = 'display:flex;gap:4px;margin-bottom:4px;';
toolbar.querySelectorAll('button').forEach(function (btn) {
  btn.style.cssText = 'padding:2px 8px;cursor:pointer;';
  btn.addEventListener('mousedown', function (e) {
    e.preventDefault();
    execCmd(btn.dataset.cmd, btn.dataset.val);
  });
});
document.body.prepend(toolbar);

var editor = document.createElement('div');
editor.contentEditable = true;
editor.style.cssText = 'border:1px solid #ccc;padding:8px;min-height:60px;margin-bottom:8px;';
editor.innerHTML =
  'That is <u>some</u> <span style="color:red"> styled text</span> on <strong>canvas</strong>!' +
  '<h2>What do you think about it?</h2>';
var container = document.getElementById('container');
document.body.insertBefore(editor, container);

// --- Load render-tag and set up canvas ---
var loadScript = function (src) {
  return new Promise(function (resolve, reject) {
    var s = document.createElement('script');
    s.src = src;
    s.onload = resolve;
    s.onerror = reject;
    document.head.appendChild(s);
  });
};

loadScript('https://cdn.jsdelivr.net/npm/render-tag/lib/render-tag.umd.js').then(function () {
  computeLayout = RenderTag.layout;
  drawLayout = RenderTag.drawLayout;

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

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

  var shape = new RichText({
    x: 10,
    y: 10,
    width: 400,
    draggable: true,
    html: editor.innerHTML,
  });
  layer.add(shape);

  editor.addEventListener('input', function () {
    shape.html(editor.innerHTML);
  });
});