Layout
The layout says where every toolbar, button, menu and panel goes. Give layout a function: it gets
the default layout and returns yours, one change per line.
{
layout: (layout) => layout.place('main', 'bottom'),
}That moves the main toolbar to the bottom. Its menus now open upward, the mode tools sit above it, and nothing else changes. Every change returns a new layout, so changes chain:
{
layout: (layout) =>
layout
.place('main', 'bottom')
.remove(['tool:pan', 'panel:comments'], { from: 'main' })
.add('document:print', { to: 'main', section: 'end' }),
}The default layout#
| Bar | Where | What’s in it |
|---|---|---|
main | top | The document menu, thumbnails, view menu, zoom and tools; the mode tabs in the middle; search and comments at the end |
annotate, shapes, insert, form, redact, measure | Along main, one at a time | The tools of the mode whose tab is selected |
pager | Floating at the bottom | The page number between the previous and next page buttons; nothing for a document of one page |
selection | Over selected text | Copy, highlight, underline, strike out |
annotation | Over selected annotations | Comment, style, delete |
snapshot | Under the box the snapshot tool holds | Copy, save image |
thumbnails | In the thumbnails panel | Insert, rotate and delete pages, and undo and redo |
outline | In the outline panel | The filter; add, expand all and collapse all |
attachments | In the attachments panel | How many files there are, and attach a file |
The ids of every bar, group, menu and button are at the end of this page.
Move a toolbar#
place(bar, place) moves a bar. A place is one of these:
| Place | Where the bar goes |
|---|---|
'top', 'bottom' | Along the top or bottom edge, horizontal |
'start', 'end' | Along the left or right edge, vertical. In a right-to-left language they swap sides. |
{ float: 'bottom' } | Floating over the pages: 'top-start', 'top', 'top-end', 'start', 'end', 'bottom-start', 'bottom' or 'bottom-end'. Add hideWhenIdle to rest it out of sight while nothing moves. |
{ with: 'main' } | Along another bar, on the side of the pages. The mode bars ride along main this way, so they follow it wherever it goes. |
{ at: 'selection' }, { at: 'annotation' }, { at: 'snapshot' } | Over selected text, over selected annotations, or by the snapshot tool’s box |
{ panel: 'thumbnails' } | At the top of a panel, under its title: any panel’s id, your own too. See A bar in a panel. |
You never say which way a menu opens or where a tooltip goes. A menu opens away from its bar’s edge, toward the pages, and flips when there’s no room. A vertical bar shows icons, with the labels as tooltips.
Several toolbars#
Add a bar with addBar(id, bar). This one puts the drawing tools in a rail on the left, next to
the main toolbar at the top:
{
layout: (layout) =>
layout.addBar('tools', {
place: 'start',
start: [
['tool:pointer', 'tool:pan'],
['tool:highlight', 'tool:underline', 'tool:ink', 'tool:note'],
['history:undo', 'history:redo'],
],
}),
}Several bars can share an edge. They stack from the outside in, in the order you add them. To put a bar in a particular spot, say which bar it goes before or after:
layout.addBar('review', {
place: { edge: 'top', after: 'main' },
center: [['acme:approve', 'acme:reject']],
});A floating toolbar#
A bar can float over the pages, as the page number does by default. This one puts the zoom field, with its out and in buttons, in the bottom-right corner:
{
layout: (layout) =>
layout.addBar('zoom', {
place: { float: 'bottom-end' },
center: [[{ custom: 'zoom' }]],
}),
}A floating bar has a shadow, keeps clear of the scrollbars, and sits above the pages but below menus and panels.
It sits over the pages, so in “fit page” a page’s edge can be under it. Give the pages room on that
side with the Stage’s padding, which takes each side on its own:
{
plugins: { stage: { padding: { top: 24, right: 24, bottom: 72, left: 24 } } },
}Below 600 px wide the Stage’s own rule makes the padding 4 on every side: see
Adapt to the space you have to set your
own.
Bars floating on one edge share it: the corner ones sit in the corners, and the one in the middle stays centered over the pages, moving aside only as far as a corner bar needs. When it doesn’t fit beside them, the corner bars move one row in, toward the middle of the pages, so no bar covers another. On a phone, the zoom above and the page number sit side by side when they fit, and one over the other when they don’t.
Here the main toolbar is at the bottom, and the zoom field has left it for a floating bar of its own:
Loading live preview…
import { PDFViewer, type ViewerConfig } from '@embedpdf/viewer-react';
const welcome = '/pdfs/welcome.pdf';
const config: ViewerConfig = {
layout: (layout) =>
layout
.place('main', 'bottom')
.remove('zoom', { from: 'main' })
.addBar('zoom', {
place: { float: 'bottom-end' },
center: [[{ custom: 'zoom' }]],
}),
};
export default function App() {
return <PDFViewer src={welcome} {...config} style={{ height: '100%' }} />;
}Try it: zoom in on the timeline on page 3 from the floating buttons at the bottom right, then open the menu at the start of the toolbar: it opens upward.
Out of the way while reading#
hideWhenIdle: true fades a floating bar out once the pages and the pointer have been still for
two seconds, and brings it back when the pages move, the pointer moves over them or touches them,
or the keyboard reaches the bar, at every size:
layout.addBar('zoom', {
place: { float: 'bottom-end', hideWhenIdle: true },
center: [['zoom:out', 'zoom:in']],
});false keeps a bar on screen. Without either, the bar with the page number rests out of sight like
this, and every other bar stays:
layout.place('pager', { float: 'bottom', hideWhenIdle: false });A bar stays while the pointer is on it, while you’re using it from the keyboard or typing in it, like the page number, and while a menu from it is open; a button you clicked doesn’t keep it. Out of sight, it takes no clicks, so a click there reaches the page, and it keeps its place in the keyboard’s order.
Add a button#
add(item, { to }) puts an item in a bar or a menu. Without a position it goes at the end:
layout.add('document:print', { to: 'main' });
layout.add('document:print', { to: 'main', section: 'end' });
layout.add('document:print', { to: 'main', after: 'panel:search' });
layout.add('document:print', { to: 'document' }); // the document menu| Option | What it is | Default |
|---|---|---|
to | The bar or menu | none |
section | 'start', 'center' or 'end' | 'end' |
group | The group to join; a new id starts a new group | a new group at the end of the section |
before, after | An item to put it next to, in that item’s group | none |
An item is a command id, such as one of yours from Buttons & commands. It can also be
your own component: see Your own components. A component goes by its id in every
change, the viewer’s own components too: remove('zoom', { from: 'main' }) takes out the zoom
field, replace('zoom', { custom: 'zoom-pill' }) puts yours in its place, and
{ to: 'pager', after: 'page-number' } goes next to the page number.
Remove buttons#
remove(ids, { from }) takes items out of one bar or menu, and a group with nothing left
disappears with its line:
layout.remove(['tool:pan', 'menu:view'], { from: 'main' });
layout.remove('view:fullscreen', { from: 'document' });The command itself stays: it can sit in another bar, its shortcut works, and your code can run it.
To leave something out everywhere at once, every button, menu row, tab and shortcut, use
disable instead: the layout says where things go, disable says
whether the viewer has them.
removeBar(id) removes a whole bar, with the bars that ride along it. Removing a mode’s bar removes
its tab, while its tools stay available for a bar of your own. It takes a list too:
layout.removeBar(['measure', 'pager']);The mode bars ride along main, so removeBar('main') takes them with it: no main toolbar, and no
modes.
Replace a button#
replace(id, item) puts another item in its place, everywhere it appears:
layout.replace('document:download', 'acme:save');Build a bar#
A bar has three sections: start (left), center and end (right). The center stays centered in
the room the others leave, and in a right-to-left language start and end swap. Each section is
a list of groups, and a line separates the groups:
layout.addBar('review', {
place: 'bottom',
start: [['acme:previous-issue', 'acme:next-issue']],
center: [{ id: 'verdict', items: ['acme:approve', 'acme:reject'], collapse: 'menu' }],
end: ['document:download'],
});A group is an array of items, or an object when it needs a name or options:
| Field | What it is | Default |
|---|---|---|
id | Its name, so add(item, { group }) can find it | none |
items | The items | none |
collapse | Fold into one button with a menu ('menu') or a dropdown ('select') when there’s no room | none |
importance | Who makes room first, 1 to 5: the group, and each item without its own | 3 |
dependent | Show only while something in the bar that isn’t dependent shows, like undo and redo beside a mode’s tools | false |
A lone item in a section, like 'document:download' above, is a group of one.
When there’s no room#
A bar never wraps and never scrolls. When its items don’t fit, it makes room in this order:
- An item gets smaller: from its first variant to the next, such as icon and label to icon only.
- A group with
collapsefolds into one button. - Items move into a “More” menu at the end of the bar, the least important first.
An item that needs more than a command id is an object:
{ command: 'acme:share', variants: ['icon+label', 'icon'], importance: 5 }importance goes from 1 (makes room first) to 5 (never leaves the bar). An item without one has
its group’s, so importance on a group is for each of its items too. The bar measures what it
really draws, so a longer translation or a bigger font just works.
A primary button#
The one thing a bar is for, such as Approve or Sign, can stand out: emphasis: 'primary' draws
its button filled in the accent color.
layout.add(
{ command: 'acme:approve', emphasis: 'primary', variants: ['icon+label', 'icon'] },
{ to: 'main', section: 'end' },
);In the “More” menu it’s a row like the others. Its part is toolbar-button-primary, and each bar
is the part toolbar-<id> too, so CSS can give one bar a look of its own: see
Theming.
A new mode#
The mode tabs list every bar that has a mode. Add one, and a tab appears:
layout.addBar('review', {
place: { with: 'main' },
mode: 'review',
title: 'Review',
center: [['acme:approve', 'acme:reject', 'acme:request-changes']],
});The tab’s command is mode:review, so a button of your own can open the mode too.
Your whole layout#
Instead of a function, layout can be a layout you wrote from scratch: bars, menus and panels,
each by id.
{
layout: {
bars: {
main: {
place: 'top',
start: [[{ custom: 'zoom' }]],
end: [['panel:search', 'document:download']],
},
},
panels: {
search: { side: 'end' },
},
},
}That’s a reading room: zoom, search and download, nothing else. A built-in panel’s id brings its
own title, icon and words, so search needs no more than its side. Which one to choose:
- A function builds on the default, so a new version of the viewer brings its new buttons with it.
- A whole layout stays exactly as you wrote it. New features arrive as new ids you add when you want them.
Good to know#
Every built-in id#
Bars and their groups
| Bar | Groups |
|---|---|
main | document, view, zoom, tools in start; modes in center; panels in end |
annotate | comment, markup, draw, text, style, history |
shapes | shapes, polygons, style, history |
insert | stamps, style, history |
form | fields, choices, buttons, signatures, history |
redact | redact, style, history |
measure | measure, scale, style |
pager | pages |
selection | selection |
annotation | actions, danger |
snapshot | snapshot |
thumbnails | edit, more, history |
outline | filter, items |
attachments | count, files |
Menus: document, view, zoom, outline-item (an outline row’s), attachment-item (a
file’s, in the attachments panel), comment-item (a card’s, in the comments panel) and page (a
page’s, in the thumbnails panel). Their buttons are menu:document, menu:view, menu:zoom,
menu:outline-item, menu:attachment-item, menu:comment-item and menu:page. See Menus.
Your own components the viewer ships: { custom: 'zoom' } (the zoom field),
{ custom: 'page-number' } (the page number and count, “3 / 10”, one button that turns into a field
to type a page, and both numbers while a spread fits across the view: “2–3 / 10”; nothing for a
document of one page), { custom: 'quick-stamps' } (the stamps
bar of the Insert mode), { custom: 'measurement-scale' } (the scale of the Measure mode),
{ custom: 'view-only' } (the View only label, in the main bar’s
panels group), { custom: 'outline-filter' } (the outline’s filter field, in its panel’s bar) and
{ custom: 'attachments-count' } (how many files the document carries).
Buttons: every command, listed with what it does on Buttons & commands.
What the user may not use#
A button for something the user may not do, or that
you turned off, isn’t in the bar, and a group,
bar, mode tab or menu with nothing left disappears. The style and history groups of the mode
bars are dependent, so a mode bar left with only style, undo and redo disappears too. Your layout
stays the same: give the user the permission, and everything is back where you put it.
Ids are kept#
The ids on this page are part of the API. They don’t change between minor versions, so a layout function written today keeps working.
Checked as you go#
A layout that names an id the viewer doesn’t have, such as a typo in remove(), logs a warning with
the id and the closest one that exists. The viewer still starts.
Your feedback goes directly to the documentation team.