# Trix: the rich text editor that treats contenteditable as an I/O device

> Trix is a WYSIWYG editor from 37signals that converts browser input into operations on its own document model, then re-renders. It fits simple documents such as messages, comments and articles, and it is a poor fit when you need a full document suite.

**basecamp/trix** — A rich text editor for everyday writing

- Repository: https://github.com/basecamp/trix
- Website: https://trix-editor.org/
- Stars: 20,015 · Forks: 1,133
- Language: JavaScript
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/basecamp-trix

## The contenteditable problem Trix was built to avoid

Trix is aimed at teams building the plain documents that most web apps are made of: messages, comments, articles and lists. The README is explicit about the audience and about what it is not. It is a WYSIWYG editor for "everyday writing", not a document suite.

The design decision that separates Trix from older editors is stated plainly in the README. When Trix was designed in 2014, most WYSIWYG editors wrapped HTML's contenteditable and execCommand APIs, which the README describes as designed by Microsoft for live editing of web pages in Internet Explorer 5.5 and later reverse-engineered by other browsers. Because those APIs were not fully specified, each browser implementation has its own bugs, and developers end up writing the compatibility layer.

Trix sidesteps that by treating contenteditable as an I/O device. Input arrives, Trix converts it into an editing operation on its internal document model, and then re-renders the document back into the editor. The README says this gives Trix complete control over what happens after every keystroke and avoids execCommand entirely. It also notes that all modern production WYSIWYG editors now take this approach, which is a fair admission that the original differentiator has become the baseline.

## How the document model, toolbar and form integration fit together

The mechanism is easiest to see in what Trix renders and what it exposes. Placing an empty trix-editor element on the page causes Trix to insert a separate trix-toolbar before the editor. The toolbar can be moved with the toolbar attribute and pointed at a specific input with the input attribute, so the toolbar does not have to sit next to the editor in the DOM.

Toolbar buttons are declarative. A button carries data-trix-attribute set to an attribute name, and Trix applies it to the current selection or, for block attributes, to the current block. Trix resolves the attribute through Trix.config.textAttributes and Trix.config.blockAttributes, which live in config/text_attributes.js and config/block_attributes.js. That means the editor's formatting vocabulary is configuration, not hardcoded behaviour, and you can overwrite Trix.config.toolbar.getDefaultHTML() to change the toolbar without patching Trix itself.

Actions are separate from attributes. The README lists the internal actions defined in controllers/editor_controller.js: undo, redo, link, increaseBlockLevel and decreaseBlockLevel. A button invokes one through data-trix-action. External actions use an x- prefix on the action name, which keeps your own handlers out of the internal namespace.

Form integration is where the architecture gets interesting. Trix wires trix-editor into forms depending on the browser's support for Element Internals. When that support is present, the editor can work without a hidden input element. The migration path is to set willCreateInput = false and render the editor without the input attribute. The README warns that if you have the input attribute set, Trix will always use the associated hidden input, so the two configurations do not mix. It also warns that without a hidden input, the editor's value is not included in form submissions unless the editor is rendered with a name attribute, set to the same value the hidden input would have had. That is a silent failure mode: the form posts, the field is simply absent.

## Installing Trix from npm or a CDN and creating a first editor

Trix ships bundled in ESM and UMD formats and works with any asset packaging system. The README calls the npm CDN route the easiest way to start, adding two tags to the head of the page. The stylesheet carries default styles for the toolbar, editor and attachments; the README says to skip it if you prefer to define those styles yourself.

```html
<head>
  <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
  <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
</head>
```

If you would rather import the package, install it from npm and import Trix in your application. The README gives this example, which listens for the trix-before-initialize event so configuration can be changed before the editor starts.

```js
import Trix from "trix"

document.addEventListener("trix-before-initialize", () => {
  // Change Trix.config if you need
})
```

Creating an editor is one empty tag. Trix inserts a toolbar before it automatically, and like a textarea the element accepts autofocus and placeholder. Unlike a textarea, it expands vertically to fit its contents, so you do not manage a fixed height.

```html
<trix-editor></trix-editor>
```

To place the toolbar elsewhere, give it an id and point the editor at it. The README's example also sets the input attribute, which is the configuration that keeps the hidden input in play.

```html
<main>
  <trix-toolbar id="my_toolbar"></trix-toolbar>
  <div class="more-stuff-inbetween"></div>
  <trix-editor toolbar="my_toolbar" input="my_input"></trix-editor>
</main>
```

A custom button is a plain button element with a data attribute. The README's bold example also binds meta+b through data-trix-key, and because bold is a text attribute rather than a block attribute, Trix applies it to the selected range.

```html
<button type="button" class="bold" data-trix-attribute="bold" data-trix-key="b"></button>
```

## The Element Internals migration is the sharpest edge

The most consequential detail in the README is not the editing model. It is the transition away from the hidden input element. Trix disables the Element Internals path with one line, setting Trix.elements.TrixEditorElement.formAssociated = false after importing Trix.

```js
import Trix from "trix"

Trix.elements.TrixEditorElement.formAssociated = false
```

That is a global switch, not a per-editor one. If you disable it, you are back to the hidden input everywhere. If you keep it, you inherit a dependency on browser support for Element Internals, and you have to reason about two rendering modes in the same codebase: editors with an input attribute, which always get a hidden input, and editors without one, which rely on a name attribute for form submission. The README's warning about missing form values is the failure mode to watch. Nothing errors. The field just does not arrive.

The other constraint is scope. The README frames Trix around messages, comments, articles and lists. There is no mention of tables, tracked changes, comments inside the document, or a plugin registry. Trix.config is the extension surface, and the README describes changing it, not building on top of a published extension API. Teams that need a document editor with a plugin marketplace will find the configuration surface narrow. Teams that need a comment box will find it is exactly the right size.

## Where Trix sits next to ProseMirror and similar model-based editors

Trix is not alone in treating contenteditable as an input device. ProseMirror takes the same position and goes further in the other direction: it exposes a schema, a transaction system and a plugin architecture as the primary interface, and expects you to build the editor UI around them. Trix inverts that. It ships a toolbar, default styles and a fixed set of text and block attributes, and lets you change them through Trix.config and the toolbar HTML function.

The practical difference is the size of the integration. With ProseMirror you assemble the editor from parts and own the toolbar, the keymap and the schema. With Trix you drop in a custom element, get a toolbar for free, and adjust configuration when the defaults do not fit. The cost of that convenience is the ceiling: the README documents no plugin system, so anything beyond the configured attributes and the documented internal actions means reading the source under src/ rather than following a published extension contract. If your editor is a feature of the product, Trix is the shorter path. If the editor is the product, the schema-first approach is the one that scales.

## Licence, release cadence and what upgrading costs you

Trix is MIT licensed and published by 37signals, LLC, with the package metadata naming 37signals as author and pointing issues at github.com/basecamp/trix. MIT is permissive and imposes no copyleft obligation on your application, but the usual caveat applies: the repository ships a LICENSE file and that file, not a summary, is the text that governs. Nothing here is legal advice.

On cadence, the repository is not archived and the last push was on 2026-09-12. The recent releases are v2.1.19 on 2026-05-09, v2.1.18 on 2026-03-26 and v2.1.17 on 2026-03-11. The gap between the last release and the last push suggests ongoing work between releases, but the release list is the only evidence available here for how changes ship.

Upgrade cost depends on which surface you touch. If you consume the distributed files and use the default toolbar, an upgrade is a version bump in your package or CDN URL. If you overwrite Trix.config.toolbar.getDefaultHTML() or depend on Trix.config.textAttributes and Trix.config.blockAttributes, you are coupled to configuration internals rather than a stable public API, and those are the places to re-read on each release. The package ships dist/*.css, dist/*.js, dist/*.map and src/{inspector,trix}/*.js, so the source is available in the published package when you need to trace behaviour. Note also that the README's CDN example pins trix@2.0.8 while the package version is 2.1.19; if you copy that snippet, you are starting three minor versions behind.

## Conclusion

Adopt Trix when your product writes messages, comments, articles and lists and you want consistent HTML without execCommand quirks; skip it if you need tables, tracked changes or a plugin ecosystem, and check first whether your target browsers support Element Internals, since the README ties form submission without a hidden input to that feature and to a name attribute on the editor element.

## FAQ

### Is Trix free to use?

Yes. The package.json lists the license as MIT, and the repository is published by 37signals, LLC. The LICENSE file in the repository is the text that governs your use.

### Is Trix a good rich text editor for React?

The README does not mention React or any framework integration. Trix is distributed as a custom element, so the documented path is to render trix-editor and trix-toolbar in your markup and import the package, which works in any framework that can render custom elements.

### How do I install Trix?

Two documented routes exist. Add the trix.css stylesheet and trix.umd.min.js script tags from the npm CDN to your page head, or install the npm package and import Trix in your application. The README calls the CDN route the easiest way to start.

### How do I add a custom button to the Trix toolbar?

Add a button element with data-trix-attribute set to an attribute name defined in Trix.config.textAttributes or Trix.config.blockAttributes. For an action that is not a formatting attribute, invoke one of the internal actions such as undo, redo, link, increaseBlockLevel or decreaseBlockLevel with data-trix-action, or prefix your own action name with x-.

### Why is my Trix editor's value missing from the form submission?

The README warns that without a hidden input element, the editor's value is not included in form submissions unless the trix-editor element is rendered with a name attribute, set to the same value the hidden input would have had. Also note that Trix always uses the associated hidden input when the input attribute is set.

## Sources

- [basecamp/trix on GitHub](https://github.com/basecamp/trix)
- [License: MIT](https://github.com/basecamp/trix/blob/main/LICENSE)
- [Project website](https://trix-editor.org/)
- [README](https://github.com/basecamp/trix/blob/main/README.md)
- [Releases](https://github.com/basecamp/trix/releases)

---

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