---
title: "Colors & theming — React"
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: "React"
source: "https://www.embedpdf.com/docs/headless/react/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.tsx`**

```tsx
import { Viewer } from '@embedpdf/react/runtime';

import { Pages } from './pages';
import { engine, plugins } from './pdf';

export function DocumentViewer() {
  return (
    <Viewer engine={engine} plugins={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:

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

**`dark-mode.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin, useSelection } from '@embedpdf/react/selection';
import { SearchLayer, searchPlugin, useSearch } from '@embedpdf/react/search';
import { localEngine } from '@embedpdf/engine';

import './dark-mode.css';

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

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.
function ShowColors() {
  const selection = useSelection();
  const search = useSearch();
  const cover = usePageList()[0]?.ref;
  useEffect(() => {
    void search.search({ text: 'PDF' });
  }, [search]);
  useEffect(() => {
    if (cover) selection.select({ page: cover, ...TITLE });
  }, [selection, cover]);
  return null;
}

export default function App() {
  // Your app's own switch: the colors are in the stylesheet, under [data-theme='dark'].
  const [theme, setTheme] = useState<'light' | 'dark'>('dark');

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

**`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);
}
```

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

```tsx
import { useState } from 'react';
import { epdfTheme } from '@embedpdf/react/runtime';

import { Pages } from './pages';

export function BrandedViewer() {
  const [accent, setAccent] = useState('#e91e63');

  return (
    <div className="pdf-viewer" style={epdfTheme({ accent })}>
      <input type="color" value={accent} onChange={(event) => setAccent(event.target.value)} />
      <Pages />
    </div>
  );
}
```

**`accent.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, epdfTheme, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { SelectionLayer, selectionPlugin, useSelection } from '@embedpdf/react/selection';
import { localEngine } from '@embedpdf/engine';

import './accent.css';

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

// 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.
function SelectTitle() {
  const selection = useSelection();
  const cover = usePageList()[0]?.ref;
  useEffect(() => {
    if (cover) selection.select({ page: cover, ...TITLE });
  }, [selection, cover]);
  return null;
}

export default function App() {
  const [accent, setAccent] = useState('#e91e63');

  return (
    <div className="pdf-viewer" style={epdfTheme({ accent })}>
      <div className="toolbar">
        <label className="picker">
          Accent
          <input type="color" value={accent} onChange={(event) => setAccent(event.target.value)} />
        </label>
        {SWATCHES.map((swatch) => (
          <button
            key={swatch}
            type="button"
            className="swatch"
            aria-label={`Accent ${swatch}`}
            aria-pressed={swatch === accent}
            style={{ background: swatch }}
            onClick={() => setAccent(swatch)}
          />
        ))}
        <code className="value">--epdf-accent: {accent}</code>
      </div>
      <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
        <DocumentGate fallback={<p className="loading">Loading…</p>}>
          <SelectTitle />
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <SelectionLayer />
              </>
            )}
          </Stage>
        </DocumentGate>
      </Viewer>
    </div>
  );
}
```

**`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);
}
```

## 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/react) builds its light and dark themes on the same variables, so a variable you
set works there too.
