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
IReadonlyThemeis - what
themeVariantmeans - why
themeVariantis not the same asIReadonlyTheme - how
ThemeProviderworks - why
this.context.themeProviderfails - 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 shapeit 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:
| Control | Supports Theme |
|---|---|
| ProgressStepsIndicator | Yes |
| ListView | Yes |
| PeoplePicker | Yes |
| TaxonomyPicker | Yes |
| LivePersona | Yes |
| DynamicForm | Yes |
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 parts50 web parts100 web parts150 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.
