EmbedPDF

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:

viewer-accent.ts
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:

theme-picker.ts
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

SettingWhat it colorsDefaultFrom CSS
accentEvery part below that has no color of its own'#3858e9'--epdf-accent
page.backgroundA page before its picture arrives'#ffffff'--epdf-page-background
page.shadowThe shadow under each page; 'none' for none'0 6px 18px rgb(0 0 0 / 0.18)'--epdf-page-shadow

Annotations: withAnnotation({ chrome })

SettingWhat it colorsDefaultFrom CSS
chrome.accentEvery annotation part below that has no color of its ownthe viewer’s accent--epdf-annotation-accent
chrome.outline.colorThe box around the selectionchrome.accent--epdf-annotation-outline
chrome.outline.widthIts line width1--epdf-annotation-outline-width
chrome.outline.style'solid' or 'dashed''solid'--epdf-annotation-outline-dash (4 3)
chrome.handles.fillInside the resize and point handles'#ffffff'--epdf-annotation-handle-fill
chrome.handles.strokeTheir edgechrome.accent--epdf-annotation-handle-stroke
chrome.rotationHandle.fillInside the rotation handlechrome.handles.fill--epdf-annotation-rotation-handle-fill
chrome.rotationHandle.strokeIts edge and its stalkchrome.handles.stroke--epdf-annotation-rotation-handle-stroke
chrome.guides.colorThe lines that show what an annotation snaps to while you move it'#e91e63'--epdf-annotation-guide
chrome.guides.rotationColorThe reference cross and the angle line while you turn itchrome.accent--epdf-annotation-rotation-guide
chrome.guides.widthThe width of both kinds of line1.5--epdf-annotation-guide-width
chrome.guides.style'solid' or 'dashed''solid'--epdf-annotation-guide-dash (4 3)
chrome.marquee.fillInside the box you drag to select several annotationschrome.accent at 8%--epdf-annotation-marquee-fill
chrome.marquee.strokeIts edgechrome.accent--epdf-annotation-marquee-stroke
chrome.textOutlineA text box while you type in itchrome.accent--epdf-annotation-text-outline
chrome.readout.backgroundBehind the angle while you turn an annotation'rgb(0 0 0 / 0.8)'--epdf-readout-background
chrome.readout.colorThe angle’s text'#ffffff'--epdf-readout-color
A tool’s ghost.opacityThe preview under the pointer0.5--epdf-ghost-opacity

Text selection: withSelection({ color, handles })

SettingWhat it colorsDefaultFrom CSS
colorSelected textthe viewer’s accent at 35%--epdf-text-selection
handles.colorThe grips at its ends, on touch screensthe viewer’s accent--epdf-text-selection-handle
handles.shadowTheir shadow'0 1px 4px rgb(0 0 0 / 0.35)'--epdf-text-selection-handle-shadow

Search: withSearch({ highlight })

SettingWhat it colorsDefaultFrom CSS
highlight.colorEvery match'#ffd500'--epdf-search-highlight
highlight.activeColorThe active match'#ff9632'--epdf-search-highlight-active
highlight.blendModeHow the highlights mix with the page'multiply'--epdf-search-blend-mode

Forms: withForm({ focus, fields })

SettingWhat it colorsDefaultFrom CSS
focus.colorThe ring around the field being filled inthe viewer’s accent--epdf-form-focus
fields.borderThe edge of a field that has no border of its ownthe viewer’s accent at 55%--epdf-form-field-border
fields.backgroundInside a field that has no background of its own'rgb(255 255 255 / 0.92)'--epdf-form-field-background
fields.colorThe 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 CSSWhat it colorsDefault
--epdf-scrollbar-thumbThe scrollbar’s thumbrgb(0 0 0 / 0.4)
--epdf-toolbar-surfaceThe toolbar’s “More” menu and folded groups#ffffff
--epdf-toolbar-borderTheir edge, and the edge of a buttonrgb(0 0 0 / 0.15)
--epdf-toolbar-activeA pressed or open buttonrgb(0 0 0 / 0.12)
--epdf-font-monoNumbers in readouts, such as the angleui-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:

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.

Was this page helpful?

Your feedback goes directly to the documentation team.