Toolbar
A viewer’s toolbar has to fit a phone and a wide screen. <epdf-toolbar> takes a list of what should be in
it, measures what fits, and makes room when it has to: labels become icons, a group folds into one
button, and what’s left over goes into a “More” menu. You draw every button.
The items are commands, so each one already knows its label, icon, whether it’s enabled, and what it does. These are from the standard commands:
import { Component } from '@angular/core';
import { EpdfToolbar, EpdfToolbarCommandTemplate, group, item } from '@embedpdf/angular/toolbar';
import { Icon } from './icon';
@Component({
selector: 'app-main-toolbar',
imports: [EpdfToolbar, EpdfToolbarCommandTemplate, Icon],
template: `
<epdf-toolbar [bar]="bar">
<ng-template epdfToolbarCommand let-command let-variant="variant" let-run="run">
<button
(click)="run()"
[disabled]="!command.enabled"
[attr.aria-pressed]="command.active"
[title]="command.label"
>
<app-icon [name]="command.icon" />
@if (variant === 'icon+label') {
{{ command.label }}
}
</button>
</ng-template>
</epdf-toolbar>
`,
})
export class MainToolbar {
protected readonly bar = {
id: 'main',
sections: {
start: [group('navigation', ['page:previous', 'page:next'])],
center: [group('zoom', ['zoom:out', item('zoom:in', { variants: ['icon+label', 'icon'] })])],
end: [group('tools', ['tool:pointer', 'tool:pan', 'tool:highlight'], { collapse: 'menu' })],
},
};
}Loading live preview…
import { ChangeDetectionStrategy, Component, signal, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { standardCommands, withCommands } from '@embedpdf/angular/commands';
import {
EpdfToolbar,
EpdfToolbarCommandTemplate,
group,
item,
} from '@embedpdf/angular/toolbar';
import type { BarSchema } from '@embedpdf/angular/toolbar';
import { localEngine } from '@embedpdf/engine';
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// Your icons, by the names the standard commands give them.
const ICONS: Record<string, string> = {
'previous-page': '‹',
'next-page': '›',
'zoom-out': '−',
'zoom-in': '+',
'fit-width': '↔',
pointer: '↖',
pan: '✥',
highlight: '▍',
underline: 'U̲',
ink: '〰',
download: '↓',
print: '⎙',
};
const bar: BarSchema = {
id: 'main',
sections: {
start: [group('pages', ['page:previous', 'page:next'])],
center: [
group('zoom', [
'zoom:out',
item('zoom:in', { variants: ['icon+label', 'icon'] }),
item('zoom:fit-width', { variants: ['icon+label', 'icon'], importance: 2 }),
]),
],
end: [
group('tools', ['tool:pointer', 'tool:pan', 'tool:highlight', 'tool:underline', 'tool:ink'], {
collapse: 'menu',
}),
group('document', [item('document:download', { importance: 5 }), 'document:print']),
],
},
};
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
EpdfSelectionLayer,
EpdfAnnotationLayer,
EpdfToolbar,
EpdfToolbarCommandTemplate,
],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
withSelection(),
withAnnotation(),
withCommands({ commands: standardCommands }),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './basic.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<!-- Narrow the toolbar to watch it make room: labels go first, then the tools fold, then "More". -->
<label class="width">
Toolbar width
<input
#range
type="range"
min="30"
max="100"
[value]="width()"
(input)="width.set(+range.value)"
/>
<output>{{ width() }}%</output>
</label>
<div class="frame" [style.width.%]="width()">
<epdf-toolbar class="toolbar" [bar]="bar">
<ng-template epdfToolbarCommand let-command let-variant="variant" let-run="run">
<button
type="button"
class="button"
[title]="command.label"
[attr.aria-label]="command.label"
[disabled]="!command.enabled"
[attr.aria-pressed]="command.active"
(click)="run()"
>
<span class="icon" aria-hidden="true">
{{ icons[command.icon ?? ''] ?? command.label.charAt(0) }}
</span>
@if (variant === 'icon+label') {
<span>{{ command.label }}</span>
}
</button>
</ng-template>
</epdf-toolbar>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-selection-layer />
<epdf-annotation-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly bar = bar;
protected readonly icons = ICONS;
protected readonly width = signal(100);
}Resize the window, and the toolbar adjusts on its own. It measures real rendered widths, so a longer translation or a bigger font just works.
How it makes room#
When the items don’t fit, the toolbar takes these steps, in order, until they do:
- An item gets smaller: from its first
variantto the next, such as'icon+label'to'icon'. - A group folds into one button with a menu, when it has
collapse: 'menu', or into a dropdown withcollapse: 'select'. - An item moves into the “More” menu, the least important first.
importance decides the order, from 1 (goes first) to 5 (never leaves the bar):
item('document:download', { importance: 5 }); // always visible
group('annotate', ['tool:note', 'tool:ink'], { importance: 2 });Sections#
start, center and end are the toolbar’s left, middle and right. The center stays centered in the
space the other two leave. In a right-to-left language, start and end swap sides.
orientation="vertical" stands the toolbar on its side, for a rail beside the pages: start,
center and end become its top, middle and bottom, and it fits the height you give it, so give it
one. Every command draws at 'icon', so put its label in a tooltip. Groups fold and items move into
“More” the same way, and the default menus open beside it.
import { Component } from '@angular/core';
import { EpdfToolbar, EpdfToolbarCommandTemplate, group } from '@embedpdf/angular/toolbar';
import { Icon } from './icon';
@Component({
selector: 'app-tool-rail',
imports: [EpdfToolbar, EpdfToolbarCommandTemplate, Icon],
template: `
<epdf-toolbar [bar]="rail" orientation="vertical" aria-label="Tools" style="height: 100%">
<ng-template epdfToolbarCommand let-command let-run="run">
<button
(click)="run()"
[disabled]="!command.enabled"
[attr.aria-pressed]="command.active"
[title]="command.label"
>
<app-icon [name]="command.icon" />
</button>
</ng-template>
</epdf-toolbar>
`,
})
export class ToolRail {
protected readonly rail = {
id: 'rail',
sections: {
start: [group('tools', ['tool:pointer', 'tool:pan', 'tool:highlight'])],
end: [group('zoom', ['zoom:in', 'zoom:out'])],
},
};
}Your own items#
Not everything is a command: a page number input, a search box. Put a custom item in the bar and draw it:
import { Component } from '@angular/core';
import {
EpdfToolbarCustomTemplate,
EpdfToolbar,
EpdfToolbarCommandTemplate,
custom,
group,
} from '@embedpdf/angular/toolbar';
import { ToolbarButton } from './toolbar-button';
import { PageNumberInput } from './page-number-input';
@Component({
selector: 'app-page-toolbar',
imports: [EpdfToolbar, EpdfToolbarCommandTemplate, EpdfToolbarCustomTemplate, ToolbarButton, PageNumberInput],
template: `
<epdf-toolbar [bar]="bar">
<ng-template epdfToolbarCommand let-command let-variant="variant" let-run="run">
<app-toolbar-button [command]="command" [variant]="variant" (click)="run()" />
</ng-template>
<ng-template epdfToolbarCustom="page-number" let-variant>
<app-page-number-input [compact]="variant === 'compact'" />
</ng-template>
</epdf-toolbar>
`,
})
export class PageToolbar {
protected readonly bar = {
id: 'main',
sections: {
center: [group('page', [custom('page-number', 'page:go-to', { variants: ['full', 'compact'] })])],
},
};
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
input,
signal,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfCommands, standardCommands, withCommands } from '@embedpdf/angular/commands';
import {
EpdfToolbar,
EpdfToolbarCommandTemplate,
EpdfToolbarCustomTemplate,
custom,
group,
item,
} from '@embedpdf/angular/toolbar';
import type { BarSchema } from '@embedpdf/angular/toolbar';
import { localEngine } from '@embedpdf/engine';
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// The page number is an item of your own: it gets smaller first, and in the "More" menu it's the
// 'page:go-to' command.
const bar: BarSchema = {
id: 'main',
sections: {
start: [group('zoom', ['zoom:out', 'zoom:in', item('zoom:fit-width', { importance: 2 })])],
center: [
group('pages', [
'page:previous',
custom('page-number', 'page:go-to', { variants: ['full', 'compact'] }),
'page:next',
]),
],
end: [group('document', [item('document:download', { variants: ['icon+label', 'icon'] })])],
},
};
const ICONS: Record<string, string> = {
'previous-page': '‹',
'next-page': '›',
'zoom-out': '−',
'zoom-in': '+',
'fit-width': '↔',
download: '↓',
};
// The item itself: "Page 3 of 120", or "3 / 120" when there's less room.
@Component({
selector: 'label[demoPageNumber]',
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-number' },
template: `
@if (!compact()) {
Page
}
<input
#field
class="page-input"
inputmode="numeric"
aria-label="Page number"
[value]="shown()"
(focus)="field.select()"
(input)="typed.set(field.value)"
(blur)="typed.set(null)"
(keydown.enter)="go()"
/>
{{ compact() ? '/ ' + stage().pageCount() : 'of ' + stage().pageCount() }}
`,
})
export class PageNumber {
readonly stage = input.required<EpdfStage>();
readonly compact = input(false);
protected readonly typed = signal<string | null>(null);
protected readonly shown = computed(
() => this.typed() ?? String(this.stage().currentPageIndex() + 1),
);
protected go() {
const number = Number(this.typed());
if (Number.isInteger(number) && number >= 1) this.stage().goToPage(number - 1);
this.typed.set(null);
}
}
// 'page:go-to', a command of your own: it opens a small "Go to page" form.
@Component({
selector: 'demo-go-to-page',
imports: [PageNumber],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (open()) {
<div class="go-to" role="dialog" aria-label="Go to page">
<label demoPageNumber [stage]="stage()"></label>
<button type="button" class="button" (click)="open.set(false)">Done</button>
</div>
}
`,
})
export class GoToPage {
readonly stage = input.required<EpdfStage>();
protected readonly open = signal(false);
constructor() {
const remove = inject(EpdfCommands).registerCommand({
id: 'page:go-to',
label: 'Go to page…',
run: () => this.open.set(true),
});
inject(DestroyRef).onDestroy(remove);
}
}
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
EpdfToolbar,
EpdfToolbarCommandTemplate,
EpdfToolbarCustomTemplate,
PageNumber,
GoToPage,
],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withCommands({ commands: standardCommands }),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './custom-item.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<label class="width">
Toolbar width
<input
#range
type="range"
min="30"
max="100"
[value]="width()"
(input)="width.set(+range.value)"
/>
<output>{{ width() }}%</output>
</label>
<div class="frame" [style.width.%]="width()">
<epdf-toolbar [bar]="bar">
<ng-template epdfToolbarCommand let-command let-variant="variant" let-run="run">
<button
type="button"
class="button"
[title]="command.label"
[attr.aria-label]="command.label"
[disabled]="!command.enabled"
(click)="run()"
>
<span aria-hidden="true">{{ icons[command.icon ?? ''] ?? '…' }}</span>
@if (variant === 'icon+label') {
<span>{{ command.label }}</span>
}
</button>
</ng-template>
<ng-template epdfToolbarCustom="page-number" let-variant>
<label demoPageNumber [stage]="stage" [compact]="variant === 'compact'"></label>
</ng-template>
</epdf-toolbar>
</div>
<demo-go-to-page [stage]="stage" />
<epdf-stage #stage="epdfStage" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly bar = bar;
protected readonly icons = ICONS;
protected readonly width = signal(100);
}The second argument, 'page:go-to' (a command of your own that opens a “Go to page” dialog), is what
the item becomes in the “More” menu when there’s no room for it. variants lets it get smaller first, like a command’s variants.
Each epdfToolbarCustom="name" template draws the item of that name. An item with no template is a
native <slot name> element with its command as a button inside: in a web component, the host’s
children for that slot show there.
The first element in your item that takes focus is where the toolbar’s arrow keys stop. The rest of its controls are your item’s own, and a text field keeps Left and Right for its caret.
Draw every part#
| Prop | Draws |
|---|---|
<ng-template epdfToolbarCommand> | A button, for a command and its variant |
<ng-template epdfToolbarCustom> | Your own items, by name |
<ng-template epdfToolbarCollapsed> | A folded group: its button and its menu |
<ng-template epdfToolbarSeparator> | The line between groups |
<ng-template epdfToolbarOverflowTrigger> | The “More” button |
<ng-template epdfToolbarOverflowMenu> | The “More” menu, with its sections, items and the call to run each one |
Every template you leave out has a plain default, so you can start with epdfToolbarCommand and add
the rest as you style them. A template you give draws every part of its kind. gap and
separatorWidth set the spacing, and a class or style on <epdf-toolbar> styles the toolbar
itself. orientation stands it on its side (Sections). <epdf-toolbar> is the toolbar
to a screen reader, so an aria-label on it names it when the page has more than one toolbar.
Whatever you draw, the toolbar is one stop for the Tab key. The arrow keys along it move between its items, past disabled ones, and Home and End go to its ends. The default menus open on their first row: the arrow keys move, typing a letter jumps to the row that starts with it, Enter picks, and Escape closes the menu and puts focus back on its button.
A toolbar per mode#
Most viewers change the toolbar with what people do: annotation tools while annotating, field tools while building a form. Keep a bar per mode and pick one from your state:
import { Component, computed, inject } from '@angular/core';
import { EpdfInteraction } from '@embedpdf/angular/interaction';
import { EpdfToolbar, EpdfToolbarCommandTemplate } from '@embedpdf/angular/toolbar';
import { formBar, mainBar } from './bars';
import { ToolbarButton } from './toolbar-button';
@Component({
selector: 'app-mode-toolbar',
imports: [EpdfToolbar, EpdfToolbarCommandTemplate, ToolbarButton],
template: `
<epdf-toolbar [bar]="bar()">
<ng-template epdfToolbarCommand let-command let-variant="variant" let-run="run">
<app-toolbar-button [command]="command" [variant]="variant" (click)="run()" />
</ng-template>
</epdf-toolbar>
`,
})
export class ModeToolbar {
private readonly interaction = inject(EpdfInteraction);
protected readonly bar = computed(() =>
this.interaction.activeToolId() === 'form-edit' ? formBar : mainBar,
);
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { standardCommands, withCommands } from '@embedpdf/angular/commands';
import { EpdfToolbar, EpdfToolbarCommandTemplate, group } from '@embedpdf/angular/toolbar';
import type { BarSchema } from '@embedpdf/angular/toolbar';
import { localEngine } from '@embedpdf/engine';
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// A bar per mode.
const viewBar: BarSchema = {
id: 'view',
sections: {
start: [group('tools', ['tool:pointer', 'tool:pan'])],
center: [
group('pages', ['page:previous', 'page:next']),
group('zoom', ['zoom:out', 'zoom:in']),
],
},
};
const annotateBar: BarSchema = {
id: 'annotate',
sections: {
start: [group('markup', ['tool:highlight', 'tool:underline', 'tool:strikeout'])],
center: [group('draw', ['tool:ink', 'tool:square', 'tool:circle', 'tool:note'])],
end: [group('edit', ['annotation:delete'])],
},
};
type Mode = 'view' | 'annotate';
const BARS: Record<Mode, BarSchema> = { view: viewBar, annotate: annotateBar };
const TOOL_OF_MODE: Record<Mode, string> = { view: 'pointer', annotate: 'highlight' };
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
EpdfSelectionLayer,
EpdfAnnotationLayer,
EpdfToolbar,
EpdfToolbarCommandTemplate,
],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
withSelection(),
withAnnotation(),
withCommands({ commands: standardCommands }),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './modes.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="chrome">
<div class="modes" role="tablist" aria-label="Mode">
@for (each of modes; track each) {
<button
type="button"
role="tab"
class="mode"
[attr.aria-selected]="mode() === each"
(click)="switchTo(each)"
>
{{ each === 'view' ? 'View' : 'Annotate' }}
</button>
}
</div>
<epdf-toolbar class="toolbar" [bar]="bars[mode()]">
<ng-template epdfToolbarCommand let-command let-run="run">
<button
type="button"
class="button"
[disabled]="!command.enabled"
[attr.aria-pressed]="command.active"
(click)="run()"
>
{{ command.label }}
</button>
</ng-template>
</epdf-toolbar>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-selection-layer />
<epdf-annotation-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly interaction = inject(EpdfInteraction);
protected readonly modes = ['view', 'annotate'] as const;
protected readonly bars = BARS;
protected readonly mode = signal<Mode>('view');
// Each mode starts with its own tool.
protected switchTo(next: Mode) {
this.mode.set(next);
this.interaction.activateTool(TOOL_OF_MODE[next]);
}
}Good to know#
Commands keep the buttons right#
Because the items are commands, a button disables itself when its command can’t run, shows as pressed when it’s active, and hides when the command isn’t visible, with no code in the toolbar. Change a permission or open a document, and the toolbar follows.
A group with dependent: true, like undo and redo, shows only while something in the bar that isn’t
dependent shows: a bar left with only those draws nothing.
Labels in the user’s language#
A command’s label comes through translations, so the toolbar is in the user’s language and re-measures when it changes.
The default look#
When you don’t draw a part yourself, the toolbar draws a plain one. Its colors come from CSS only,
because you’re meant to replace them: --epdf-toolbar-surface (the “More” menu and folded groups,
#ffffff), --epdf-toolbar-border (rgb(0 0 0 / 0.15)) and --epdf-toolbar-active (a pressed or
open button, rgb(0 0 0 / 0.12)).
Your feedback goes directly to the documentation team.