Skip to content

Theming ​

The viewer chrome (toolbar, ribbon, dialogs, backstage) is themed through a ViewerTheme object: a set of CSS custom properties (--pptx-*) applied to the viewer root. This is entirely separate from a presentation's own OOXML color scheme and fonts (the Design tab's "Themes" gallery edits the .pptx document itself); ViewerTheme only affects the app's own UI.

The theme system is framework-agnostic. The types, defaults, presets, and helpers below are implemented once (in the internal pptx-viewer-shared package) and re-exported identically by every published binding: pptx-react-viewer, pptx-vue-viewer, pptx-angular-viewer, pptx-svelte-viewer, and pptx-vanilla-viewer.

The ViewerTheme shape ​

ts
interface ViewerTheme {
	/** Semantic UI colors. Each key maps to a `--pptx-<key>` custom property. */
	colors?: Partial<ViewerThemeColors>;
	/** Base border-radius value (e.g. "0.5rem", "8px"). */
	radius?: string;
	/** Escape hatch: arbitrary CSS custom properties set on the viewer root. Keys include the `--` prefix. */
	cssVars?: Record<string, string>;
}

Every field is optional; unset values fall back to the built-in dark defaults. ViewerThemeColors has 19 semantic tokens, named after the shadcn/ui convention. Any valid CSS color string is accepted (hex, rgb(), hsl(), oklch(), named colors):

TokensRole
background, foregroundRoot background and default text
card, cardForegroundCard / panel surfaces
popover, popoverForegroundPopovers and dropdowns
primary, primaryForegroundPrimary actions (buttons, active states)
secondary, secondaryForegroundSubdued actions
muted, mutedForegroundMuted surfaces and secondary text
accent, accentForegroundHover-highlight surfaces
destructive, destructiveForegroundDanger / delete actions
border, input, ringBorders, input borders, focus ring

See the React theming page for the full token-by-token table with each token's exact CSS variable name.

How themeToCssVars produces --pptx-* variables ​

themeToCssVars(theme, omitDefaults = false) converts a ViewerTheme into a flat Record<string, string> of CSS custom properties ready to apply to the viewer root as inline style. Every binding calls it internally when you pass a theme; it is also exported for building your own tooling.

  • Each colors key becomes --pptx-<kebab-case-key>: primaryForeground: '#fff' emits --pptx-primary-foreground: #fff.
  • Each color is also mirrored to the matching Tailwind semantic token (--color-primary-foreground), so that in a Tailwind CSS v4 host the value overrides the @theme declaration, which cannot see variables set on a child element.
  • radius becomes --pptx-radius, plus derived --radius-sm / --radius-md / --radius-lg / --radius-xl values (calc(r - 4px) through calc(r + 4px)).
  • cssVars entries pass through verbatim.
  • With omitDefaults: true, values equal to the built-in defaults are skipped.
ts
import { themeToCssVars, defaultCssVars } from 'pptx-react-viewer';

themeToCssVars({ colors: { primary: '#6366f1' }, radius: '0.75rem' });
// {
//   '--pptx-primary': '#6366f1', '--color-primary': '#6366f1',
//   '--pptx-radius': '0.75rem',
//   '--radius-sm': 'calc(0.75rem - 4px)', ... '--radius-xl': 'calc(0.75rem + 4px)',
// }

defaultCssVars();
// The complete set of --pptx-* properties with the built-in dark defaults,
// for generating a full fallback stylesheet.

Built-in themes and presets ​

ExportPalette
defaultThemeColors + defaultRadiusThe built-in dark UI (Tailwind gray scale, indigo primary, 0.5rem radius). Applied when no theme is passed.
vermilionLightTheme / vermilionLightColorsWarm light "paper" palette with the vermilion accent used by this documentation site.
vermilionDarkTheme / vermilionDarkColorsDimmed dark "presenter room" palette with the same accent.
vermilionRadius'0.375rem', the radius both vermilion presets use.

Both vermilion presets are complete ViewerTheme objects (all 19 tokens plus radius), so they fully replace the dark defaults. The raw *Colors palettes are exported alongside for deriving variants:

ts
import { vermilionDarkColors, vermilionRadius } from 'pptx-react-viewer';
import type { ViewerTheme } from 'pptx-react-viewer';

const custom: ViewerTheme = {
	colors: { ...vermilionDarkColors, primary: '#38bdf8' },
	radius: vermilionRadius,
};

A plain (non-vermilion) light palette also exists, but it is not a named export of the bindings; reach it through the theme catalog: resolveThemeCatalogEntry('light').

Applying a theme per binding ​

Every binding takes the same ViewerTheme object; only the delivery mechanism differs.

tsx
import { PowerPointViewer, vermilionDarkTheme } from 'pptx-react-viewer';

<PowerPointViewer content={bytes} theme={vermilionDarkTheme} />;
vue
<script setup lang="ts">
import { PowerPointViewer, vermilionDarkTheme } from 'pptx-vue-viewer';
</script>

<template>
	<PowerPointViewer :content="bytes" :theme="vermilionDarkTheme" />
</template>
ts
// <pptx-viewer [content]="bytes" [theme]="theme" />
import { vermilionDarkTheme } from 'pptx-angular-viewer';

export class DeckComponent {
	theme = vermilionDarkTheme;
}
// Or share one theme across a subtree without the input:
// providers: [provideViewerTheme(vermilionDarkTheme)]
svelte
<script lang="ts">
	import { PowerPointViewer, vermilionDarkTheme } from 'pptx-svelte-viewer';
</script>

<PowerPointViewer content={bytes} theme={vermilionDarkTheme} />
ts
import { createPptxViewer, vermilionDarkTheme } from 'pptx-vanilla-viewer';

const viewer = createPptxViewer(host, {
	source: bytes,
	theme: vermilionDarkTheme,
});
// Change later at runtime:
viewer.setTheme({ colors: { primary: '#38bdf8' } });

For sharing one theme across multiple viewers, React exports ViewerThemeProvider / useViewerTheme, Vue exports provideViewerTheme / useViewerTheme, and Angular exports provideViewerTheme / the VIEWER_THEME injection token.

Styling modes ​

The viewer UI references --pptx-* custom properties for every visual token, which allows three styling setups (in increasing order of control):

ModeSetup
Tailwind CSS v4 hostNo CSS import needed; the viewer's classes resolve through your existing config. Override values with the theme prop/input.
Bundled stylesheetimport 'pptx-react-viewer/styles' (the Vue and Angular packages also expose /styles and /styles.css). The Svelte package compiles its styles into its components. Ships all required utility classes plus the dark defaults.
Raw CSS custom propertiesDefine the --pptx-* properties yourself (see defaultCssVars() for the full list) and skip both.

The vanilla binding is the exception: createPptxViewer injects its own scoped stylesheet automatically (idempotent, #pptx-vanilla-viewer-styles). Hosts with a strict CSP can pre-render the string from getViewerCss() instead.

Selection-control artwork ​

Selection artwork uses the existing ViewerTheme.cssVars field, not a separate theme object or component prop, in all five bindings. These optional tokens do not change a binding's default appearance when omitted. They affect editor controls, not slide content, font sizes, authored geometry, or exported documents.

CSS custom propertyControls
--pptx-selection-corner-sizeCorner artwork width and height
--pptx-selection-corner-radiusCorner artwork border radius
--pptx-selection-edge-lengthEdge artwork length parallel to the edge
--pptx-selection-edge-thicknessEdge artwork thickness across the edge
--pptx-selection-edge-radiusEdge artwork border radius
--pptx-selection-handle-fillResize artwork fill
--pptx-selection-handle-border-colorResize and Rotate artwork border color
--pptx-selection-outline-colorSelection outline and existing Rotate stem
--pptx-selection-rotate-sizeCircular Rotate artwork diameter
--pptx-selection-rotate-fillRotate artwork fill
--pptx-selection-rotate-foregroundRotate glyph color, where a glyph is drawn

Use positive pixel lengths such as 6px for sizes, CSS lengths or percentages for radii, and valid CSS colors for colors. A var() fallback handles an omitted token, not an arbitrary invalid CSS value. The viewer does not parse or sanitize these CSS strings. Remove an override to restore the binding's own default. Radius values use the control's local CSS coordinates, like its existing border and shadow; they are not separately compensated for stage zoom. Use 0px for square corners or 50% for a proportional round shape across bindings.

ts
const theme = {
	cssVars: {
		'--pptx-selection-corner-size': '6px',
		'--pptx-selection-corner-radius': '0px',
		'--pptx-selection-edge-length': '6px',
		'--pptx-selection-edge-thickness': '6px',
		'--pptx-selection-edge-radius': '0px',
		'--pptx-selection-handle-fill': '#ffffff',
		'--pptx-selection-handle-border-color': '#6366f1',
		'--pptx-selection-outline-color': '#6366f1',
		'--pptx-selection-rotate-fill': '#ffffff',
		'--pptx-selection-rotate-foreground': '#6366f1',
	},
};

Pass this same theme through the binding-specific examples above. For a custom shell, apply themeToCssVars(theme) to the common ancestor of the slide and its selection controls. Do not target private child classes or assume handles are nested inside the selected shape.

Artwork remains centered on the existing anchors. Smaller artwork does not shrink the original invisible mouse or touch target. Larger artwork expands its frame, but existing neighbor-hit partitioning still applies on tiny shapes; oversized visuals can overlap without taking another handle's input region. This contract does not expose hit sizes or Rotate offsets. Keep large custom artwork practical for the shapes being edited. Adjustment diamonds and connector endpoint indicators retain their distinct meanings and appearance.

File > Options > Appearance ​

Every binding's Settings dialog has an Appearance tab: a small gallery of built-in theme presets (Default, Light, Vermilion Light, Vermilion Dark) a user can click through at runtime, defined by the THEME_CATALOG export:

ts
interface ThemeCatalogEntry {
	key: string; // 'default' | 'light' | 'vermilionLight' | 'vermilionDark'
	labelKey: string; // pptx.* translation key for the entry's label
	theme: ViewerTheme | undefined; // undefined = reset to the built-in default
}

This is deliberately a short, curated list, not a full gallery; pass availableThemes (below) for more or fewer choices. resolveThemeCatalogEntry(key, catalog?) looks an entry up by key.

Precedence: an explicit theme always wins ​

If you pass a theme prop/input to the viewer, it keeps winning over anything picked in the Appearance tab; the picker is inert while theme is set. This is intentional: a host that owns its own theme (for example, syncing with an app-wide dark-mode toggle) should not have it overridden by a click inside the viewer.

The Appearance tab only takes effect when you do not pass an explicit theme. In that standalone mode, resolution order is:

  1. defaultThemeKey prop (for a non-'default' starting point)
  2. A previously persisted choice (localStorage, key pptx-viewer-prefs)
  3. The catalog's 'default' entry

Catalog props (all optional) ​

PropTypePurpose
defaultThemeKeystringInitial THEME_CATALOG key, used only when nothing is persisted yet.
availableThemesThemeCatalogEntry[]Override the catalog offered in Appearance (add your own presets, or narrow the list).
onThemeChange(key: string) => voidWhen supplied, you own persisting the choice; the viewer stops writing to localStorage and only calls this back.
defaultLocalestringSame idea for Language: initial locale code when nothing is persisted.
availableLocalesLocaleCatalogEntry[]Override the locales offered in Options > Language.
onLocaleChange(code: string) => voidWhen supplied, the viewer never touches your i18n instance itself; it only calls this back.

React, Vue, Angular, and Svelte all use this exact shape. Vanilla is the one exception: it already had public theme/locale constructor options and setTheme()/setLocale() methods, so those serve as the initial value instead of separate defaultThemeKey/defaultLocale options; availableThemes/availableLocales/onThemeChange/onLocaleChange are the same as everywhere else.

tsx
// React - host owns theme persistence (e.g. syncing with app-wide dark mode)
<PowerPointViewer
	content={bytes}
	defaultThemeKey={systemPrefersDark ? 'vermilionDark' : 'vermilionLight'}
	onThemeChange={(key) => saveUserPreference('viewerTheme', key)}
/>

Adding your own presets ​

Pass availableThemes with your own ThemeCatalogEntry[]; extend the built-ins rather than replacing them by spreading THEME_CATALOG:

ts
import { THEME_CATALOG } from 'pptx-react-viewer';

const availableThemes = [
	...THEME_CATALOG,
	{ key: 'brand', labelKey: 'app.theme.brand', theme: { colors: { primary: '#7c3aed' } } },
];

labelKey is looked up through whatever i18n dictionary your app supplies (see Localization); for a custom entry, register that key yourself. Unregistered keys fall back to a readable label derived from the key's last segment.

Per-binding details ​

Next steps ​

  • Localization (i18n) - the Language tab works the same way, and this is where the pptx.* translation keys backing theme labels come from.
  • Account & Sign-in - File > Account's profile editor also persists to the same localStorage key as the theme/locale fallback.

Released under the Apache-2.0 License.