Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ const nextConfig = {
destination: '/editor/getting-started/install',
permanent: true,
},
{
source: '/editor/getting-started/install/cdn',
destination: '/editor/getting-started/install/vanilla-javascript',
permanent: true,
},
{
source: '/editor/markdown/getting-started',
destination: '/editor/markdown/getting-started/installation',
Expand Down
167 changes: 167 additions & 0 deletions src/content/editor/core-concepts/decorations.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
title: Decorations
description: How to use Tiptap's Decorations API - implementing decorations in extensions, decoration types, and performance tips.
meta:
title: Decorations | Tiptap Editor Docs
description: Learn how to implement and optimize decorations in Tiptap extensions.
category: Editor
---

## TL;DR

Decorations let you draw styling or UI on top of the document without changing the document content. Add a `decorations()` factory to an extension and return a small array of simple decoration items. Use `shouldUpdate` to avoid unnecessary recalculation.

## What is the Decoration API?

The Decoration API is a small, extension-facing surface that lets extensions describe visual decorations (highlights, badges, widgets) without mutating the document. Tiptap turns those descriptions into ProseMirror decorations and renders them in the editor view.

## Decoration types

- inline – styling applied to a range of text (e.g., highlights).
- node – attributes applied to an entire node (e.g., add a class to a paragraph).
- widget – a DOM node rendered at a specific position (e.g., an inline badge or button). Widgets are non-editable.

## How to add decorations to an extension

Add a `decorations()` factory to your extension that returns an object with `create({ state, view, editor })` and an optional `shouldUpdate`.

Inline example (using the helper):

```js
import { Extension, createInlineDecoration } from '@tiptap/core'

Comment on lines +31 to +32
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports reference functions that don't exist in the codebase. The helper functions createInlineDecoration, createNodeDecoration, and createWidgetDecoration are documented throughout this file but are not found in the actual Tiptap packages. Either these functions need to be implemented, or the documentation should be updated to show the actual API for creating decorations.

Copilot uses AI. Check for mistakes.
export const MakeWordRed = Extension.create({
name: 'makeWordRed',

decorations: () => ({
create({ state }) {
// return an array of decoration items
return [createInlineDecoration(5, 11, { style: 'color: red' })]
},
shouldUpdate: ({ tr }) => tr.docChanged,
}),
})
```

Node example (using the helper):

```js
import { Extension, createNodeDecoration } from '@tiptap/core'

export const HighlightParagraph = Extension.create({
name: 'highlightParagraph',

decorations: () => ({
create({ state }) {
// highlight the first paragraph
const pos = 0
const end = 50
return [createNodeDecoration(pos, end, { class: 'my-paragraph' })]
},
shouldUpdate: ({ tr }) => tr.docChanged,
}),
})
```

Widget example (using the helper):

```js
import { Extension, createWidgetDecoration } from '@tiptap/core'

export const StarAfter = Extension.create({
name: 'starAfter',

decorations: () => ({
create() {
return [
createWidgetDecoration(10, () => {
const el = document.createElement('span')
el.textContent = ' ⭐'
el.setAttribute('contenteditable', 'false')
return el
}),
]
},
shouldUpdate: ({ tr }) => tr.docChanged,
}),
})
```

## Utility functions

- `createInlineDecoration(from, to, attributes)` - returns a small object describing an inline decoration.
- `createNodeDecoration(from, to, attributes)` - returns a node decoration object.
- `createWidgetDecoration(at, widget)` - returns a widget item where `widget` is a function that creates a DOM node.

Use these helpers for concise examples; under the hood Tiptap maps these items to ProseMirror decorations.

## Create a decoration without helpers

You can return the plain decoration items directly if you prefer to avoid helpers. The shape is intentionally small and simple:

```js
// manual inline decoration
return [{ type: 'inline', from: 5, to: 11, attributes: { style: 'background: yellow' } }]

// manual widget
return [
{
type: 'widget',
from: 20,
to: 20,
widget: () => {
const el = document.createElement('button')
el.textContent = 'Click'
el.setAttribute('contenteditable', 'false')
return el
},
},
]
```

Notes:

- `from`/`to` are document positions.
- For widgets, return a DOM node from the `widget` function. If you mount React components, return a container element and use the React widget helper from the React package.

## Best practices & performance

- Use `shouldUpdate` to limit recalculation. A common simple implementation is `({ tr }) => tr.docChanged`.
- Keep `spec` data small (strings/numbers) if you add it to items - it's used to decide whether a decoration meaning changed.
- Avoid scanning the whole document every transaction. Narrow traversal to nodes of interest or cache results per node when possible.
- For widgets, provide a stable `spec.key` (or ensure your widget markup is stable) to avoid unnecessary remounts.

More about `spec` and stability

- The `spec` object on a decoration is used to decide whether a decoration's meaning changed between renders. Keep it tiny and primitive (strings, numbers, booleans). Avoid functions, DOM nodes, or large objects inside `spec`.
- For widgets, include a stable identifier in `spec` (for example `spec.key: 'comment-123'`) so the renderer can reuse the same DOM node across updates and avoid remounting React components.
- Do NOT rely only on document positions for stability. Positions move when the doc changes. Prefer a stable id from the node (for example an `id` in `node.attrs`) or a key you manage on the extension side.

Short widget example with a stable key:

```js
return [
{
type: 'widget',
from: pos,
to: pos,
widget: () => {
const el = document.createElement('span')
el.textContent = '⭐'
el.setAttribute('contenteditable', 'false')
return el
},
spec: { key: 'my-widget-42' },
},
]
```

- If you need to compute keys from node content, compute a stable id once and store it on the node (attrs/marks) or in a side map. Recomputing ephemeral keys on every `create` will cause remounts.
- Combine sensible `shouldUpdate` logic with stable `spec` values: `shouldUpdate` decides when to recreate the list, `spec` decides whether individual decorations should be updated/reused.

## Troubleshooting

- Widgets flicker or lose internal state: give widgets stable keys (via `spec.key`) or avoid remounting DOM nodes.
- Widget callbacks see wrong positions after edits: call the provided `getPos()` in widget callbacks (or use the editor APIs) rather than relying on an earlier captured `pos`.
- Decorations don't update: check `shouldUpdate` and ensure it returns `true` for transactions that should trigger an update.

40 changes: 0 additions & 40 deletions src/content/editor/getting-started/install/cdn.mdx

This file was deleted.

46 changes: 40 additions & 6 deletions src/content/editor/getting-started/install/vanilla-javascript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,39 @@ category: Editor

import { Callout } from '@/components/ui/Callout'

Are you using plain JavaScript or a framework that isn't listed? No worries, we provide everything you need.
Are you building without a frontend framework like React or Vue? No problem, you can use Tiptap directly in plain JavaScript.

<Callout title="Hint" variant="hint">
If you don't use a bundler like Webpack or Rollup, please follow the [CDN](/editor/getting-started/install/cdn) guide instead. Since Tiptap is built in a modular way, you will need to use `<script type="module">` in your HTML to get our CDN imports working.

"Vanilla JavaScript" here means **no frontend framework**, but still using **modern JavaScript with ES module imports** (e.g. through Vite, Rollup, or Webpack).

If you’re not using a build tool, check out the [CDN example below](#using-a-cdn-no-build-step) for an example that runs directly in the browser.

</Callout>

## Install dependencies

For the following example, you will need `@tiptap/core` (the actual editor), `@tiptap/pm` (the ProseMirror library), and `@tiptap/starter-kit`. The StarterKit doesn't include all extensions, only the most common ones.
You’ll need the core Tiptap packages to get started:

- `@tiptap/core` – the main editor API
- `@tiptap/pm` – ProseMirror, the engine behind Tiptap
- `@tiptap/starter-kit` – a convenient bundle of common extensions

```bash
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit
```

### Add markup

Add the following HTML where you'd like to mount the editor:
Add a simple container in your HTML where you want the editor to appear:

```html
<div class="element"></div>
```

## Initialize the editor

Everything is in place, so let's set up the editor. Add the following code to your JavaScript:
Create a JavaScript file (for example `src/main.js`) and add the following code:

```js
import { Editor } from '@tiptap/core'
Expand All @@ -45,7 +53,33 @@ new Editor({
})
```

Open your project in the browser to see Tiptap in action. Good work!
Now run your dev server (for example with Vite: `npm run dev`) and open the page in your browser. You should see a working Tiptap editor.

## A quick note about styling

Tiptap doesn’t include visual styles by default. The editor only outputs semantic HTML. You can style it however you like with your own CSS or a framework such as Tailwind or Bootstrap.

If you’re using the StarterKit, it includes minimal defaults that make the text render like a basic document.
Learn more in the [Styling guide](/editor/getting-started/style-editor).

## Using a CDN (no build step)

If you’d rather skip the build tool, here’s a working example using CDN imports:

```html
<script type="module">
import { Editor } from 'https://esm.sh/@tiptap/core'
import StarterKit from 'https://esm.sh/@tiptap/starter-kit'

new Editor({
element: document.querySelector('.element'),
extensions: [StarterKit],
content: '<p>Hello from CDN!</p>',
})
</script>

<div class="element"></div>
```

## Next steps

Expand Down
4 changes: 4 additions & 0 deletions src/content/editor/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,10 @@ export const sidebarConfig: SidebarConfig = {
href: '/editor/core-concepts/schema',
title: 'Schema',
},
{
href: '/editor/core-concepts/decorations',
title: 'Decorations',
},
{
href: '/editor/core-concepts/keyboard-shortcuts',
title: 'Keyboard shortcuts',
Expand Down