Understanding the PnP SPFx Progress Control — Part 1: The Fundamentals
“Progress from Scratch” series: in this article (1 of 5) we’ll understand the core concept behind the Progress control from @pnp/spfx-controls-react, and build the first Web Part — fully static, just to see the control rendered on screen.
What Progress is NOT
Before writing any code, it’s worth clearing up a common misunderstanding: Progress is not a percentage progress bar (like a component that fills up from 0% to 100%). It’s meant to represent the state of multiple sequentially executed actions — think of an installation checklist, where each item changes state as the process moves forward.
Another important point: the control doesn’t execute anything by itself. It only renders the current state of the actions. Running the actual logic for each action (calling an API, creating a folder, copying a file, etc.) is the developer’s responsibility. Progress just receives information and draws it.
The core concept: actions + currentActionIndex
The control basically receives two things:
actions: an array of objects following theIProgressActioninterface. At minimum, each action has atitle— the text displayed for that step.currentActionIndex: a number indicating which position in the array execution is “at” right now.
From these two pieces of information, the control calculates the visual state of each item on its own:
- ✅ Completed — when the item’s index is less than
currentActionIndex - 🔄 In progress — when the item’s index equals
currentActionIndex - ⬜ Not started — when the item’s index is greater than
currentActionIndex
In other words, the visual state of each step is mathematically derived from a single number. You never say directly “paint this item green” — you say “I’m on step 2” and the control figures out the rest on its own. This is the key insight for understanding any more advanced use of Progress.
Web Part 1 — Static Progress, no logic at all
The goal of this first Web Part is to isolate the learning as much as possible: no useState, no button, no async execution. Just the control rendered with fixed data, so you can visually observe how it behaves.
import * as React from 'react';import { Progress, IProgressAction } from '@pnp/spfx-controls-react/lib/Progress';export default class Wp1BasicProgress extends React.Component { // MOCKED data: a fixed array, each item only needs a "title" private mockActions: IProgressAction[] = [ { title: 'Create folder' } as IProgressAction, { title: 'Copy files' } as IProgressAction, { title: 'Set permissions' } as IProgressAction, ]; public render(): React.ReactElement { return ( <Progress title={'My first Progress'} actions={this.mockActions} currentActionIndex={1} // fixed: pretending we're on step 2 (index 1) showOverallProgress={true} /> ); }}
Why the code is written this way
currentActionIndex={1}is hardcoded on purpose. There’s no state here at all, intentionally. The idea is to manually change this number (0, 1, 2, 3…) and re-run it, just to observe the control “repainting” the list differently for each value.- A complete
IProgressActionhas more optional fields (like anexecutemethod), but for this first step justtitleis enough — that’s whyas IProgressActionis used, so TypeScript doesn’t complain about the incomplete typing.
Experiment before moving to Part 2
Change currentActionIndex={1} to currentActionIndex={3} — a number greater than the number of items in the array (which only has 3 positions, indices 0, 1, and 2). Observe what happens to the list.
This small experiment reveals how the control handles the “the sequence is over” case, which will be essential in Part 3, when we start executing real actions and need to know when to stop.
In Part 2 of the series, we’ll move away from fixed data and introduce a button that manually advances currentActionIndex, to understand how the sequence’s “cursor” moves with real state.
