Colors & theming
Every color EmbedPDF draws on the screen, such as the text selection, search highlights and the handles of a selected annotation, is a setting with a sensible default. Give the viewer your accent, and every part follows it:
import { Component } from '@angular/core';
import { withRender } from '@embedpdf/angular/render';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { withStage } from '@embedpdf/angular/stage';
import { Pages } from './pages';
import { engine } from './pdf';
@Component({
selector: 'app-document-viewer',
imports: [Pages],
providers: [
provideEmbedPdf({ engine, accent: '#e91e63', page: { shadow: 'none' } }, withStage(), withRender()),
],
template: `<app-pages />`,
})
export class DocumentViewer {}Then fine-tune any part with its plugin’s settings, such as the handles of an annotation:
withAnnotation({
chrome: {
outline: { style: 'dashed' },
handles: { shape: 'circle', fill: '#fce4ec' },
rotationHandle: { stalk: false }, // no line between the box and the handle
},
});Every setting is typed, has its default, and is listed with its plugin under Settings. All of them together are under Every color.
Override from CSS#
Every color setting can also come from a CSS variable, and a CSS variable always wins over a setting. Use it for dark mode, for a different look per viewer, or to keep colors in your stylesheet:
.pdf-viewer {
--epdf-accent: #e91e63;
--epdf-search-highlight: #7dd3fc;
}Put the class on an element around your viewer; nothing needs importing. The rule is short: any CSS
variable wins over any setting. A part looks for its own variable first, then the variable of the
part it follows: the rotation handle uses --epdf-annotation-handle-fill unless you give it its
own, and a part that follows the accent uses --epdf-annotation-accent, then --epdf-accent. Only
when none is set does its setting apply, then the setting it follows, then the built-in color.
Only paint comes from CSS: colors, line widths, dashes, shadows, opacity and blend modes. How big a handle is, where the rotation handle sits and how close snapping starts are settings only, because they decide where a click lands.
Dark mode#
Set the variables inside a media query, or under a class your app already switches:
@media (prefers-color-scheme: dark) {
.pdf-viewer {
--epdf-accent: #8ab4ff;
--epdf-page-background: #2b2b2b;
--epdf-readout-background: #f5f5f5;
--epdf-readout-color: #111111;
}
}
[data-theme='dark'] .pdf-viewer {
--epdf-accent: #8ab4ff;
}No code runs and nothing re-renders: the browser repaints.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
signal,
untracked,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { EpdfSelection, EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfSearch, EpdfSearchLayer, withSearch } from '@embedpdf/angular/search';
import { localEngine } from '@embedpdf/engine';
const TITLE = { start: 10, count: 52 };
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()) };
};
// On load: the cover's title is selected and "PDF" is found, so every color below has something to paint.
@Component({
selector: 'demo-show-colors',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '',
})
export class ShowColors {
constructor() {
const selection = inject(EpdfSelection);
const document = inject(EpdfDocument);
void inject(EpdfSearch).search({ text: 'PDF' });
effect(() => {
const cover = document.pages()[0]?.ref;
if (cover) untracked(() => selection.select({ page: cover, ...TITLE }));
});
}
}
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
EpdfSearchLayer,
EpdfSelectionLayer,
ShowColors,
],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
withSelection(),
withSearch(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './dark-mode.css',
encapsulation: ViewEncapsulation.None,
template: `
<!-- Your app's own switch: the colors are in the stylesheet, under [data-theme='dark']. -->
<div class="pdf-viewer" [attr.data-theme]="theme()">
<div class="toolbar">
<div class="segmented" role="group" aria-label="Theme">
<button type="button" [attr.aria-pressed]="theme() === 'light'" (click)="theme.set('light')">
Light
</button>
<button type="button" [attr.aria-pressed]="theme() === 'dark'" (click)="theme.set('dark')">
Dark
</button>
</div>
</div>
<ng-container *epdfDocumentGate="let document; fallback: loading">
<demo-show-colors />
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-search-layer />
<epdf-selection-layer />
</ng-template>
</epdf-stage>
</ng-container>
</div>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly theme = signal<'light' | 'dark'>('dark');
}Change colors while the app runs#
Every setting can change while the app runs. A plugin’s settings change with updateSettings():
annotation.updateSettings({ chrome: { accent: '#0f6e56' } });The viewer’s accent and page change with inject(EpdfViewer).updateSettings({ accent }), since
the settings you give provideEmbedPdf() are read once.
For every part at once, such as an accent the user picks, set the variables on your wrapper.
epdfTheme() turns setting names into the variables, with autocomplete:
import { Component, computed, signal } from '@angular/core';
import { epdfTheme } from '@embedpdf/angular/runtime';
import { Pages } from './pages';
@Component({
selector: 'app-branded-viewer',
imports: [Pages],
template: `
<div class="pdf-viewer" [style]="theme()">
<input #picker type="color" [value]="accent()" (input)="accent.set(picker.value)" />
<app-pages />
</div>
`,
})
export class BrandedViewer {
protected readonly accent = signal('#e91e63');
protected readonly theme = computed(() => epdfTheme({ accent: this.accent() }));
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
signal,
untracked,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocument,
EpdfDocumentGate,
epdfTheme,
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 { EpdfSelection, EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { localEngine } from '@embedpdf/engine';
// The characters of the cover's title.
const TITLE = { start: 10, count: 52 };
const SWATCHES = ['#3858e9', '#e91e63', '#0f6e56', '#c2410c'];
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 cover's title is selected on load, so the accent shows; drag over any text to see more.
@Component({
selector: 'demo-select-title',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '',
})
export class SelectTitle {
constructor() {
const selection = inject(EpdfSelection);
const document = inject(EpdfDocument);
effect(() => {
const cover = document.pages()[0]?.ref;
if (cover) untracked(() => selection.select({ page: cover, ...TITLE }));
});
}
}
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
EpdfSelectionLayer,
SelectTitle,
],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
withSelection(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './accent.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="pdf-viewer" [style]="theme()">
<div class="toolbar">
<label class="picker">
Accent
<input #picker type="color" [value]="accent()" (input)="accent.set(picker.value)" />
</label>
@for (swatch of swatches; track swatch) {
<button
type="button"
class="swatch"
[attr.aria-label]="'Accent ' + swatch"
[attr.aria-pressed]="swatch === accent()"
[style.background]="swatch"
(click)="accent.set(swatch)"
></button>
}
<code class="value">--epdf-accent: {{ accent() }}</code>
</div>
<ng-container *epdfDocumentGate="let document; fallback: loading">
<demo-select-title />
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-selection-layer />
</ng-template>
</epdf-stage>
</ng-container>
</div>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly swatches = SWATCHES;
protected readonly accent = signal('#e91e63');
// Every part inside follows the variables; a variable set in your stylesheet still wins.
protected readonly theme = computed(() => epdfTheme({ accent: this.accent() }));
}Different looks for different viewers#
Each viewer has its own settings. And a CSS variable applies to everything inside the element you set it on, so two viewers on one page can each have their own colors from one stylesheet:
.contract-viewer {
--epdf-accent: #0f6e56;
}
.review-viewer {
--epdf-accent: #993c1d;
}Every color#
The viewer
| Setting | What it colors | Default | From CSS |
|---|---|---|---|
accent | Every part below that has no color of its own | '#3858e9' | --epdf-accent |
page.background | A page before its picture arrives | '#ffffff' | --epdf-page-background |
page.shadow | The shadow under each page; 'none' for none | '0 6px 18px rgb(0 0 0 / 0.18)' | --epdf-page-shadow |
Annotations: withAnnotation({ chrome })
| Setting | What it colors | Default | From CSS |
|---|---|---|---|
chrome.accent | Every annotation part below that has no color of its own | the viewer’s accent | --epdf-annotation-accent |
chrome.outline.color | The box around the selection | chrome.accent | --epdf-annotation-outline |
chrome.outline.width | Its line width | 1 | --epdf-annotation-outline-width |
chrome.outline.style | 'solid' or 'dashed' | 'solid' | --epdf-annotation-outline-dash (4 3) |
chrome.handles.fill | Inside the resize and point handles | '#ffffff' | --epdf-annotation-handle-fill |
chrome.handles.stroke | Their edge | chrome.accent | --epdf-annotation-handle-stroke |
chrome.rotationHandle.fill | Inside the rotation handle | chrome.handles.fill | --epdf-annotation-rotation-handle-fill |
chrome.rotationHandle.stroke | Its edge and its stalk | chrome.handles.stroke | --epdf-annotation-rotation-handle-stroke |
chrome.guides.color | The lines that show what an annotation snaps to while you move it | '#e91e63' | --epdf-annotation-guide |
chrome.guides.rotationColor | The reference cross and the angle line while you turn it | chrome.accent | --epdf-annotation-rotation-guide |
chrome.guides.width | The width of both kinds of line | 1.5 | --epdf-annotation-guide-width |
chrome.guides.style | 'solid' or 'dashed' | 'solid' | --epdf-annotation-guide-dash (4 3) |
chrome.marquee.fill | Inside the box you drag to select several annotations | chrome.accent at 8% | --epdf-annotation-marquee-fill |
chrome.marquee.stroke | Its edge | chrome.accent | --epdf-annotation-marquee-stroke |
chrome.textOutline | A text box while you type in it | chrome.accent | --epdf-annotation-text-outline |
chrome.readout.background | Behind the angle while you turn an annotation | 'rgb(0 0 0 / 0.8)' | --epdf-readout-background |
chrome.readout.color | The angle’s text | '#ffffff' | --epdf-readout-color |
A tool’s ghost.opacity | The preview under the pointer | 0.5 | --epdf-ghost-opacity |
Text selection: withSelection({ color, handles })
| Setting | What it colors | Default | From CSS |
|---|---|---|---|
color | Selected text | the viewer’s accent at 35% | --epdf-text-selection |
handles.color | The grips at its ends, on touch screens | the viewer’s accent | --epdf-text-selection-handle |
handles.shadow | Their shadow | '0 1px 4px rgb(0 0 0 / 0.35)' | --epdf-text-selection-handle-shadow |
Search: withSearch({ highlight })
| Setting | What it colors | Default | From CSS |
|---|---|---|---|
highlight.color | Every match | '#ffd500' | --epdf-search-highlight |
highlight.activeColor | The active match | '#ff9632' | --epdf-search-highlight-active |
highlight.blendMode | How the highlights mix with the page | 'multiply' | --epdf-search-blend-mode |
Forms: withForm({ focus, fields })
| Setting | What it colors | Default | From CSS |
|---|---|---|---|
focus.color | The ring around the field being filled in | the viewer’s accent | --epdf-form-focus |
fields.border | The edge of a field that has no border of its own | the viewer’s accent at 55% | --epdf-form-field-border |
fields.background | Inside a field that has no background of its own | 'rgb(255 255 255 / 0.92)' | --epdf-form-field-background |
fields.color | The check mark and text of a field that has no color of its own | '#1f2a44' | --epdf-form-field-color |
Parts you style yourself have no settings, only CSS, for the look they have until you style them:
| From CSS | What it colors | Default |
|---|---|---|
--epdf-scrollbar-thumb | The scrollbar’s thumb | rgb(0 0 0 / 0.4) |
--epdf-toolbar-surface | The toolbar’s “More” menu and folded groups | #ffffff |
--epdf-toolbar-border | Their edge, and the edge of a button | rgb(0 0 0 / 0.15) |
--epdf-toolbar-active | A pressed or open button | rgb(0 0 0 / 0.12) |
--epdf-font-mono | Numbers in readouts, such as the angle | ui-monospace, monospace |
What isn’t here#
Colors saved in the PDF belong to the document, not to your app, and look the same in every app that opens the file:
- an annotation’s color, set in each tool’s defaults (Tools & defaults);
- a redaction’s fill and label (Redaction);
- the look of a new form field (Building forms);
- a stamp’s colors (Stamps).
Good to know#
Tailwind#
Set a variable with an arbitrary property on the wrapper, for example
class="pdf-viewer [--epdf-accent:var(--color-pink-500)]".
Inside your components#
CSS variables pass through view encapsulation, so a variable set on a parent element reaches
EmbedPDF’s elements inside your own components, whatever their encapsulation.
Translucent defaults#
Defaults such as “the accent at 35%” are mixed with color-mix(), which every current browser
supports, so they always match the color they follow. A translucent part you set yourself, such as
withSelection({ color }), keeps that color when --epdf-accent changes; set its own variable
(--epdf-text-selection) to change it from CSS.
The ready-made Viewer#
The Viewer builds its light and dark themes on the same variables, so a variable you set works there too.
Your feedback goes directly to the documentation team.