One of the most overlooked areas in SharePoint Framework development is theme awareness.

A web part can be technically correct, fully functional, and still feel visually “wrong” if it ignores the current SharePoint theme.


Mastering Theme Handling in SPFx: Using ThemeProvider, IReadonlyTheme, and Theme-Aware PnP Controls

One of the most overlooked areas in SharePoint Framework development is theme awareness.

A web part can be technically correct, fully functional, and still feel visually “wrong” if it ignores the current SharePoint theme.

This becomes critical in enterprise environments where:

  • tenant branding is enforced
  • dark mode is used
  • Microsoft Teams integration exists
  • multiple site collections use different themes

In this article we will deeply explore:

  • what IReadonlyTheme is
  • what themeVariant means
  • why themeVariant is not the same as IReadonlyTheme
  • how ThemeProvider works
  • why this.context.themeProvider fails
  • the correct serviceScope.consume() pattern
  • event-driven theme updates
  • passing themes into React
  • integrating themes with PnP SPFx React Controls controls

This is fundamental knowledge for building production-grade SPFx solutions.


Why theme matters

Modern SharePoint Online supports:

  • default Microsoft themes
  • custom tenant themes
  • dark themes
  • Teams themes
  • Viva Connections themes

Without theme support:

your component looks isolated

With theme support:

your component feels native

This is a major architectural difference.


The most common beginner mistake

Many developers try this:

themeVariant: IReadonlyTheme.

or:

const themeVariant = this.context.themeProvider.tryGetTheme();

Both are conceptually wrong.

Why?


Understanding IReadonlyTheme

IReadonlyTheme is only:

a TypeScript interface

Meaning:

it defines shape
it does not contain data

Example:

export interface IReadonlyTheme {
palette: ...
semanticColors: ...
}

Think:

Blueprint ≠ House

The interface describes the house.

It is not the house.


So where does the actual theme come from?

The actual theme comes from:

ThemeProvider

Imported from:

@microsoft/sp-component-base

This service provides the real active theme object.


Why this.context.themeProvider fails

You got this error:

Property 'themeProvider' does not exist on type 'WebPartContext'

This is expected.

Because:

WebPartContext does not expose ThemeProvider directly

Instead, SPFx exposes services through:

serviceScope

This is dependency injection.

Very important.


The correct architecture

SPFx theme flow:

SharePoint Theme Engine
ThemeProvider Service
Current Theme Object
Theme Changed Event
Web Part State
React Props
PnP Control

This is the real pipeline.


Step 1 — Create the Props Interface

File:

IProgressStepsIndicatorWpProps.ts

import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IProgressStepsIndicatorWpProps {
themeVariant?: IReadonlyTheme;
}

Why optional?

Because during initialization the theme may not be ready.


Step 2 — Consume ThemeProvider from ServiceScope

This is where everything starts.

WebPart class

private _themeProvider: ThemeProvider | undefined;
private _themeVariant: IReadonlyTheme | undefined;

Why?

We need:

  • provider instance
  • active theme instance

These are different things.


Step 3 — Initialize ThemeProvider

Inside onInit():

protected onInit(): Promise<void> {
this._themeProvider = this.context.serviceScope.consume(
ThemeProvider.serviceKey
);
this._themeVariant = this._themeProvider.tryGetTheme();
this._themeProvider.themeChangedEvent.add(
this,
this._handleThemeChangedEvent
);
return Promise.resolve();
}

This is the official pattern.

Important:

consume()

means:

give me the registered service instance

Step 4 — React to Theme Changes

Modern SharePoint themes can change dynamically.

Handle it:

private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
this._themeVariant = args.theme;
this.render();
}

This means:

If tenant switches to dark mode:

your component updates automatically

Without page refresh.

Enterprise-ready.


Step 5 — Pass theme into React

Inside render:

public render(): void {
const element: React.ReactElement<IProgressStepsIndicatorWpProps> =
React.createElement(
ProgressStepsIndicatorWp,
{
themeVariant: this._themeVariant
}
);
ReactDom.render(element, this.domElement);
}

Simple and clean.


Step 6 — Use theme in the React component

Component:

ProgressStepsIndicatorWp.tsx

import * as React from 'react';
import { IProgressStepsIndicatorWpProps } from './IProgressStepsIndicatorWpProps';
import { ProgressStepsIndicator } from '@pnp/spfx-controls-react/lib/ProgressStepsIndicator';
const progressSteps = [
{
title: 'Step 1',
description: 'Planning'
},
{
title: 'Step 2',
description: 'Development'
},
{
title: 'Step 3',
description: 'Testing'
},
{
title: 'Step 4',
description: 'Deployment'
}
];
const ProgressStepsIndicatorWp: React.FC<IProgressStepsIndicatorWpProps> = ({
themeVariant
}) => {
return (
<ProgressStepsIndicator
steps={progressSteps}
currentStep={0}
themeVariant={themeVariant}
/>
);
};
export default ProgressStepsIndicatorWp;

Now the control respects:

  • tenant branding
  • site colors
  • dark mode

Automatically.


The JSX error we fixed

You also had:

Wrong:

return (
{
<ProgressStepsIndicator />
}
);

Problem:

This returns an object.

Not JSX.

Error:

Type boolean is not assignable to ReactElement

Correct:

return (
<ProgressStepsIndicator />
);

Simple.

But critical.


ThemeProvider vs onThemeChanged()

There are two theme mechanisms:


Base WebPart onThemeChanged()

protected onThemeChanged(currentTheme: IReadonlyTheme | undefined)

Good for:

  • CSS variables
  • DOM-level styling

Example:

this.domElement.style.setProperty(...)

ThemeProvider

Better for:

  • React props
  • Fluent UI
  • PnP Controls
  • deeper component trees

Preferred for component-driven architecture.


When should you pass themeVariant?

Always when supported.

Examples:

PnP controls supporting theme:

ControlSupports Theme
ProgressStepsIndicatorYes
ListViewYes
PeoplePickerYes
TaxonomyPickerYes
LivePersonaYes
DynamicFormYes

This keeps consistency.


Real enterprise scenario

Imagine:

Tenant A:

Blue corporate branding

Tenant B:

Green sustainability branding

Without theme:

your component stays default gray

With theme:

your component adapts automatically

Huge difference.


Best practices

Always:

✔ consume ThemeProvider from serviceScope
✔ store current theme
✔ subscribe to theme changes
✔ remove listeners on dispose
✔ pass themeVariant down the component tree
✔ make theme props optional


Avoid:

✘ hardcoded colors
✘ assuming light theme
✘ using this.context.themeProvider
✘ confusing interface with instance
✘ returning JSX wrapped in {}


Final Architecture

SPFx Runtime
ThemeProvider
Current Theme
Theme Event
WebPart State
React Props
PnP Controls
Fluent UI Rendering

This is the correct mental model.

Understand this once, and every future SPFx solution becomes cleaner.

Especially important in large solutions:

10 web parts
50 web parts
100 web parts
150 web parts

Exactly the kind of reusable architecture your SharePoint atomic app project is targeting.


Official References

SPFx ThemeProvider:

Microsoft Learn – ThemeProvider in SPFx

SPFx Theme Support:

Microsoft Learn – Use theme colors in SPFx

SPFx BaseClientSideWebPart:

Microsoft Learn – BaseClientSideWebPart API

PnP ProgressStepsIndicator:

PnP SPFx Controls – ProgressStepsIndicator

Fluent UI Theming:

Microsoft Fluent UI Theming Documentation

This is one of the most important SPFx infrastructure concepts to master. Once understood, it affects every single reusable component you build.

Edvaldo Guimrães Filho Avatar

Published by