Customizing the Current Step Color in ProgressStepsIndicator (SPFx + PnP Controls)
Customizing the Current Step Color in ProgressStepsIndicator (SPFx + PnP Controls)
One of the biggest advantages of using the PnP SPFx React Controls library is speed.
You get production-ready controls with minimal code.
But sooner or later, business requirements demand customization.
A very common example:
The current step must be visually highlighted in green.
Why?
Because colors communicate meaning.
In enterprise systems:
- Green = active / successful / approved
- Yellow = pending / attention
- Red = blocked / failed
This is especially useful in:
- approval flows
- onboarding processes
- incident resolution
- document lifecycle
- training workflows
In this article, we will customize the ProgressStepsIndicator current step by combining:
- SPFx ThemeProvider
- local theme override
- CSS override
- Fluent UI theme inheritance
This keeps the solution isolated without changing the entire SharePoint site theme.
The default behavior
By default, the control uses:
themeVariant.palette.themePrimary
That means the active step inherits the tenant theme color.
Example:
Tenant theme:
Blue
Current step:
Blue border
This is good for consistency.
But not always enough.
The business requirement
We want:
Before:
○ Step 1○ Step 2○ Step 3○ Step 4
After:
🟢 Step 1○ Step 2○ Step 3○ Step 4
Meaning:
Current step fully filled in greenWhite text inside
Project structure
src/├── webparts/│ ├── progressStepsIndicatorWp/│ │ ├── ProgressStepsIndicatorWpWebPart.ts│ │ ├── components/│ │ │ ├── IProgressStepsIndicatorWpProps.ts│ │ │ ├── ProgressStepsIndicatorWp.tsx│ │ │ ├── ProgressStepsIndicatorWp.module.scss
Step 1 — Define props
IProgressStepsIndicatorWpProps.ts
import { IReadonlyTheme } from '@microsoft/sp-component-base';export interface IProgressStepsIndicatorWpProps { themeVariant?: IReadonlyTheme;}
This keeps the control theme-aware.
Step 2 — Configure ThemeProvider in SPFx
ProgressStepsIndicatorWpWebPart.ts
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme} from '@microsoft/sp-component-base';private _themeProvider: ThemeProvider | undefined;private _themeVariant: IReadonlyTheme | undefined;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();}private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void { this._themeVariant = args.theme; this.render();}
This makes the control react to:
- dark mode
- tenant theme changes
- Teams theme changes
Official SPFx docs:
Microsoft Learn – ThemeProvider in SPFx
Step 3 — Clone the theme locally
Inside the React component:
ProgressStepsIndicatorWp.tsx
import * as React from 'react';import styles from './ProgressStepsIndicatorWp.module.scss';import { ProgressStepsIndicator } from '@pnp/spfx-controls-react/lib/ProgressStepsIndicator';import { IProgressStepsIndicatorWpProps } from './IProgressStepsIndicatorWpProps';const progressSteps = [ { title: 'Step 1' }, { title: 'Step 2' }, { title: 'Step 3' }, { title: 'Step 4' }];const ProgressStepsIndicatorWp: React.FC<IProgressStepsIndicatorWpProps> = ({ themeVariant}) => { const customTheme = themeVariant ? { ...themeVariant, palette: { ...themeVariant.palette, themePrimary: '#107c10' } } : undefined; return ( <div className={styles.progressWrapper}> <ProgressStepsIndicator steps={progressSteps} currentStep={0} themeVariant={customTheme} /> </div> );};export default ProgressStepsIndicatorWp;
This changes only this control.
Not the site.
Not the page.
Not the tenant.
Step 4 — Force fill the current circle
This is the key.
The control internally creates dynamic classes like:
bulletCurrent-59
These numbers change.
Never use:
.bulletCurrent-59
Wrong.
Use partial selector.
ProgressStepsIndicatorWp.module.scss
.progressWrapper { :global(div[class*="bulletCurrent"]) { background: #107c10 !important; border: 2px solid #107c10 !important; border-radius: 50%; } :global(div[class*="bulletCurrent"] label) { color: white !important; }}
This is resilient because:
bulletCurrent-59bulletCurrent-88bulletCurrent-132
All match.
Final result
Before:
Green border only
After:
Green filled circleWhite number
Exactly what we wanted.
Why this approach is better
Because it combines:
✔ SPFx ThemeProvider
✔ Local Theme Override
✔ CSS Targeting
✔ PnP Control Reuse
✔ Fluent UI Compatibility
This means:
Global consistencyLocal flexibility
That is the ideal enterprise pattern.
Important lesson
If a control exposes theme support:
Use it.
If internal styles block deeper customization:
Override carefully.
If customization becomes too complex:
Build your own.
That is the real maturity point in SPFx architecture.
Official References
PnP ProgressStepsIndicator:
PnP SPFx Controls Documentation
Fluent UI:
Microsoft Fluent UI Documentation
SPFx ThemeProvider:
Microsoft Learn – ThemeProvider
SPFx Overview:
Microsoft Learn – SharePoint Framework Overview
This is a perfect example of balancing reuse and customization in modern SharePoint development.
