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.svelte
<script lang="ts">
  import { Viewer } from '@embedpdf/svelte/runtime';

  import Pages from './pages.svelte';
  import { engine, plugins } from './pdf';
</script>

<Viewer {engine} {plugins} accent="#e91e63" page={{ shadow: 'none' }}>
  <Pages />
</Viewer>

Then fine-tune any part with its plugin’s settings, such as the handles of an annotation:

annotationPlugin({
  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…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { SelectionLayer, selectionPlugin } from '@embedpdf/svelte/selection';
  import { SearchLayer, searchPlugin } from '@embedpdf/svelte/search';
  import { localEngine } from '@embedpdf/engine';
  import ShowColors from './ShowColors.svelte';

  import '../dark-mode.css';

  const engine = localEngine();
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    selectionPlugin(),
    searchPlugin(),
  ];

  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()) };
  };
</script>

<script lang="ts">
  // Your app's own switch: the colors are in the stylesheet, under [data-theme='dark'].
  let theme: 'light' | 'dark' = $state('dark');
</script>

<div class="pdf-viewer" data-theme={theme}>
  <div class="toolbar">
    <div class="segmented" role="group" aria-label="Theme">
      <button type="button" aria-pressed={theme === 'light'} onclick={() => (theme = 'light')}>
        Light
      </button>
      <button type="button" aria-pressed={theme === 'dark'} onclick={() => (theme = 'dark')}>
        Dark
      </button>
    </div>
  </div>
  <Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
    <DocumentGate>
      {#snippet fallback()}
        <p class="loading">Loading…</p>
      {/snippet}
      <ShowColors />
      <Stage class="stage">
        <RenderLayer />
        <SearchLayer />
        <SelectionLayer />
      </Stage>
    </DocumentGate>
  </Viewer>
</div>

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 follow the values you give it, like any other prop.

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.svelte
<script lang="ts">
  import { epdfTheme } from '@embedpdf/svelte/runtime';

  import Pages from './pages.svelte';

  let accent = $state('#e91e63');
</script>

<div class="pdf-viewer" style={epdfTheme({ accent })}>
  <input type="color" bind:value={accent} />
  <Pages />
</div>

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, epdfTheme, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { SelectionLayer, selectionPlugin } from '@embedpdf/svelte/selection';
  import { localEngine } from '@embedpdf/engine';
  import SelectTitle from './SelectTitle.svelte';

  import '../accent.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];

  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()) };
  };
</script>

<script lang="ts">
  let accent = $state('#e91e63');
</script>

<div class="pdf-viewer" style={epdfTheme({ accent })}>
  <div class="toolbar">
    <label class="picker">
      Accent
      <input type="color" bind:value={accent} />
    </label>
    {#each SWATCHES as swatch (swatch)}
      <button
        type="button"
        class="swatch"
        aria-label="Accent {swatch}"
        aria-pressed={swatch === accent}
        style:background={swatch}
        onclick={() => (accent = swatch)}
      ></button>
    {/each}
    <code class="value">--epdf-accent: {accent}</code>
  </div>
  <Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
    <DocumentGate>
      {#snippet fallback()}
        <p class="loading">Loading…</p>
      {/snippet}
      <SelectTitle />
      <Stage class="stage">
        <RenderLayer />
        <SelectionLayer />
      </Stage>
    </DocumentGate>
  </Viewer>
</div>

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: annotationPlugin({ 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: selectionPlugin({ 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: searchPlugin({ 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: formPlugin({ 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)]".

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 selectionPlugin({ 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.