---
title: "Contract review — Vanilla JS"
description: "A viewer for reviewing a document someone sent you, with its own app bar, a review status, Request changes and an Approve that waits for the checklist, the tools on a rail and the notes docked open."
integration: "Vanilla JS"
source: "https://www.embedpdf.com/docs/viewer/vanilla/examples/review"
---

# Contract review

For a legal or sales team reviewing a document someone sent them. The viewer gets an app bar of its
own with the document's name, the review status and the verdict; the review tools sit on a rail,
the notes stay docked on the right, and a checklist of its own opens beside the rail. **Approve**
waits for the checklist: it stays greyed out until every item is ticked. Its colors, font and words
are the review's.

**`review.html`**

```html
<link rel="stylesheet" href="./review.css" />

<embedpdf-viewer id="viewer" style="height: 100%">
  <div slot="item:title" class="review-title">
    <strong id="title"></strong>
    <span id="author"></span>
  </div>
  <span slot="item:status" id="status" class="review-status" data-status="review">In review</span>
  <div slot="panel:checklist" id="checklist" class="review-checklist">
    <p id="count" class="review-count">0 of 4 done</p>
  </div>
</embedpdf-viewer>

<script type="module">
  import 'https://cdn.jsdelivr.net/npm/@embedpdf/viewer@3/dist/embedpdf.js';

  const proposal = '/pdfs/proposal.pdf';

  const STATUS = {
    review: 'In review',
    approved: 'Approved',
    changes: 'Changes requested',
  };

  // What the reviewer checks before a verdict, in the Checklist panel.
  const CHECKS = [
    'The problem is stated clearly',
    'The phases and weeks add up',
    'Each open question has an owner',
    'Every review note has an answer',
  ];

  // What the commands read from the app: the ticked checks, the status and how to set it.
  let review = {
    done: [],
    status: 'review',
    setStatus: (status) => update({ status }),
  };

  // Each change is a new value: the viewer's buttons ask their commands again.
  function update(changes) {
    review = { ...review, ...changes };
    document.getElementById('viewer').appState = review;
    const status = document.getElementById('status');
    status.dataset.status = review.status;
    status.textContent = STATUS[review.status];
    document.getElementById('count').textContent = `${review.done.length} of ${CHECKS.length} done`;
  }

  // The checklist: ticking an item gives `done` a new value.
  const checklist = document.getElementById('checklist');
  for (const check of CHECKS) {
    const box = Object.assign(document.createElement('input'), { type: 'checkbox', value: check });
    const label = Object.assign(document.createElement('label'), { className: 'review-check' });
    label.append(box, check);
    document.getElementById('count').before(label);
  }
  checklist.addEventListener('change', () => {
    const ticked = checklist.querySelectorAll('input:checked');
    update({ done: Array.from(ticked, (box) => box.value) });
  });

  document.getElementById('viewer').config = {
    src: proposal,
    appState: review,
    // The review's look, words, tools, verdict and layout.
    theme: {
      preference: 'light',
      accent: '#4f46e5',
      tokens: {
        background: '#eef0f4',
        surface: '#ffffff',
        'surface-raised': '#ffffff',
        text: '#161a23',
        'text-muted': '#5d6474',
        border: '#dde1e8',
        hover: '#f0f2f6',
        canvas: '#363c4a',
        radius: '8px',
        font: '"Avenir Next", Avenir, "Segoe UI", system-ui, sans-serif',
      },
    },
    icons: {
      approve: [
        'M14 3v4a1 1 0 0 0 1 1h4',
        'M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z',
        'M9 15l2 2l4 -4',
      ],
      requestChanges: [
        'M14 3v4a1 1 0 0 0 1 1h4',
        'M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z',
        'M10 18l5 -5a1.414 1.414 0 0 0 -2 -2l-5 5v2h2z',
      ],
      checklist: [
        'M3.5 5.5l1.5 1.5l2.5 -2.5',
        'M3.5 11.5l1.5 1.5l2.5 -2.5',
        'M3.5 17.5l1.5 1.5l2.5 -2.5',
        'M11 6l9 0',
        'M11 12l9 0',
        'M11 18l9 0',
      ],
    },
    // The words of a review.
    strings: {
      en: {
        // The comments panel's button and its header.
        'panel:comments': 'Review notes',
        'tool:note': 'Review note',
        'annotation:comment': 'Review note',
        'tool:strikeout': 'Suggest a deletion',
        'tool:insert-text': 'Suggest an insertion',
        'tool:replace-text': 'Suggest a replacement',
      },
    },
    // What a review doesn't need.
    disable: [
      'shapes',
      'stamps',
      'signatures',
      'forms',
      'redaction',
      'measurement',
      'attachments',
      'pages',
    ],
    // Deletions in red, suggestions in the review's own color.
    plugins: {
      annotation: {
        tools: [
          { id: 'highlight', defaults: { color: '#ffcd45' } },
          { id: 'underline', defaults: { color: '#30a46c' } },
          { id: 'strikeout', defaults: { color: '#e5484d' } },
          { id: 'insert-text', defaults: { color: '#4f46e5' } },
          { id: 'replace-text', defaults: { color: '#4f46e5' } },
        ],
      },
    },
    commands: [
      {
        id: 'acme:approve',
        label: 'Approve',
        icon: 'approve',
        shortcut: 'Mod+Enter',
        // Greyed out until every check is ticked, and pressed once approved.
        enabled: ({ appState }) => appState.done.length === CHECKS.length,
        active: ({ appState }) => appState.status === 'approved',
        run: ({ appState }) => appState.setStatus('approved'),
      },
      {
        id: 'acme:request-changes',
        label: 'Request changes',
        icon: 'requestChanges',
        // The status, then the note tool, to say what should change.
        run: ({ appState, viewer }) => {
          appState.setStatus('changes');
          void viewer.commands.execute('tool:note');
        },
      },
    ],
    layout: (layout, env) =>
      layout
        // Your own bars take the place of the toolbar, and the mode bars along it go with it.
        .removeBar('main')
        // The review bar: the document and its status, then the verdict.
        .addBar('review', {
          place: 'top',
          start: [
            env.size === 'compact'
              ? [{ custom: 'status' }]
              : [{ custom: 'title' }, { custom: 'status' }],
          ],
          end: [
            { items: ['document:print', 'document:download'], importance: 2 },
            ['panel:comments'],
            {
              items: [
                // Short of room, Request changes gives up its label first: Approve keeps its own.
                {
                  command: 'acme:request-changes',
                  variants: ['icon+label', 'icon'],
                  importance: 4,
                },
                { command: 'acme:approve', emphasis: 'primary', variants: ['icon+label', 'icon'] },
              ],
              importance: 5,
            },
          ],
        })
        // The review tools on a rail, and the panels on its side at its end.
        .addBar('tools', {
          place: env.size === 'compact' ? 'bottom' : 'start',
          start: [
            ['tool:pointer', 'tool:pan'],
            ['tool:note', 'tool:highlight', 'tool:underline', 'tool:strikeout'],
            ['tool:insert-text', 'tool:replace-text'],
            ['history:undo', 'history:redo'],
          ],
          end: [['panel:checklist', 'panel:thumbnails', 'panel:search']],
        })
        // Zoom joins the page number, floating over the pages.
        .add({ custom: 'zoom' }, { to: 'pager', section: 'center' })
        .addPanel('checklist', { side: 'start', title: 'Checklist', icon: 'checklist' })
        // The right side is the review notes': search and style open on the left.
        .panel('search', { side: 'start' })
        .panel('style', { side: 'start' })
        .panels({ present: { medium: 'dock' } })
        // The review notes are open from the start, and stay open where they dock.
        .panel('comments', { open: true, closable: false, width: 340 }),
    onReady: (viewer) =>
      viewer.documents.onOpened(async ({ documentId }) => {
        // The title and the author come from the PDF itself.
        const metadata = viewer.forDocument(documentId).get('metadata');
        await metadata.ensureLoaded();
        const fields = metadata.getSnapshot();
        document.getElementById('title').textContent = fields?.title ?? 'Untitled';
        document.getElementById('author').textContent = fields?.author
          ? `From ${fields.author}`
          : '';
      }),
  };
</script>
```

**`review.css`**

```css
/* Your own parts of the viewer: they use its colors, through its CSS variables. */

/* The document's title and who sent it, at the start of the review bar */
.review-title {
  display: flex;
  flex-direction: column;
  max-width: 300px;
  line-height: 1.3;
}

.review-title strong {
  overflow: hidden;
  font-size: 14px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.review-title span {
  color: var(--epdf-text-muted);
  font-size: 12px;
}

/* The review status, which Approve and Request changes set */
.review-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-inline-start: 8px;
  padding: 4px 10px;
  border-radius: 999px;
  color: #8a5300;
  background: #fff1d6;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}

.review-status::before {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  content: '';
}

.review-status[data-status='approved'] {
  color: #12703a;
  background: #dcf5e4;
}

.review-status[data-status='changes'] {
  color: #b42318;
  background: #fde4e1;
}

/* The checklist panel */
.review-checklist {
  display: flex;
  flex-direction: column;
  padding: 8px 16px 16px;
}

.review-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--epdf-border);
  font-size: 14px;
  line-height: 1.4;
  cursor: pointer;
}

.review-check input {
  margin: 2px 0 0;
  accent-color: var(--epdf-accent);
}

.review-count {
  margin: 12px 0 0;
  color: var(--epdf-text-muted);
  font-size: 12px;
}
```

> **Try it:** read the thread on Dana Smith's note next to the title in **Review notes**. **Approve**
> is greyed out until the review is done: open the **Checklist** at the bottom of the rail and tick
> all four items. Then **Approve** turns the status next to the document's name green and stays
> pressed; **Request changes** turns it red and picks the note tool, so your next click on the page
> leaves a note.

## What it uses

- [Bars of its own](../customize/layout#several-toolbars): the review bar on top and the tools
  rail, in place of the main toolbar, which it [removes](../customize/layout#remove-buttons) with
  the mode bars along it.
- [Its own components](../customize/components) in the review bar: the document's name and its
  status.
- [Its own commands](../customize/commands), with [their own icons](../customize/commands#icons)
  and a shortcut: **Approve** and **Request changes**, with their labels while there's
  [room](../customize/layout#when-theres-no-room). **Approve** is the bar's
  [primary button](../customize/layout#a-primary-button).
- [The app's state](../customize/commands#your-apps-state): the commands read the checklist and the
  status from `appState`, so **Approve** is
  [greyed out](../customize/commands#enabled-active-and-visible) until every item is ticked and
  pressed once approved, and both commands set the status through it.
- [A panel of its own](../customize/panels#your-own-panel), the checklist, with search and style
  [moved](../customize/panels) to its side, so the right side stays the notes'.
- [How the panels open](../customize/panels#how-a-panel-opens): docked at medium widths too, and the
  notes 340 px wide, [open from the start](../customize/panels#open-from-the-start) with no close
  button where they dock.
- [A floating bar](../customize/layout#a-floating-toolbar): the zoom field beside the page number.
- [Small screens](../customize/small-screens): on a phone the rail moves to the bottom, the
  document's name leaves the bar, and the notes stay closed until you open them, instead of
  covering the page.
- [The theme's tokens](../customize/theming#every-color), with a dark canvas and its own `font`.
- [Its own words](../customize/translations#change-a-word) for the tools, and one for
  `panel:comments` that renames Comments to Review notes,
  [on its button and in its header](../customize/panels#a-panels-title).
- [Features turned off](../features/turning-off) that a review doesn't need.
- [The tools' defaults](../features/annotations#the-tools): red for deletions, the review's color
  for suggestions.
- [The viewer in code](../code/viewer#react-to-what-happens): once the document opens, the name
  comes from the [metadata plugin](../code/plugins#every-plugin).
