---
title: "Panels — Vue"
description: "Move the built-in panels, choose how they open at each size, set their width, and add your own."
integration: "Vue"
source: "https://www.cloudpdf.com/docs/viewer/vue/customize/panels"
---

# 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)`:

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

## The built-in panels

| Panel         | What's in it                                                                                                                                    | Side    | Opens with                                       |
| :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------- | :------ | :----------------------------------------------- |
| `thumbnails`  | A small picture of every page; pick one to go there, or [organize the pages](#organize-pages)                                                   | `start` | `panel:thumbnails`                               |
| `outline`     | The document's table of contents (its bookmarks): follow, filter, and with the right to change the outline, add, rename, drag and delete items  | `start` | `panel:outline`                                  |
| `attachments` | The 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 files | `start` | `panel:attachments`                              |
| `search`      | Find text, and every match in context; [a find bar](#search-without-copying) where copying isn't allowed                                        | `end`   | `panel:search`, `search:find` (`Ctrl`/`Cmd`+`F`) |
| `comments`    | Every comment and its replies                                                                                                                   | `end`   | `panel:comments`                                 |
| `style`       | Color, width and font of the selected annotation, or of the tool                                                                                | `end`   | `panel:style`                                    |
| `stamps`      | Stamp libraries; pick one, then click the page                                                                                                  | `end`   | `panel:stamps`                                   |
| `signatures`  | Saved signatures and initials; the document's signatures                                                                                        | `end`   | `panel:signatures`                               |
| `redaction`   | Marked areas waiting to be removed                                                                                                              | `end`   | `panel:redaction`                                |
| `measurement` | The scale, and every measurement on the page                                                                                                    | `end`   | `panel:measurement`                              |
| `form-design` | The form's fields, and the selected one's settings and buttons; in the Form mode only                                                           | `end`   | `form:design`, `panel:form-design`               |

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

A panel whose feature you [turn off](../features/turning-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`](translations#change-a-word):

```js
{ 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:

```js
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:

| `present`   | What 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:

```js
{
  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](small-screens#the-three-sizes).

A sheet is as wide as the viewer on a phone. On a wider viewer it is as wide as the panel's
[`width`](#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:

```js
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:

```js
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](https://www.cloudpdf.com/docs/headless/vue/viewing/stage) over the same document, so `plugins.thumbnails`
takes the Stage's settings:

```js
{
  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](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](https://www.cloudpdf.com/docs/headless/vue/documents/attachments).

## 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`](layout#every-built-in-id). 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:

```js
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 }`.

```js
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:

```js
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.

```js
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

```js
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:

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

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

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

## Your own panel

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

```js
layout.addPanel('notes', { side: 'end', title: 'Notes', icon: 'book' });
```

| Field       | What it is                                                                                     | Default                                                     |
| :---------- | :--------------------------------------------------------------------------------------------- | :---------------------------------------------------------- |
| `side`      | `'start'` or `'end'`                                                                           | `'end'`                                                     |
| `title`     | Its title, in its header and on its button; or `titleKey` for a [translated](translations) one | none                                                        |
| `icon`      | Its button's icon                                                                              | none                                                        |
| `present`   | `'dock'`, `'overlay'` or `'sheet'`, or one per size: `{ compact, medium, expanded }`           | `{ compact: 'sheet', medium: 'overlay', expanded: 'dock' }` |
| `width`     | Where it starts, in px, or `{ initial, min, max }`                                             | `{ initial: 288, min: 200, max: 640 }`                      |
| `resizable` | Whether people can drag its edge                                                               | `true`                                                      |
| `open`      | Whether it opens when a document opens, where it docks                                         | `false`                                                     |
| `closable`  | Whether people can close it where it docks                                                     | `true`                                                      |

The viewer draws the frame, the header and the way it opens; you draw what's inside. See [Your own
components](components#a-panel-of-your-own) 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:

**`notes.vue`**

```vue
<script setup lang="ts">
import { PDFViewer, Panel, type ViewerConfig } from '@cloudpdf/viewer-vue';
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>

<template>
  <PDFViewer :src="welcome" v-bind="config" style="height: 100%">
    <Panel id="notes">
      <textarea class="notes" placeholder="Your notes"></textarea>
    </Panel>
  </PDFViewer>
</template>
```

**`notes.css`**

```css
/* What goes inside the panel: a box for notes that fills it. The viewer's colors come in as
   CSS variables. */
.notes {
  flex: 1;
  box-sizing: border-box;
  margin: 12px;
  padding: 10px 12px;
  font: inherit;
  font-size: 14px;
  line-height: 1.5;
  color: inherit;
  background: var(--epdf-surface);
  border: 1px solid var(--epdf-border);
  border-radius: 8px;
  resize: none;
}

.notes:focus-visible {
  outline: 2px solid var(--epdf-accent);
  outline-offset: -1px;
}
```

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