Keep Users in the Loop: Building Spinners and Progress Bars in Canvas Apps
A guide to setting user expectations and improving app reliability through well-placed loading indicators, saving animations, and progress bars. Learn how to prevent double taps and reduce perceived latency.
Every second a user spends staring at a static screen feels like an eternity. In Power Apps, data operations rarely happen instantly—especially when dealing with large SharePoint lists, SQL tables, or complex workflows. Without visual feedback, users instinctively tap buttons multiple times, causing duplicate submissions or corrupted data flows.
The solution isn't just faster code (though you should optimize). It is about managing perception. Spinners, saving animations, and progress bars tell the user, "Something is happening. Your action was received." This article walks you through building all three for a sample Warehouse Inventory Audit app.
Scenario Overview: Warehouse Inventory Audit
To keep things concrete, I will reference a simple Warehouse Inventory Audit app throughout this guide.
Key Screens:
- Scan Station: The user scans a bin barcode to begin an audit.
- Audit Form: A detail form where the user records item counts and condition notes.
- Sync Data: An administrative screen that loads configuration data (catalog, team roster, history) before the app becomes usable.
Let us look at how progress indicators improve each of these screens.
Building a Reliable Loading Spinner for Screen Navigations
When a user taps a "Scan Bin" button on the Scan Station screen, the app must look up the bin details before the Audit Form appears. This lookup is nearly instant, but on a slow network the delay is noticeable. Instead of a confusing flash, we display a spinner and block further interactions.
Step 1: Acquire a Spinner Image
Power Apps does not include a default spinner asset. You can supply your own using free tools like Loading.io. Upload the exported SVG or GIF image to your app under App Settings > Resources.
Step 2: The Variable and Overlay Pattern
Place the spinner image in the center of the Scan Station screen. On top of every control on the screen, add a transparent overlay label sized to fill the whole screen. This overlay prevents the user from tapping anything else while the spinner is active.
Set the Visible property of both the overlay and the spinner image to a variable like var_IsLoading.
var_IsLoading
Set the overlay label's Fill property to a semi-transparent black to visually dim the screen:
RGBA(0, 0, 0, 0.2)
Step 3: Trigger the Spinner on Button Click
On the OnSelect of the "Scan Bin" button, show the spinner immediately, perform the data lookup, and then navigate to the destination screen.
// Show the loading state
Set(var_IsLoading, true);
// Perform the data retrieval
Set(var_CurrentBarcode, txt_Scan.Text);
Set(var_CurrentBinLookup, LookUp(WarehouseBins, Barcode = var_CurrentBarcode));
// Navigate to the audit screen
Navigate('AuditForm', ScreenTransition.Fade);Wait — Why doesn't the spinner hide here?
Placing
Set(var_IsLoading, false)right after theNavigatefunction is unreliable. WhenNavigateexecutes, the screen's context is often replaced before the next line runs. Instead, we hide the spinner on the destination screen.
Step 4: Hide the Spinner on the Destination Screen
Using a Timer control on the AuditForm screen is a robust way to clear the loading state.
- Add a
Timercontrol. - Set
Durationto 500 (half a second). - Set
AutoStartto true. - Set
OnTimerEndtoSet(var_IsLoading, false).
Alternatively, simply place the reset logic in the OnVisible property of the destination screen:
Set(var_IsLoading, false);
This pattern guarantees the spinner will eventually disappear and does not block the user interface of the new screen.
Crafting a Dependable Saving Spinner
A "Saving..." indicator is critical for forms. Without it, a user might click a Submit button multiple times, flooding your data source with duplicate records.
Setup the Overlay
Reuse the same technique from the loading spinner:
- A full-screen transparent
Label(with a white or transparent fill). - A spinner image and a "Saving..."
Label. - Set all their
Visibleproperties tovar_Saving.
Required Form Logic
Modern forms have clear lifecycle events. Use these to reliably control the spinner.
Submit Button (OnSelect):
Set(var_Saving, true); SubmitForm(frm_AuditForm);
Audit Form (OnSuccess and OnFailure):
// OnSuccess
Set(var_Saving, false);
Notify("Audit record saved", NotificationType.Success);
// OnFailure
Set(var_Saving, false);
Notify("Save failed. Please try again.", NotificationType.Error);The OnFailure handler is crucial. Without it, a failed SubmitForm leaves the spinner spinning forever.
Preventing Double Taps
The true safety net is disabling the Submit button while the save is in progress. Set the DisplayMode property of the Submit button to:
If(var_Saving, DisplayMode.Disabled, DisplayMode.Edit)
Now the button appears greyed out and unresponsive during the save operation. The user knows exactly what is happening.
Developing an Informative Progress Bar
For long operations, like loading multiple tables on app startup or syncing data, a progress bar provides context that keeps the user from abandoning the app.
The Visual Setup
- Track: A dark gray
RectangleorLabelwith height ~20 and width covering most of the screen. - Fill: A blue
Rectangle(orLabel) placed exactly over the track. - Status Label: A
Labelpositioned directly above the bar.
Width of the Fill Bar:
lbl_Track.Width * var_ProgressWidth
Text of the Status Label:
var_LoadingMessage
Sequential Loading with Estimated Progress
When loading tables one after another, you can manually increase a progress variable. This is predictable but often slower.
// Reset Set(var_LoadingMessage, "Starting data sync..."); Set(var_ProgressWidth, 0.0); // Step 1 Set(var_LoadingMessage, "Creating product catalog..."); Set(var_ProgressWidth, 0.25); ClearCollect(col_Catalog, Products); // Step 2 Set(var_LoadingMessage, "Syncing auditor roster..."); Set(var_ProgressWidth, 0.50); ClearCollect(col_Team, Auditors); // Step 3 Set(var_LoadingMessage, "Loading audit history..."); Set(var_ProgressWidth, 0.75); ClearCollect(col_History, AuditLog5000); // Step 4 Set(var_LoadingMessage, "Sync complete! Ready."); Set(var_ProgressWidth, 1.0);
Concurrent Loading with a Simple Status Message
If you use the Concurrent() function to load multiple tables at once for better performance, tracking the exact progress percentage is difficult. In this case, it is better to use an indefinite spinner combined with a clear status message.
Set(var_ProgressWidth, 0.1); Set(var_LoadingMessage, "Loading initial data..."); Concurrent( ClearCollect(col_Catalog, Products), ClearCollect(col_Team, Auditors), ClearCollect(col_History, AuditLog5000) ); Set(var_LoadingMessage, "All data loaded! Ready."); Set(var_ProgressWidth, 1.0);
Delegation Warning: A progress bar over a non-delegated query is a poor user experience. The spinner will stay visible until the entire data set is downloaded to the device. Always filter your data at the source using delegation-friendly functions like
FilterandSearch.
Common Mistakes and Troubleshooting
Even experienced builders hit these snags. Here is how to fix them:
1. The Spinner Never Hides
- Cause: The variable is never set to
false. This usually happens in forms whenOnSuccessorOnFailuredoes not fire. - Solution: Add a time limit. Wrap your spinner logic in a
Timerthat automatically sets the variable tofalseafter 30 seconds as a safety net.
2. Transparent Overlay Does Not Block Clicks
- Cause: A
Labelcontrol only blocks clicks if itsFillproperty is set to a color (even a transparent color likeRGBA(0,0,0,0)). If theFillisTransparent, clicks pass through. - Solution: Use
RGBA(0, 0, 0, 0.2)for the loading overlay so it blocks clicks and dims the screen slightly.
3. Progress Bar Does Not Reset
- Cause: The
var_ProgressWidthvariable retains its value from the previous run. - Solution: Reset it to
0in the screen'sOnVisibleproperty before the loading sequence starts.
If( var_Role = "Admin", Set(var_ProgressWidth, 0); Set(var_LoadingMessage, ""); )
4. Saving Spinner Visible on Screen Load
- Cause: The
var_Savingvariable is not initialized. - Solution: Set
var_Saving = falseinApp.OnStartor the screen'sOnVisible.
Final Recommendation
Visual feedback is not a decorative element—it is a core reliability feature. You should integrate these patterns into every project from day one:
- Use a Spinner for any screen transition that queries a database.
- Use a Saving Indicator for every
SubmitFormcall. - Use a Progress Bar for lengthy startup sequences or bulk synchronization.
The absence of feedback creates anxious users. An anxious user is a double-clicking user. By systematically implementing these indicators, you make your app feel faster, more professional, and safer.
References
- Original guide on loading spinners, saving spinners, and progress bars by Matthew Devaney: https://www.matthewdevaney.com/power-apps-loading-spinners-saving-spinners-and-progress-bars/
- Microsoft Learn: Working with screens in canvas apps [URL placeholder]
- Microsoft Learn: Form and data source controls in Power Apps [URL placeholder]
Learn how small coding practices—like using descriptive names, flattening conditions, and simplifying logic—can make your apps easier to update and less error-prone.
Move past the gallery and discover the hidden patterns for validation, navigation, and smart submission handling in your data entry forms.
PowerShell unlocks admin capabilities that the Power Platform admin center simply doesn’t offer—from recovering deleted apps to blocking trial licenses. Here’s how to wield them safely.