---
title: "Contract review — Svelte"
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: "Svelte"
source: "https://www.cloudpdf.com/docs/viewer/svelte/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.svelte`**

```svelte
<script lang="ts">
  import {
    Panel,
    PDFViewer,
    ToolbarItem,
    type Viewer,
    type ViewerConfig,
  } from '@cloudpdf/viewer-svelte';

  import './review.css';

  const proposal = { grant: 'shr_qnqHtCNRpzz-s7DPFBIbuOAF' };

  type Status = 'review' | 'approved' | 'changes';

  // What the commands read from the app: the ticked checks, the status and how to set it.
  interface Review {
    done: string[];
    status: Status;
    setStatus(status: Status): void;
  }

  const STATUS: Record<Status, string> = {
    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',
  ];

  // The review's look, words, tools, verdict and layout. The commands read the app from
  // `appState`, so they never hold an old value.
  const config: ViewerConfig<Review> = {
    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 }),
  };

  let status = $state<Status>('review');
  let title = $state('');
  let author = $state('');
  let done = $state<string[]>([]);

  function setStatus(value: Status) {
    status = value;
  }

  function onready(viewer: 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();
      title = fields?.title ?? 'Untitled';
      author = fields?.author ?? '';
    });
  }
</script>

<PDFViewer
  src={proposal}
  {...config}
  appState={{ done, status, setStatus }}
  {onready}
  style="height: 100%"
>
  <ToolbarItem id="title">
    <div class="review-title">
      <strong>{title}</strong>
      {#if author}<span>From {author}</span>{/if}
    </div>
  </ToolbarItem>
  <ToolbarItem id="status">
    <span class="review-status" data-status={status}>{STATUS[status]}</span>
  </ToolbarItem>
  <Panel id="checklist">
    <div class="review-checklist">
      {#each CHECKS as check (check)}
        <label class="review-check">
          <input type="checkbox" value={check} bind:group={done} />
          {check}
        </label>
      {/each}
      <p class="review-count">{done.length} of {CHECKS.length} done</p>
    </div>
  </Panel>
</PDFViewer>
```

**`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).
