The author describes building a custom text editor, experimenting with canvas, contenteditable, and textarea methods, and discusses challenges like performance and accessibility in web development.
# Fine, I’ll build my own text editor!
Source: [https://dbushell.com/2026/09/01/text-editor/](https://dbushell.com/2026/09/01/text-editor/)
Tuesday 1Sept2026
[“They don’t make ’em like Sublime Text anymore”](https://dbushell.com/2026/08/07/sublime-text/)resonated with a lot of folk\. Software these days is garbage\. That got me thinking; I’m good at building garbage\!
Why can’t I build my own text editor?
VS Code is built upon[Monaco Editor](https://microsoft.github.io/monaco-editor/)which is a`<div\>`soup hellscape\. I was late to the VS Code train because for years my Intel inside™ Mac was too slow\. That issue was resolved when I bought Apple silicon\. If that’s the standard I have a lot of room to make mistakes\.
## Canvas
My first experiment renders everything on a`<canvas\>`element\.
You can’t tell, but your CPU is doing a lot of work to render that picture at 60–120 frames per second\. Lack of interactivity is an obvious problem for a text editor\.
I made a list of the “minimum viable” features and implemented them\.
- Pointer down to position text cursor
- Arrow keys to move text cursor
- Highlight current line
- Type to enter text
- Fancy cursor animation
This next demo is interactive, click around and type\.
Before you @ me about Vim bindings: shut up, I’ve got more pressing issues\. Canvas gives me nothing for free\. Amongst many desirable features, I’m missing:
- Text selection
- Undo/redo history
- Multi\-line paste
- Overflow scrolling
That last one is critical\. Life is too short to implement custom elastic scrollbars\. I decided to cheat and use native browser overflow on a hidden element\. A`<div\>`is sized to match the canvas text and the scroll position is used to calculate render offsets on the`canvas`\.
I’m pleased with how that’s coming along but I’m also disheartened because`<canvas\>`is entirely inaccessible\. I could continue to add text selection and other features but I’m not solving the fundamental accessibility issue\.
I had a better idea\.
## Content editable
Instead of rendering text on the`<canvas\>`I can just render it natively in the overflow`<div\>`and make it editable with a[`contenteditable`attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/contenteditable)\. That attribute has a`plaintext\-only`value that is perfect for code\. All content remains within a single text node\.
```
<div
contenteditable="plaintext-only"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
translate="no">
<!-- text goes here -->
</div>
```
Attributes like`spellcheck`must be disabled to avoid input latency spikes\. Want to guess how many days it took me to discover that fix?*Days\!*
Using`contenteditable`gives native text selection and undo history etc\. So much accessibility goodness is wired up for free by the browser\.
The[`Selection API`](https://developer.mozilla.org/en-US/docs/Web/API/Selection)provides metrics I use to continue rendering a custom text cursor\.`::selection`is available so I can style that too\. I’ve set the native[`caret\-color`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/caret-color)invisible, which is probably a no\-no\.
The`contenteditable`technique is promising but I’ve noticed strange performance issues beyond a certain character count\. Chromium browsers perform worse than WebKit and whatever Firefox is now but it’s unpredictable\.
## Textarea
Instead of plaintext`contenteditable`would a simple`<textarea\>`be viable? In short: yes\. Turns out a`<textarea\>`is far more performant for longer text\.
In this final demo I’ve added syntax highlighting too\.
My original plan was to use[custom`::highlight`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::highlight)on the`contenteditable`element\.`<textarea\>`can’t use CSS highlights so a third layer was required\. For demo purposes I added some`<div\>`soup for the visible lines to apply[MicroLighter](https://davatron5000.github.io/microlighter/)\.
**Edit:**I’m told the new[OpaqueRange API](https://olliewilliams.xyz/blog/opaquerange/)unlocks custom highlights for`<textarea\>`— neat\!
Too many CSS highlights are another performance bottleneck\. A more robust solution would be to use[Tree\-sitter](https://tree-sitter.github.io/tree-sitter/)to generate a syntax tree and walk that to generate highlights for only visible lines\. I was hoping to avoid virtualised scrolling entirely but I could improve it using the[inverse sticky technique](https://pierre.computer/writing/on-rendering-diffs#:~:text=systems%20and%20browsers.-,The%20Inverse%20Sticky%20Technique,-For%20CodeView%2C%20many)\. Or I can go back to`contenteditable`because the file sizes I’d be editing don’t hit the performance wall\.
Anyway, looking good, right?
Looks like 90% of a text editor with 1% of the features\. From here it’s pretty straight forward to[draw the rest of the owl](https://knowyourmeme.com/memes/how-to-draw-an-owl)\. I’m tempted to keep drawing but then I think about all the little things like tab indentation\. Right now I just hijack the tab key to insert two spaces…
My demos above are unoptimised and far from perfectly accessible but at least I’m not starting from a losing position\. Rendering on`<canvas\>`would be a nightmare\.
I’m filing this project away for a rainy day\.
---
JavaScript strings and text ranges work with UTF\-16 code units\. It’s easy to naively introduce bugs\. I’m sure my demos are full of them\. I’ll leave with a code example to nerd snipe\.
```
"🍋🟩".length; // 5
[..."🍋🟩"].length; // 3
const segmenter = new Intl.Segmenter("en", {granularity: "grapheme"});
[...segmenter.segment("🍋🟩")].length; // 1
```
Wordgard is a new open-source JavaScript rich-text editor library from Marijn Haverbeke, designed for building customized content editors with a powerful programming interface.
A compact GTK+Adwaita split-view Markdown editor implementation in about 600 lines, using a GTK WebView and CDN-loaded marked.js and KaTeX. It serves as a prototype inspired by Apostrophe to demonstrate how easy building such apps has become.
A veteran macOS/iOS developer recounts the struggle of implementing a Markdown-supporting chat UI using Apple's native frameworks (SwiftUI, AppKit, TextKit) and ultimately finds that web-based technologies like Electron offer a more practical solution for rich text rendering.
The author expands the 'don't roll your own crypto' principle to web development, arguing against custom implementations of scrolling, link navigation, text selection, and other browser-native behaviors.
A developer shares frustration with traditional word processors and presents an experiment to turn Typst into a WYSIWYG editor optimized for e-ink tablets, leveraging AI to generate templates from descriptions.