---
title: "Build a suggestion list"
description: "Create a sidebar component that displays all tracked change suggestions with accept and reject actions."
canonical_url: "https://tiptap.dev/docs/tracked-changes/guides/suggestion-list"
---

# Build a suggestion list

Create a sidebar component that displays all tracked change suggestions with accept and reject actions.

Learn how to build a sidebar component that lists all pending suggestions in the document, showing who made each change and allowing users to accept or reject them.

> **Framework note:**
>
> This guide uses React, but the same concepts apply to Vue and any other framework supported by
> Tiptap.

## Prerequisites

This guide assumes you have a working editor with the Tracked Changes extension. If you don't, follow the [editor setup guide](https://tiptap.dev/docs/tracked-changes/guides/editor-setup.md) first.

## Query suggestions

The extension exports a `findSuggestions` utility that returns all suggestions in the document. Call it on `onCreate` and `onUpdate` to keep your list in sync.

```jsx
import { useState } from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { TrackedChanges, findSuggestions } from '@tiptap-pro/extension-tracked-changes'

function Editor() {
  const [suggestions, setSuggestions] = useState([])

  const editor = useEditor({
    extensions: [
      StarterKit,
      TrackedChanges.configure({
        enabled: true,
        userId: 'user-123',
        userMetadata: { name: 'John Doe' },
      }),
    ],
    content: '<p>Start editing to see tracked changes.</p>',
    onCreate: ({ editor: currentEditor }) => {
      setSuggestions(findSuggestions(currentEditor))
    },
    onUpdate: ({ editor: currentEditor }) => {
      setSuggestions(findSuggestions(currentEditor))
    },
  })

  // Deduplicate — a suggestion can span multiple text nodes
  const uniqueSuggestions = Array.from(
    new Map(suggestions.map(s => [s.id, s])).values(),
  )

  if (!editor) {
    return null
  }

  return (
    <div className="editor-layout">
      <div className="editor-content">
        <EditorContent editor={editor} />
      </div>
      <SuggestionList
        suggestions={uniqueSuggestions}
        onAccept={(id) => {
          editor.commands.acceptSuggestion({ id })
          setSuggestions(findSuggestions(editor))
        }}
        onReject={(id) => {
          editor.commands.rejectSuggestion({ id })
          setSuggestions(findSuggestions(editor))
        }}
      />
    </div>
  )
}
```

`★ Insight ─────────────────────────────────────`
A single suggestion can span multiple text nodes when it crosses formatting boundaries (e.g. part of the text is bold). `findSuggestions` returns one entry per text node, so deduplication by `id` is essential to avoid showing the same suggestion multiple times in the list.
`─────────────────────────────────────────────────`

## Build the SuggestionList component

Each suggestion has a `type` (`add`, `delete`, or `replace`), the changed `text`, user metadata, and a timestamp. Use these to render a clear list.

```jsx
function SuggestionList({ suggestions, onAccept, onReject }) {
  if (suggestions.length === 0) {
    return (
      <div className="suggestion-list">
        <p>No pending suggestions</p>
      </div>
    )
  }

  return (
    <div className="suggestion-list">
      <h3>Suggestions ({suggestions.length})</h3>
      {suggestions.map(suggestion => (
        <SuggestionItem
          key={suggestion.id}
          suggestion={suggestion}
          onAccept={() => onAccept(suggestion.id)}
          onReject={() => onReject(suggestion.id)}
        />
      ))}
    </div>
  )
}
```

## Build the SuggestionItem component

Display the suggestion type, text preview, user info, and action buttons.

When a suggestion affects a block-level node (such as a paragraph or heading), the suggestion exposes `insertedNodes` or `deletedNodes` — arrays of `JSONContent` describing the full node structure. Use these to render a node type badge above the content text.

```jsx
function SuggestionItem({ suggestion, onAccept, onReject }) {
  const typeLabel = {
    add: '+ Insertion',
    delete: '− Deletion',
    replace: '~ Replacement',
  }

  // Derive a node type label from block-level node suggestions
  const nodes = suggestion.insertedNodes ?? suggestion.deletedNodes
  const nodeType = nodes?.[0]?.type

  return (
    <div className="suggestion-item">
      <div className="suggestion-item__type">
        {typeLabel[suggestion.type]}
      </div>

      {nodeType && (
        <span className="suggestion-item__node-badge">
          {nodeType}
        </span>
      )}

      <div className="suggestion-item__text">
        "{suggestion.text}"
        {suggestion.type === 'replace' && suggestion.replacedText && (
          <span className="suggestion-item__replaced-text">
            {' '}(was: "{suggestion.replacedText}")
          </span>
        )}
      </div>

      <div className="suggestion-item__meta">
        {suggestion.userMetadata?.name || suggestion.userId}
        {' · '}
        {new Date(suggestion.createdAt).toLocaleTimeString()}
      </div>

      <div className="suggestion-item__actions">
        <button onClick={onAccept}>Accept</button>
        <button onClick={onReject}>Reject</button>
      </div>
    </div>
  )
}
```

Using `suggestion.text` shows what each author contributed on their own — the right default for a review panel. When another author has stacked an edit inside this suggestion, `suggestion.fullText` differs from `suggestion.text` and holds the complete span including the nested text. You can surface that as a hint:

```jsx
{suggestion.fullText !== suggestion.text && (
  <span className="suggestion-item__stacked-hint">
    with stacked edits: "{suggestion.fullText}"
  </span>
)}
```

See [`text` vs `fullText`](https://tiptap.dev/docs/tracked-changes/api-reference/types.md#text-vs-fulltext) and [nested suggestions](https://tiptap.dev/docs/tracked-changes/usage/advanced-usage.md#nested-and-stacked-suggestions) for the full behavior.

Style the components to match your UI. Here is a minimal CSS starting point:

```css
.editor-layout {
  display: flex;
  gap: 1rem;
}

.editor-content {
  flex: 1;
}

.suggestion-list {
  width: 280px;
  padding: 1rem;
}

.suggestion-item {
  padding: 0.75rem;
  margin-bottom: 0.5rem;
  border-radius: 0.375rem;
  border: 1px solid #e2e8f0;
}

.suggestion-item__type {
  font-size: 0.75rem;
  font-weight: 600;
  margin-bottom: 0.25rem;
}

.suggestion-item__node-badge {
  display: inline-block;
  font-size: 0.65rem;
  font-weight: 600;
  padding: 0.1rem 0.4rem;
  border-radius: 0.25rem;
  background: #f1f5f9;
  color: #475569;
  margin-bottom: 0.25rem;
}

.suggestion-item__text {
  font-size: 0.875rem;
  margin-bottom: 0.25rem;
}

.suggestion-item__replaced-text {
  color: #64748b;
}

.suggestion-item__meta {
  font-size: 0.75rem;
  color: #64748b;
  margin-bottom: 0.5rem;
}

.suggestion-item__actions {
  display: flex;
  gap: 0.5rem;
}
```

## Add bulk actions

Add buttons above the list to accept or reject all suggestions at once.

```jsx
function SuggestionList({ suggestions, onAccept, onReject, onAcceptAll, onRejectAll }) {
  return (
    <div className="suggestion-list">
      <h3>Suggestions ({suggestions.length})</h3>

      {suggestions.length > 0 && (
        <div className="suggestion-list__bulk-actions">
          <button onClick={onAcceptAll}>Accept all</button>
          <button onClick={onRejectAll}>Reject all</button>
        </div>
      )}

      {suggestions.map(suggestion => (
        <SuggestionItem
          key={suggestion.id}
          suggestion={suggestion}
          onAccept={() => onAccept(suggestion.id)}
          onReject={() => onReject(suggestion.id)}
        />
      ))}
    </div>
  )
}
```

Wire up the bulk actions in the parent:

```jsx
<SuggestionList
  suggestions={uniqueSuggestions}
  onAccept={(id) => {
    editor.commands.acceptSuggestion({ id })
    setSuggestions(findSuggestions(editor))
  }}
  onReject={(id) => {
    editor.commands.rejectSuggestion({ id })
    setSuggestions(findSuggestions(editor))
  }}
  onAcceptAll={() => {
    editor.commands.acceptAllSuggestions()
    setSuggestions([])
  }}
  onRejectAll={() => {
    editor.commands.rejectAllSuggestions()
    setSuggestions([])
  }}
/>
```

## Filter suggestions

Use the options parameter on `findSuggestions` to filter by type or user:

```jsx
import { findSuggestions } from '@tiptap-pro/extension-tracked-changes'

// Only insertions
const insertions = findSuggestions(editor, 'suggestion', { type: 'add' })

// Only deletions
const deletions = findSuggestions(editor, 'suggestion', { type: 'delete' })

// Only from a specific user
const userSuggestions = findSuggestions(editor, 'suggestion', { userId: 'user-123' })
```

You can add filter controls in your sidebar to let users view specific suggestion types.

## Listen to events

For more reactive updates, listen to the extension's events instead of (or in addition to) polling on `onUpdate`:

```jsx
import { useEffect } from 'react'

useEffect(() => {
  if (!editor) return

  const updateSuggestions = () => {
    setSuggestions(findSuggestions(editor))
  }

  editor.on('trackedChanges:suggestionCreated', updateSuggestions)
  editor.on('trackedChanges:suggestionAccepted', updateSuggestions)
  editor.on('trackedChanges:suggestionRejected', updateSuggestions)
  editor.on('trackedChanges:suggestionRemoved', updateSuggestions)
  editor.on('trackedChanges:suggestionsUpdated', updateSuggestions)

  return () => {
    editor.off('trackedChanges:suggestionCreated', updateSuggestions)
    editor.off('trackedChanges:suggestionAccepted', updateSuggestions)
    editor.off('trackedChanges:suggestionRejected', updateSuggestions)
    editor.off('trackedChanges:suggestionRemoved', updateSuggestions)
    editor.off('trackedChanges:suggestionsUpdated', updateSuggestions)
  }
}, [editor])
```

## Next steps

- Add [comments integration](https://tiptap.dev/docs/tracked-changes/guides/comments-integration.md) for a full review workflow
- Explore the [utilities API](https://tiptap.dev/docs/tracked-changes/api-reference/utilities.md) for more query functions like `getSuggestionAtSelection` and `findSuggestionsInRange`
