I just needed a text box. Why was that so hard? The story of building my own simple, lightweight text editor and npm package in just 5.2 kB.
It started with a simple brief
The requirement sounded almost too straightforward: build a task management app. Not a Jira replacement. Not an enterprise monster. Something clean, simple, and usable by any small team even one with zero technical background. A tool people could open on day one and actually understand.
We drew the wireframes, broke down the features, and started building. Everything was going smoothly until we got to the task description field.
“We need rich text. Just basic rich text. Bold, italics, links, lists. That’s it.”
We needed rich text. Not much. Bold, italic, links, bullet lists. Enough that someone could write “Follow up with the client about the proposal, see notes” and have it actually look like a note. That was the whole spec.
What followed was the part nobody warns you about.
The text editor problem
It started where most Angular developers start: looking at what’s already out there. ngx-quill, ngx-editor, the various TipTap wrappers, ProseMirror bindings. Each one looked promising in the README and then fell apart the moment I checked the bundle.
Some clocked in past 200 kB. One of them pulled 14 transitive dependencies for features I wasn’t going to ship: image uploads, code blocks, math rendering, real-time collaboration, full markdown editing with a split preview pane.
I was building a task description field. None of that belonged in my app.
The bundle size was the obvious problem. The deeper problem only showed up when I started using the editors myself.
Try this. Open ChatGPT, ask it for a summary, copy the response, paste it into one of these editors. What do you get? You get this, sitting in your beautiful task field:
## Summary
**Key points:**
- The proposal needs revising
- Client wants a call by Friday
Asterisks and hash signs and dashes, sitting there as plain text. Your user, who has never heard the word “markdown,” sees garbage and concludes your app is broken.
That was the moment I stopped looking and started building.
What I actually needed
Let me be upfront about something. Writing your own text editor is usually a bad idea. There’s a reason ProseMirror and Quill exist. They handle a thousand edge cases you haven’t thought about, from IME composition to selection across nested nodes.
But I wasn’t trying to compete with them. I was trying to do less. A lot less.
The constraints I gave myself:
- Under 6 kB gzipped. If I couldn’t hit that, the whole exercise was pointless and I’d just eat the cost of a heavy option.
- One peer dependency, and it had to be DOMPurify. Sanitization is non-negotiable.
- Standalone Angular component. No NgModule. Works with Angular 18 signals out of the box.
- Reactive Forms and ngModel support, because anything else is a deal-breaker for real apps.
- Auto-detect markdown on paste and convert it. This was the whole point.
What came out the other side:
| Minified | 23.2 kB |
| Gzipped | 5.2 kB |
| Download on slow 3G | 104 ms |
| Download on 4G | 6 ms |
| Dependencies | 1 (DOMPurify) |
How the markdown paste actually works
This is the bit I’m most happy with, so let me walk through it.
When a paste event fires, the first thing the code does is grab the plain-text representation of the clipboard, not the HTML. Then it runs a quick detection pass: does this text contain heading prefixes, bold markers, list bullets, or markdown link syntax?
If yes, the text routes through MarkdownParserService, which converts the supported subset to HTML. Then everything goes through SanitizerService, which is DOMPurify with a strict config. Only then does anything touch the DOM.
If no markdown is detected, the editor falls back to the clipboard’s HTML payload, sanitizes it, and inserts.
So when your user pastes that ChatGPT summary, they see real headings, real bold text, a proper bullet list. They don’t have to do anything. They don’t even know markdown happened.
A couple of honest caveats. The supported syntax is deliberately narrow: h1, h2, bold, italic, links, ordered and unordered lists. That’s it. Code blocks pass through as plain text. Tables pass through as plain text. Blockquotes too. h3 and deeper render as paragraphs. If your users need to author technical documentation, this is the wrong tool. For task descriptions, meeting notes, or comments on a card? It’s the right amount.
Security, because contenteditable is a footgun
Anything that accepts paste input is an XSS target. The editor treats that as a hard requirement, not an afterthought.
Every HTML string entering the editor passes through DOMPurify with this config:
DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['h1','h2','p','strong','em','s','a','ul','ol','li','br','span'],
ALLOWED_ATTR: ['href','target','rel','class'],
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script','style','iframe','object','embed'],
});
A post-sanitization hook handles links specifically: target="_blank" and rel="noopener noreferrer" get enforced on every anchor, and any href starting with javascript:, data:, or vbscript: has the attribute stripped. The anchor and visible text are preserved, just without the link.
There is no bypass path. Not for [content], not for paste, not for ControlValueAccessor.writeValue. If you find one, file an issue.
Dropping it into your app
Install:
npm install ng-text-editor-lite dompurify
npm install --save-dev @types/dompurify
Use it. Standalone component, so no NgModule wiring:
import { Component, signal } from '@angular/core';
import { EditorComponent, EditorConfig, EditorEventPayload } from 'ng-text-editor-lite';
@Component({
standalone: true,
imports: [EditorComponent],
template: `
`,
})
export class TaskFormComponent {
html = signal('');
config: EditorConfig = {
placeholder: 'Describe the task...',
maxLength: 3000,
theme: 'light',
};
onChanged(event: EditorEventPayload): void {
this.html.set(event.html);
}
}
For Reactive Forms, it implements ControlValueAccessor, so this just works:
Calling control.disable() puts the editor into disabled mode automatically. No extra wiring needed.
Theming
All visual tokens are CSS custom properties scoped to the host element. The built-in light and dark themes are just default values for those properties:
ng-text-editor-lite {
--ngx-editor-bg: #fdf6e3;
--ngx-editor-color: #657b83;
--ngx-editor-toolbar-bg: #eee8d5;
--ngx-editor-link-color: #268bd2;
}
The editor uses scoped selectors and an all: unset reset on the editable surface, so Bootstrap, Tailwind, and Angular Material can’t accidentally override the typography inside the editor. That cuts a whole class of “why are my headings huge” bug reports before they ever come in.
Switching themes at runtime is one config update:
this.config.update(c => ({
...c,
theme: c.theme === 'light' ? 'dark' : 'light'
}));
What it isn’t
A few things I want to be honest about, because picking a library on incomplete information is how you waste a week.
- No SSR support. It’s a browser-only package. If you’re rendering on the server, this won’t work for you.
- No image upload, no code blocks, no tables, no nested lists. If your app needs any of those, you want one of the heavier editors.
- The toolbar is fixed. There’s no plugin system. You get bold, italic, strikethrough, links, lists, and that’s it.
If those constraints fit your app, the bundle savings are real. If they don’t, pick something else. I won’t be offended.
The package
@thedevankit/ng-text-editor-lite on npm.
I wrote it because I wanted a text editor that fit the job, not the other way around. The task app it was built for is shipping with it.
If you’re solving a similar problem, give it a try. PRs, bug reports, and honest feedback are all welcome on the repo. GitHub Repo Link:


