Skip to main content

Text editing in HTML5 canvas with Konva

Users cannot edit Konva.Text content directly for many reasons. The Canvas API is not designed for text editing. It is possible to emulate text editing on canvas (by drawing blinking cursor, emulate selection, etc). Konva has not support for such case. We recommend to edit the user input outside of your canvas with native DOM elements such as input or textarea.

If you want to enable full rich text editing features see Rich Text Demo.

Instructions: Double click on text to edit it. Type something. Press Enter or click outside to save changes.

import Konva from 'konva';

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

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

const textNode = new Konva.Text({
text: 'Some text here',
x: 50,
y: 80,
fontSize: 20,
draggable: true,
width: 200,
});

layer.add(textNode);

const tr = new Konva.Transformer({
nodes: [textNode],
enabledAnchors: ['middle-left', 'middle-right'],
boundBoxFunc: function (oldBox, newBox) {
newBox.width = Math.max(30, newBox.width);
return newBox;
},
});

textNode.on('transform', function () {
textNode.setAttrs({
width: textNode.width() * textNode.scaleX(),
scaleX: 1,
});
});

layer.add(tr);

textNode.on('dblclick dbltap', () => {
textNode.hide();
tr.hide();

const textPosition = textNode.absolutePosition();
const stageBox = stage.container().getBoundingClientRect();

const areaPosition = {
x: stageBox.left + textPosition.x,
y: stageBox.top + textPosition.y,
};

const textarea = document.createElement('textarea');
document.body.appendChild(textarea);

textarea.value = textNode.text();
textarea.style.position = 'absolute';
textarea.style.top = areaPosition.y + 'px';
textarea.style.left = areaPosition.x + 'px';
textarea.style.width = textNode.width() - textNode.padding() * 2 + 'px';
textarea.style.height = textNode.height() - textNode.padding() * 2 + 5 + 'px';
textarea.style.fontSize = textNode.fontSize() + 'px';
textarea.style.border = 'none';
textarea.style.padding = '0px';
textarea.style.margin = '0px';
textarea.style.overflow = 'hidden';
textarea.style.background = 'none';
textarea.style.outline = 'none';
textarea.style.resize = 'none';
textarea.style.lineHeight = textNode.lineHeight().toString();
textarea.style.fontFamily = textNode.fontFamily();
textarea.style.transformOrigin = 'left top';
textarea.style.textAlign = textNode.align();
textarea.style.color = textNode.fill().toString();

const rotation = textNode.rotation();
let transform = '';
if (rotation) {
transform += 'rotateZ(' + rotation + 'deg)';
}
transform += 'translateY(-' + 2 + 'px)';
textarea.style.transform = transform;

textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 3 + 'px';

textarea.focus();

function removeTextarea() {
textarea.parentNode.removeChild(textarea);
window.removeEventListener('click', handleOutsideClick);
window.removeEventListener('touchstart', handleOutsideClick);
textNode.show();
tr.show();
tr.forceUpdate();
}

function setTextareaWidth(newWidth = 0) {
if (!newWidth) {
newWidth = textNode.placeholder.length * textNode.fontSize();
}
textarea.style.width = newWidth + 'px';
}

textarea.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
textNode.text(textarea.value);
removeTextarea();
}
if (e.key === 'Escape') {
removeTextarea();
}
});

textarea.addEventListener('keydown', function () {
const scale = textNode.getAbsoluteScale().x;
setTextareaWidth(textNode.width() * scale);
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + textNode.fontSize() + 'px';
});

function handleOutsideClick(e) {
if (e.target !== textarea) {
textNode.text(textarea.value);
removeTextarea();
}
}
setTimeout(() => {
window.addEventListener('click', handleOutsideClick);
});
});

What a textarea overlay does not cover

The method above works well when the visitor edits one field at a time. It stops being sufficient as soon as the text has to behave like a document:

  • Reflow. The overlay does not re-wrap while the box is resized, so the editing view and the drawn view disagree during a transform.
  • Font metrics. A web font that loads after the first paint changes the line height, so cached measurements and the caret position go stale.
  • Per-character styling. Konva.Text applies one style to the whole node. Mixed bold, colour, or size inside one paragraph needs several nodes and your own layout pass.
  • History. Each keystroke has to be folded into the same undo stack as the canvas operations, or undo will jump between two separate timelines.
  • Export fidelity. The exported image is drawn by the canvas, not by the textarea, so any difference between the two shows up in the output.

At that point you are writing a text engine rather than an editor feature. If you would rather not, Polotno is a commercial design editor SDK built with Konva that already solves this.