TipTap is a headless framework for creating rich text editors, based on ProseMirror. TipTap comes with pre-built extensions which allow you to pick and choose which text editing features you want in your editor. This makes creating custom editors fitting your needs fast and easy.

But because TipTap is headless (doesn’t have a UI), some of its extensions are missing some features that are tightly coupled to having a UI.

For example, editing alt text for images.

TL;DR: Here’s the repository with the full example from this blog post.

Starting point

Here’s our starting point: a simple editor using the image extension and the drop cursor for dragging images around. TipTap’s image extension supports alt attributes for images, but doesn’t have a way to edit them out of the box.

Note: this blog post uses an example in React, but TipTap itself is framework-agnostic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// App.jsx
import Document from '@tiptap/extension-document'
import Dropcursor from '@tiptap/extension-dropcursor'
import Image from '@tiptap/extension-image'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { EditorContent, useEditor } from '@tiptap/react'
import React from 'react'
import './App.css'

export default () => {
  const editor = useEditor({
    extensions: [
      Document,
      Paragraph,
      Text,
      Image,
      Dropcursor,
    ],
    content: `
    <p>There are two images below. One with alt text, and one without.</p>
    <img
      src="https://source.unsplash.com/8xznAGy4HcY/400x200"
      alt="Sandy hills with a foggy background"
    />
    <img src="https://source.unsplash.com/K9QHL52rE2k/400x200" />
  `,
  })

  if (!editor) { return null }

  return (
    <div className="editorWrapper">
      <h1>TipTap Image Alt Text demo</h1>
      <EditorContent editor={editor} />
    </div>
  )
}

See this code on GitHub.

TipTap editor with a paragraph and two images. The two images are black-and-white photos of a desert. The editor's UI does not show us which image has alt text.
One of those images has alt text, the other doesn't. But which one is which?

Step 1 - node view

To be able to add custom UI elements to the image, we need to use a node view. Thankfully we don’t need to rewrite the image extension provided by TipTap - we can simply extend it.

Let’s create a separate file for our custom image component.

In the node view component, we can access the attributes of the image under props.node.attrs. We’re adding a custom image class to the component to allow for some styling which will be necessary for the custom UI components that we add to the image later.

The class ProseMirror-selectednode gets added to nodes by default when the node is selected. For nodes that have a custom node view, we need to add the class explicitly.

TipTap images are draggable by default. For nodes that have a custom node view, we need to add the data attribute data-drag-handle somewhere in the component to make it draggable again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Image.jsx
import Image from '@tiptap/extension-image'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
import './Image.css'

function ImageNode(props) {
  const { src, alt } = props.node.attrs

  let className = 'image'
  if (props.selected) { className += ' ProseMirror-selectednode'}

  return (
    <NodeViewWrapper className={className} data-drag-handle>
      <img src={src} alt={alt} />
    </NodeViewWrapper>
  )
}

export default Image.extend({
  addNodeView() {
    return ReactNodeViewRenderer(ImageNode)
  }
})
1
2
3
4
5
6
7
8
9
10
11
// App.jsx
// Replace `import Image from '@tiptap/extension-image'` with:
import CustomImage from './Image'

// and pass it to useEditor:
const editor = useEditor({
  extensions: [
    // ...,
    CustomImage
  ]
})

See this code as a commit on GitHub, including some extra CSS changes.

Step 2 - alt text indicator

With a node view in place, we’re ready to add new UI components to the image node. It is entirely up to you how you want the UI of this feature to look like. I’m doing a quick demo here, so it’s not very beautiful :wink:.

The first UI component that we would like to see is some sort of indication whether an image has alt text already, and if it does, what it is.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Image.jsx
return (
  <NodeViewWrapper className={className} data-drag-handle>
    <img src={src} alt={alt} />
    <span className="alt-text-indicator">
      { alt ?
          <span className="symbol symbol-positive"></span> :
          <span className="symbol symbol-negative">!</span>
      }
      { alt ?
        <span className="text">Alt text: "{alt}".</span>:
        <span className="text">Alt text missing.</span>
      }
    </span>
  </NodeViewWrapper>
)

See this code as a commit on GitHub, including some extra CSS changes.

TipTap editor with a paragraph and two images, the same as the previous image. This time, each image has a small overlapping rectangle in the left bottom corner. On the first image, the rectangle reads: "Alt text: Sandy hills with a foggy background" and has a green checkmark. On the other image, the rectangle reads: "Alt text missing" and has a red exclamation mark.
Now we can see the alt text that was in the document from the very beginning.

Step 3 - edit button

The last step is to allow editing the alt text in the editor. Let’s add an “edit” button somewhere after the alt text indicator. When the button is clicked, we need some sort of input, prefilled with the current alt text, that will allow us to set a new alt text. In this demo, I will achieve this by using window.prompt but you probably want something nicer that will fit your application’s design.

The custom node view’s props have a updateAttributes function that allows us to update this node’s attributes, which is exactly what we need to change the image’s alt text.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Image.jsx
const { updateAttributes } = props

const onEditAlt = () => {
  const newAlt = prompt('Set alt text:', alt || '')
  updateAttributes({ alt: newAlt })
}

return (
  <NodeViewWrapper className={className} data-drag-handle>
    <img src={src} alt={alt} />
    <span className="alt-text-indicator">
      {/* ... code omitted */}
      <button className="edit" type="button" onClick={onEditAlt}>
        Edit
      </button>
    </span>
  </NodeViewWrapper>
)

See this code as a commit on GitHub, including some extra CSS changes.

TipTap editor with a paragraph and two images, the same as the previous image. This time, the two images have an "edit" button that allows changing their alt text by opening a window prompt.
The alt editing feature in action.