CloudPDF
DocsPricing
Start building

Panels

Panels hold what sits next to the pages: thumbnails, the outline, search results, comments. Each one has a button, panel:<id>, and a side. Move one with panel(id, changes):

{
  layout: (layout) => layout.panel('thumbnails', { side: 'end' }),
}

The built-in panels#

PanelWhat’s in itSideOpens with
thumbnailsA small picture of every page; pick one to go there, or organize the pagesstartpanel:thumbnails
outlineThe document’s table of contents (its bookmarks): follow, filter, and with the right to change the outline, add, rename, drag and delete itemsstartpanel:outline
attachmentsThe files inside the PDF, its own and those pinned to its pages: save them, go to a page’s file, and with the right to, attach and remove filesstartpanel:attachments
searchFind text, and every match in context; a find bar where copying isn’t allowedendpanel:search, search:find (Ctrl/Cmd+F)
commentsEvery comment and its repliesendpanel:comments
styleColor, width and font of the selected annotation, or of the toolendpanel:style
stampsStamp libraries; pick one, then click the pageendpanel:stamps
signaturesSaved signatures and initials; the document’s signaturesendpanel:signatures
redactionMarked areas waiting to be removedendpanel:redaction
measurementThe scale, and every measurement on the pageendpanel:measurement
form-designThe form’s fields, and the selected one’s settings and buttons; in the Form mode onlyendform:design, panel:form-design

Each one comes with its title, its icon and what goes inside, wherever its id is: a whole layout with panels: { search: { side: 'end' } } has the search panel as it is.

A panel whose feature you turn off is gone, with its button. The outline and attachments panels show while they have a job: the document has items or files to list, or the user may add some. A read-only viewer on a document without an outline shows no outline button; the same for attachments.

A panel’s title#

A panel’s title is its button’s label too: one word for both. Rename them per language with the button’s command id in strings:

{ strings: { en: { 'panel:comments': 'Review notes' } } }

Or give the panel a title, the same in every language, or a titleKey for words of your own per language. Either one replaces the built-in words, on the button and in the header:

layout.panel('comments', { title: 'Review notes' });

A string for panel:comments comes first, over the layout’s title.

How a panel opens#

A panel opens in one of three ways:

presentWhat it does
'dock'Beside the pages, which make room for it
'overlay'Over the edge of the pages, which stay where they are and dim; a click on them closes it. The toolbars keep working.
'sheet'Up from the bottom of the viewer, over everything

By default a panel is a sheet on a phone, an overlay at medium widths, and docks when the viewer is wide: { compact: 'sheet', medium: 'overlay', expanded: 'dock' }. Give present one way for every size, or a way for the sizes you name; the other sizes keep the default:

{
  layout: (layout) =>
    layout
      // Every panel docks at medium widths too: never an overlay.
      .panels({ present: { medium: 'dock' } })
      // The thumbnails open over the pages on a phone, not as a sheet.
      .panel('thumbnails', { present: { compact: 'overlay' } })
      // The comments always open over the pages.
      .panel('comments', { present: 'overlay' }),
}

panels(changes) changes every panel the layout has so far, the way panel(id, changes) changes one. A map of sizes changes only the sizes it names, so the thumbnails above still dock at medium widths. The sizes go by the viewer’s own width: see Small screens.

A sheet is as wide as the viewer on a phone. On a wider viewer it is as wide as the panel’s width, in the middle.

Search without copying#

Where the document’s permissions let people search but not copy text, search opens as a find bar instead. The panel would list each match with the words around it, and showing those words is copying them, so there would be nothing to list. The bar sits over the top corner of the pages on the panel’s side, and across them on a phone. It shows which match you’re on and how many there are. Enter goes to the next match, Shift+Enter to the one before, and its options sit behind a button. It has the same button and the same Ctrl/Cmd+F, and keeps the same search, but present doesn’t apply to it.

Width#

A panel starts 288 px wide. People can drag its edge, docked or over the pages, between 200 and 640 px and never past half the viewer, and the viewer keeps that width while it runs. On the edge, the arrow keys move it, four times as far with Shift; Home makes the panel its narrowest and End its widest. A sheet has no edge. Set where it starts, how far it drags, or no dragging at all:

layout.panel('comments', { width: 360 }); // where it starts
layout.panel('comments', { width: { initial: 360, min: 280, max: 520 } }); // and how far it drags
layout.panel('comments', { resizable: false }); // no edge to drag

panels() takes them too: layout.panels({ resizable: false }) fixes every panel’s width.

Keep the width people chose#

onPanelResize tells you each width someone gives a panel: once a drag ends, and on each key. Keep it where you like, and start the panel at it next time:

const saved = Number(localStorage.getItem('thumbnails-width'));
 
{
  layout: (layout) => (saved ? layout.panel('thumbnails', { width: saved }) : layout),
  onPanelResize: ({ id, width }) => {
    if (id === 'thumbnails') localStorage.setItem('thumbnails-width', String(width));
  },
}

A number keeps the panel’s limits: it only says where it starts. The <embedpdf-viewer> element fires epdf:panelresize with the same { id, width } as event.detail.

The thumbnails#

The thumbnails wrap: one column at the panel’s default width, and more as it gets wider. They are a second Stage over the same document, so plugins.thumbnails takes the Stage’s settings:

{
  plugins: {
    thumbnails: {
      zoom: { pageWidth: 110 }, // each thumbnail 110 px wide; the default is 150
      columns: 1, // always one column; 'auto', the default, wraps
    },
  },
}

From code, viewer.get('thumbnails') is that Stage: viewer.get('thumbnails').updateSettings({ columns: 2 }).

Organize pages#

Where the user may change the pages, the thumbnails panel is where they do it, as in most PDF apps:

  • Select pages. A click selects a page and goes to it; Ctrl/Cmd+click adds a page or takes it out, Shift+click selects a range.
  • The panel’s bar inserts a blank page, rotates and deletes the selected pages; with room to spare it also inserts a PDF, duplicates and extracts. Undo and redo sit at its end, and in a narrow panel what doesn’t fit is in its “More” menu.
  • Each page has a menu: its button, a right-click, or a long press. It rotates, inserts a blank page or a PDF after the page, duplicates, extracts and deletes.
  • Drag pages to move them: a line shows where they land. Dragging a selected page moves the whole selection. On a phone, hold the page first, so a swipe still scrolls.
  • Drop a PDF between two pages to insert its pages there, instead of opening it as a tab.
  • The keyboard: the arrows go through the pages, Shift extends the selection, Space adds a page or takes it out, Alt+arrows move the selection one place, Delete deletes, and Ctrl/Cmd+A selects every page.

Every edit is one step of the history, so undo puts the pages back as they were, a deleted page with its annotations. The buttons are the page:* commands, so disable and readOnly turn editing off and leave a plain list of pages. A user without doc.pages.assemble in their scope sees the plain list too.

The attachments#

The attachments panel lists the files the PDF carries: its own first, then the files pinned to its pages, each with its name, size and description, and its page for a page’s file. A name never shows the characters that can make one file look like another.

  • Click a file to save it; click a page’s file to go to it on its page.
  • Each file has a menu: its button or a right-click. It saves the file, goes to a page’s file, and deletes the file.
  • Attach files with the button in the panel’s bar, beside how many files there are, or drop them on the panel; a file dropped there is attached, not opened as a tab. A name the document has already gets a number, notes (2).txt.
  • A selected file attachment on the page has Save file in its bar.

Saving a file is a download: it goes with documents.download and needs doc.download in the user’s scope. Attaching and deleting are attachments.adding, which readOnly turns off, and need doc.attachments.modify; each is one step of the history. From code, viewer.get('attachment') is the attachment plugin.

A bar in a panel#

The thumbnails, outline and attachments panels each have a bar under their title, named after the panel: thumbnails, outline and attachments. The outline’s holds its filter. They’re bars like the toolbar, so the same edits work, and what doesn’t fit goes into a “More” menu:

layout
  .add('acme:print-pages', { to: 'thumbnails' })
  .remove(['outline:expand-all', 'outline:collapse-all'], { from: 'outline' });

Any panel can have a bar, your own too: its place is { panel: id }.

layout.addBar('review', {
  place: { panel: 'comments' },
  start: [['acme:approve-all', 'acme:export']],
});

A panel’s bars stack under its title in the order you add them. The search panel shows its bars over its list of hits, not in the find bar it becomes when the hits come without their text.

Panels on the same side#

One panel shows per side at a time. Opening another on that side closes the first, and the panel’s header switches between the panels that share its side. Both sides can be open at once: the thumbnails on the left and the comments on the right.

Open from the start#

open: true opens a panel when a document opens, where it docks. Where it would cover the pages, as a sheet on a phone or over them at medium widths, it waits for its button:

layout.panel('comments', { open: true });

A panel people shouldn’t close gets closable: false. Where it docks, it’s always open: it has no close button, and its button opens it but doesn’t close it. Another panel on its side can take its place, and when that one closes, it’s back. Over the pages it closes like any other panel, since it would cover them.

layout.panel('comments', { closable: false });

Of two panels on one side that open from the start, the first in the layout opens.

Remove a panel#

layout.removePanel('attachments');

Its button goes with it, from every bar and menu, and so do the bars at its top.

Open a panel from code#

Run its command:

viewer.commands.execute('panel:comments');

To open one with the cursor in it, ask the shell for focus:

viewer.get('shell').open('search', { focus: true });

Your own panel#

Add it with addPanel(id, panel) and give the viewer what goes inside:

layout.addPanel('notes', { side: 'end', title: 'Notes', icon: 'book' });
FieldWhat it isDefault
side'start' or 'end''end'
titleIts title, in its header and on its button; or titleKey for a translated onenone
iconIts button’s iconnone
present'dock', 'overlay' or 'sheet', or one per size: { compact, medium, expanded }{ compact: 'sheet', medium: 'overlay', expanded: 'dock' }
widthWhere it starts, in px, or { initial, min, max }{ initial: 288, min: 200, max: 640 }
resizableWhether people can drag its edgetrue
openWhether it opens when a document opens, where it docksfalse
closableWhether people can close it where it dockstrue

The viewer draws the frame, the header and the way it opens; you draw what’s inside. See Your own components for the content in each framework.

This one holds a box for notes. In the frame below the viewer is medium width, so the panel opens over the pages, and its edge drags there too:

Loading live preview…

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

  const welcome = { grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };

  const config: ViewerConfig = {
    layout: (layout) =>
      layout
        .addPanel('notes', {
          side: 'end',
          title: 'Notes',
          icon: 'book',
          width: { initial: 320, min: 240, max: 480 },
        })
        .add('panel:notes', { to: 'main', section: 'end' }),
  };
</script>

<PDFViewer src={welcome} {...config} style="height: 100%">
  <Panel id="notes">
    <textarea class="notes" placeholder="Your notes"></textarea>
  </Panel>
</PDFViewer>

Try it: open Notes, the book at the end of the toolbar, and write a line about the cover. Then drag the panel’s left edge to make it wider or narrower, or focus the edge and use the arrow keys.

Good to know#

Each document keeps its panels#

With several documents open, each one remembers which panels were open, so switching tabs brings them back. Widths are the viewer’s: a panel keeps its width from one document to the next.

Focus#

An overlay or a sheet takes focus when it opens and gives it back to its button when it closes; Escape closes it. A sheet keeps Tab inside it; from an overlay, Tab moves on to the toolbars, which keep working while it’s open. A docked panel takes focus only when it’s asked to: it’s part of the page, and Tab reaches it in order.

Search asks, from its button and from Ctrl/Cmd+F (search:find): the cursor goes in its field, with its text selected, so typing replaces it. Ctrl/Cmd+F works from any text field in the viewer, the search field among them, and keeps search open. Your code asks with viewer.get('shell').open(id, { focus: true }); a plain open(id) leaves the cursor where it is.