---
title: "Colors & theming — Vue"
description: "Every color the viewer draws is a setting with a default. Set your accent once, fine-tune any part, and override colors from CSS for dark mode or a different look per viewer."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/ui/theming"
---

# 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.vue`**

```vue
<script setup lang="ts">
import { Viewer } from '@embedpdf/vue/runtime';

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

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

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

```ts
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](#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:

```css
.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:

```css
@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.

**`App.vue`**

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

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

// Your app's own switch: the colors are in the stylesheet, under [data-theme='dark'].
const theme = ref<'light' | 'dark'>('dark');
</script>

<template>
  <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'" @click="theme = 'light'">
          Light
        </button>
        <button type="button" :aria-pressed="theme === 'dark'" @click="theme = 'dark'">
          Dark
        </button>
      </div>
    </div>
    <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
      <DocumentGate>
        <template #fallback><p class="loading">Loading…</p></template>
        <ShowColors />
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <SearchLayer />
            <SelectionLayer />
          </template>
        </Stage>
      </DocumentGate>
    </Viewer>
  </div>
</template>
```

**`dark-mode.css`**

```css
/* Colors: the example's own, and EmbedPDF's (--epdf-*) for the dark theme */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The dark theme: no code runs when it switches, the browser repaints */
.pdf-viewer[data-theme='dark'] {
  --ink: #e6ecfa;
  --muted: #93a3c7;
  --line: #2a3a63;
  --surface: #131d3b;
  --tint: #1d2a52;
  --canvas: #0b1328;
  --accent: #8ab4ff;

  --epdf-accent: #8ab4ff;
  --epdf-page-background: #2b2b2b;
  --epdf-page-shadow: 0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px rgb(0 0 0 / 0.6);
  --epdf-search-highlight: #7dd3fc;
  --epdf-search-highlight-active: #f0abfc;
}

.pdf-viewer {
  padding: 12px;
  border-radius: 16px;
  background: var(--surface);
  color: var(--ink);
  transition: background-color 0.2s ease;
}

/* The theme switch, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.segmented button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--muted);
  background: transparent;
  border: none;
  border-radius: 7px;
  padding: 6px 12px;
  cursor: pointer;
}

.segmented button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 2px var(--shadow);
}

.segmented button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ShowColors.vue`**

```vue
<script setup lang="ts">
import { computed, onMounted, watchEffect } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useSelection } from '@embedpdf/vue/selection';
import { useSearch } from '@embedpdf/vue/search';

const TITLE = { start: 10, count: 52 };

// On load: the cover's title is selected and "PDF" is found, so every color has something to paint.
const selection = useSelection();
const search = useSearch();
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);

onMounted(() => {
  void search.search({ text: 'PDF' });
});
watchEffect(
  () => {
    if (cover.value) selection.select({ page: cover.value, ...TITLE });
  },
  { flush: 'post' },
);
</script>

<template></template>
```

## Change colors while the app runs

Every setting can change while the app runs. A plugin's settings change with `updateSettings()`:

```ts
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.vue`**

```vue
<script setup lang="ts">
import { computed, ref } from 'vue';
import { epdfTheme } from '@embedpdf/vue/runtime';

import Pages from './pages.vue';

const accent = ref('#e91e63');
const theme = computed(() => epdfTheme({ accent: accent.value }));
</script>

<template>
  <div class="pdf-viewer" :style="theme">
    <input v-model="accent" type="color" />
    <Pages />
  </div>
</template>
```

**`App.vue`**

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

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

const accent = ref('#e91e63');
</script>

<template>
  <div class="pdf-viewer" :style="epdfTheme({ accent })">
    <div class="toolbar">
      <label class="picker">
        Accent
        <input v-model="accent" type="color" />
      </label>
      <button
        v-for="swatch in SWATCHES"
        :key="swatch"
        type="button"
        class="swatch"
        :aria-label="`Accent ${swatch}`"
        :aria-pressed="swatch === accent"
        :style="{ background: swatch }"
        @click="accent = swatch"
      />
      <code class="value">--epdf-accent: {{ accent }}</code>
    </div>
    <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
      <DocumentGate>
        <template #fallback><p class="loading">Loading…</p></template>
        <SelectTitle />
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <SelectionLayer />
          </template>
        </Stage>
      </DocumentGate>
    </Viewer>
  </div>
</template>
```

**`accent.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The accent picker, above the pages; it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.picker {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
}

.picker input {
  width: 36px;
  height: 30px;
  padding: 2px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}

.swatch {
  width: 26px;
  height: 26px;
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--line);
  cursor: pointer;
}

.swatch[aria-pressed='true'] {
  box-shadow: 0 0 0 2px var(--ink);
}

.swatch:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.value {
  margin-inline-start: auto;
  font-size: 12px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`SelectTitle.vue`**

```vue
<script setup lang="ts">
import { computed, watchEffect } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useSelection } from '@embedpdf/vue/selection';

// The characters of the cover's title.
const TITLE = { start: 10, count: 52 };

// The cover's title is selected on load, so the accent shows; drag over any text to see more.
const selection = useSelection();
const pages = usePageList();
const cover = computed(() => pages.value[0]?.ref);

watchEffect(
  () => {
    if (cover.value) selection.select({ page: cover.value, ...TITLE });
  },
  { flush: 'post' },
);
</script>

<template></template>
```

## 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:

```css
.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**: `annotationPlugin({ 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**: `selectionPlugin({ 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**: `searchPlugin({ 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**: `formPlugin({ 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](../viewing/stage#scrollbars)'s thumb   | `rgb(0 0 0 / 0.4)`        |
| `--epdf-toolbar-surface` | The [toolbar](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](../annotations/tools));
- a redaction's fill and label ([Redaction](../annotations/redaction#settings));
- the look of a new form field ([Building forms](../forms/building));
- a stamp's colors ([Stamps](../annotations/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)]"`.

### 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](https://www.embedpdf.com/docs/viewer/vue) builds its light and dark themes on the same variables, so a variable you
set works there too.
