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 green
White 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-59
bulletCurrent-88
bulletCurrent-132

All match.


Final result

Before:

Green border only

After:

Green filled circle
White 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 consistency
Local 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.

Edvaldo Guimrães Filho Avatar

Published by