# Squire: the rich text editor that refuses to own your document

> Squire is a 16KB dependency-free HTML5 editor built for email composition, where the HTML itself stays the source of truth instead of a structured document model. That choice buys blockquote nesting and third-party markup fidelity, and it costs you a toolbar, a sanitizer and a lot of edge cases.

**fastmail/Squire** — The rich text editor for arbitrary HTML.

- Repository: https://github.com/fastmail/Squire
- Stars: 4,916 · Forks: 414
- Language: TypeScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/fastmail-squire

## HTML is the document model, because the input is arbitrary

Squire was designed for email composition in the Fastmail web app, and that single fact decides its architecture. Because an editor may be used to quote or forward mail from third parties, it has to preserve their HTML without breaking the formatting, which rules out the structured internal model that most modern editors use. The HTML stays the source of truth, and the second consequence is the one users notice: several levels of blockquote nest properly. The package is 16KB of JavaScript after minification and gzip, with no dependencies at all. Nothing in the file claims a benchmark or a feature comparison, so the honest summary is narrow and specific: this is an editor engineered around one hard requirement, with size and cross-browser normalisation as the constraints that shaped it.

## No toolbar, no widgets, no overlays

Squire deliberately ships no UI. It is built to be integrated with your own UI framework, and what you get is a component you insert in place of a `<textarea>` and then drive programmatically, so your application does not load two UI toolkits. The price is that every button, menu and state indicator is your code. The file has advice on the state problem specifically: monitor the pathChange event and derive button state from the new path, and when the selection crosses nodes, call hasFormat per button. For a look at the integration surface, the repository carries Demo.html, described as a very bare-bones integration you clone and open; the fuller demo is a Fastmail trial signup. The bug-reporting rule follows the same logic: reproduce with Demo.html first, to rule out your own integration.

## Three lines to an editor, and content arrives afterwards

The whole install is a package, an import and a constructor:

```bash
npm install squire-rte
```

```js
import Squire from 'squire-rte';
editor = new Squire(node);
```

Read the constructor's side effect before you wire it to server-rendered markup: instantiating removes any current children of the node, and you must call the setHTML command after initialising to put content in. A script tag works too, loading dist/squire.js, or squire-raw.js for the debuggable unminified version:

```html
<script type="text/javascript" src="dist/squire.js"></script>
```

You can have several instances on one page without trouble. In a long-lived single-page app the file asks you to call editor.destroy() when an instance is finished, so it does not leak resources, which makes instance ownership something your router has to track.

## Sanitization is mandatory and defaults to a global

The security section is the shortest and the strictest in the file. Malicious HTML is a source of XSS and other problems, so you MUST provide a way to convert raw HTML into DOM nodes. Squire integrates automatically with DOMPurify when DOMPurify is already present in the page. When it is not, you set a custom sanitizeToDOMFragment function in your config, and it is called in place of the DOMPurify call. Its signature is `(html: string, editor: Squire) => DocumentFragment`: it receives the HTML string and the editor instance, and it must return a DocumentFragment node belonging to the same document as the editor's root node, containing clean DOM nodes. That last constraint is the part people trip on, because a fragment from another document cannot be inserted here, and a sanitizer that returns a wrapped element instead of a fragment will not satisfy the contract.

## execCommand is unused, so changeFormat and modifyBlocks are the primitives

The stated philosophy is to let the browser do as much as it can, which the file admits is not very much, and to take control wherever it deviates from what is required or where cross-browser differences are significant. document.execCommand is therefore not used at all. Every formatting action is a custom function, and certain keys, enter and backspace among them, are handled by the editor itself. Two methods carry most of the weight: changeFormat adds or removes any inline formatting you want, and modifyBlocks handles complicated block-level changes. If you need a command the simple API does not expose, the file suggests reading the source, which it notes is not very long, and copying how the other methods are written in terms of those two. That is a deliberate invitation to fork behaviour rather than a gap.

## Blank lines are div, and P is a constructor option

The default block element is a `<div>`, chosen because users conditioned by Microsoft Word expect Enter to behave like a carriage return. If you want paragraphs instead, a config object goes in as the second constructor parameter, and you can attach attributes to every default block:

```js
var editor = new Squire(document, {
    blockTag: 'P',
    blockAttributes: { style: 'font-size: 16px;' }
});
```

This is a small example with a large consequence. Whitespace in HTML is significant enough that the editor has to normalise across browsers to make a paragraph break survive a round trip, and blockAttributes means your base styling is injected into the document rather than applied from a stylesheet, which changes how specificity works against content pasted from elsewhere.

## The package is squire-rte 2.4.9 and the repository has no releases

Naming is the first surprise. This repository is fastmail/Squire, the npm package is squire-rte at version 2.4.9, and the manifest points its repository, bugs and homepage fields at neilj/Squire, with Neil Jenkins as author. The package is ESM: type is module, main is dist/squire.mjs and types is dist/types/Squire.d.ts, while the script tag path in the file still points at dist/squire.js, and dist/ is committed at the repository root next to an .npmignore. Development runs on esbuild with a build.js script followed by tsc, tests run through vitest, and linting is eslint over source with prettier available through the fix script; the devDependencies pin TypeScript, eslint, prettier, jsdom and vitest. The MIT license is the only license. This repository publishes no GitHub releases, so the version trail lives in package.json and CHANGELOG.md, with the last push on 2026-09-16.

## Conclusion

Squire is the right pick when your editor has to survive input it did not write: quoted mail from strangers, forwarded threads, nested blockquotes, markup from another client. It is the wrong pick when you want a turnkey editing surface, because there is no toolbar, and when you cannot supply a sanitization path, because the file is blunt about that being your responsibility. Before you commit, check four things in your own app: that a DocumentFragment based sanitizeToDOMFragment can run in your document, that constructor side effects wiping the node's children will not eat server-rendered content, that destroy() gets called on single-page routes, and that your target browsers match the no-IE support statement.

## FAQ

### What is Squire used for?

It is an HTML5 rich text editor built for email composition in the Fastmail web app, sized at 16KB of JavaScript after minification and gzip with no dependencies. Its distinguishing requirement is handling arbitrary HTML, so quoted and forwarded mail keeps its formatting and blockquotes nest to several levels.

### How do I install Squire and create an editor?

Run `npm install squire-rte`, then `import Squire from 'squire-rte';`, then call `editor = new Squire(node);`. Instantiating removes any current children of the node, so you have to call the setHTML command afterwards to insert content.

### Does Squire sanitize HTML for me?

You must supply the path. Squire uses DOMPurify automatically when it is present in the page; otherwise you set a custom sanitizeToDOMFragment in your config, taking the HTML string and the editor instance and returning a DocumentFragment that belongs to the same document as the editor's root node.

### Does Squire come with a toolbar or any UI?

No. It is designed to be integrated with your own UI framework and provides no toolbar, widgets or overlays. You insert it in place of a textarea and drive it programmatically, which is why Demo.html in the repository is described as a very bare-bones integration.

### Does Squire use document.execCommand?

Not at all. All formatting is done through custom functions built on changeFormat for inline formatting and modifyBlocks for block-level changes, and keys such as enter and backspace are handled by the editor rather than by the browser default.

### Which browsers does Squire support, and what is the package called?

The npm package is squire-rte at version 2.4.9, published as ESM with main dist/squire.mjs and types dist/types/Squire.d.ts, under the MIT license. Browser support is stated as all reasonably recent browsers, with no version of IE supported.

## Sources

- [fastmail/Squire on GitHub](https://github.com/fastmail/Squire)
- [Issues](https://github.com/fastmail/Squire/issues)
- [License: MIT](https://github.com/fastmail/Squire/blob/master/LICENSE)
- [README](https://github.com/fastmail/Squire/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/fastmail-squire
