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
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):
| Tokens | Role |
|---|---|
background, foreground | Root background and default text |
card, cardForeground | Card / panel surfaces |
popover, popoverForeground | Popovers and dropdowns |
primary, primaryForeground | Primary actions (buttons, active states) |
secondary, secondaryForeground | Subdued actions |
muted, mutedForeground | Muted surfaces and secondary text |
accent, accentForeground | Hover-highlight surfaces |
destructive, destructiveForeground | Danger / delete actions |
border, input, ring | Borders, 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
colorskey 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@themedeclaration, which cannot see variables set on a child element. radiusbecomes--pptx-radius, plus derived--radius-sm/--radius-md/--radius-lg/--radius-xlvalues (calc(r - 4px)throughcalc(r + 4px)).cssVarsentries pass through verbatim.- With
omitDefaults: true, values equal to the built-in defaults are skipped.
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
| Export | Palette |
|---|---|
defaultThemeColors + defaultRadius | The built-in dark UI (Tailwind gray scale, indigo primary, 0.5rem radius). Applied when no theme is passed. |
vermilionLightTheme / vermilionLightColors | Warm light "paper" palette with the vermilion accent used by this documentation site. |
vermilionDarkTheme / vermilionDarkColors | Dimmed 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:
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.
import { PowerPointViewer, vermilionDarkTheme } from 'pptx-react-viewer';
<PowerPointViewer content={bytes} theme={vermilionDarkTheme} />;<script setup lang="ts">
import { PowerPointViewer, vermilionDarkTheme } from 'pptx-vue-viewer';
</script>
<template>
<PowerPointViewer :content="bytes" :theme="vermilionDarkTheme" />
</template>// <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)]<script lang="ts">
import { PowerPointViewer, vermilionDarkTheme } from 'pptx-svelte-viewer';
</script>
<PowerPointViewer content={bytes} theme={vermilionDarkTheme} />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):
| Mode | Setup |
|---|---|
| Tailwind CSS v4 host | No CSS import needed; the viewer's classes resolve through your existing config. Override values with the theme prop/input. |
| Bundled stylesheet | import '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 properties | Define 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 property | Controls |
|---|---|
--pptx-selection-corner-size | Corner artwork width and height |
--pptx-selection-corner-radius | Corner artwork border radius |
--pptx-selection-edge-length | Edge artwork length parallel to the edge |
--pptx-selection-edge-thickness | Edge artwork thickness across the edge |
--pptx-selection-edge-radius | Edge artwork border radius |
--pptx-selection-handle-fill | Resize artwork fill |
--pptx-selection-handle-border-color | Resize and Rotate artwork border color |
--pptx-selection-outline-color | Selection outline and existing Rotate stem |
--pptx-selection-rotate-size | Circular Rotate artwork diameter |
--pptx-selection-rotate-fill | Rotate artwork fill |
--pptx-selection-rotate-foreground | Rotate 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.
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:
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:
defaultThemeKeyprop (for a non-'default'starting point)- A previously persisted choice (
localStorage, keypptx-viewer-prefs) - The catalog's
'default'entry
Catalog props (all optional)
| Prop | Type | Purpose |
|---|---|---|
defaultThemeKey | string | Initial THEME_CATALOG key, used only when nothing is persisted yet. |
availableThemes | ThemeCatalogEntry[] | Override the catalog offered in Appearance (add your own presets, or narrow the list). |
onThemeChange | (key: string) => void | When supplied, you own persisting the choice; the viewer stops writing to localStorage and only calls this back. |
defaultLocale | string | Same idea for Language: initial locale code when nothing is persisted. |
availableLocales | LocaleCatalogEntry[] | Override the locales offered in Options > Language. |
onLocaleChange | (code: string) => void | When 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.
// 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:
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
- React theming - the
themeprop,ViewerThemeProvider, and the full token table - Vue theming - the
themeprop andprovideViewerTheme - Angular theming - the
themeinput,provideViewerTheme, and theVIEWER_THEMEtoken - Svelte theming - the
themeprop - Vanilla theming - the
themeoption,setTheme(), andgetViewerCss()
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
localStoragekey as the theme/locale fallback.