Controls

ProgressBar

ProgressBar is the framework's passive range display for work that can be measured or is still in flight. It shares the same RangeBase foundation as Slider and ScrollBar, but it is intentionally read-only from the user's perspective: the control reports state instead of collecting it.

Quick start

Use ProgressBar when the UI should expose completion, load state, validation-adjacent status, or background activity without exposing direct manipulation. The same control can render as a classic determinate fill, a vertical meter, or an indeterminate activity strip.

Minimal determinate bar

<ProgressBar Width="260"
             Minimum="0"
             Maximum="100"
             Value="64" />

Vertical meter

<ProgressBar Width="10"
             Height="140"
             Orientation="Vertical"
             Minimum="0"
             Maximum="1"
             Value="0.3" />

Indeterminate activity bar

<ProgressBar Width="220"
             Height="10"
             IsIndeterminate="True" />

Choose the right range control: use ProgressBar when the value is informational. Use Slider when the user should choose a value, and use ScrollBar when the range represents viewport movement through content.

What the range means

ProgressBar inherits Minimum, Maximum, and Value from RangeBase. Those properties are always kept in a legal state, and the effective value is what drives the filled geometry, the template-part layout, and the read-only automation surface.

Lower and upper bounds

Minimum defaults to 0 and Maximum defaults to 100 for ProgressBar. If the range would become invalid, coercion keeps the effective bounds legal.

Current value

Value defaults to 0 and is clamped back into the effective range whenever it changes directly or because the bounds changed around it.

Horizontal mapping

For a horizontal bar, the normalized value fills from left to right.

Vertical mapping

For a vertical bar, the normalized value fills from bottom to top so lower values stay anchored near the base of the control.

<ProgressBar Width="240"
             Minimum="0"
             Maximum="1"
             Value="0.42" />

That bar renders at roughly forty-two percent of the available track length. If Value is later assigned to 2, the visible fill still resolves to the effective maximum.

How the control renders

ProgressBar has two rendering modes. Determinate mode maps the effective range value into a visible fill segment. Indeterminate mode ignores percentage display and advances an animated segment each frame across the resolved track area.

Determinate mode

When IsIndeterminate is false, the control computes a normalized value from the range and turns that into indicator width or height.

Indeterminate mode

When IsIndeterminate is true, the control advances an internal phase value and repositions the indicator and glow on each frame update.

Passive behavior

The control sets IsHitTestVisible to false by default. It reports status only and does not expose pointer or keyboard interaction paths for changing Value.

Value is still meaningful

Even in indeterminate mode, the range properties remain valid state. The bar simply stops drawing a percentage-based fill and shows the activity animation instead.

<StackPanel>
  <TextBlock Text="Upload progress" Margin="0,0,0,6" />
  <ProgressBar Width="260"
               Minimum="0"
               Maximum="100"
               Value="72" />

  <TextBlock Text="Waiting for workers" Margin="0,14,0,6" />
  <ProgressBar Width="260"
               Height="10"
               IsIndeterminate="True" />
</StackPanel>

This is the common production pattern: show a determinate bar whenever the system can provide a percentage, and switch to indeterminate only when the work is active but not yet measurable.

Template structure and visual states

ProgressBar is a templated control. The shipped theme uses named parts plus template-root visual states. When those parts exist, the runtime sizes and repositions them directly during arrange and per-frame indeterminate updates. When they do not exist, the control falls back to its direct renderer instead of throwing.

Core parts

PART_Track hosts the moving geometry, and PART_Indicator is the actual filled segment.

Optional glow

PART_GlowRect is optional. The default theme uses it for the indeterminate glow pass, but the control still works without it.

CommonStates

The default template switches between Determinate and Indeterminate using template-root visual states.

ValidationStates

The default theme also exposes Valid, InvalidFocused, and InvalidUnfocused so validation chrome can change without changing range behavior.

  • PART_Track supplies the geometry used for both percentage fill and indeterminate travel.
  • PART_Indicator is resized and repositioned directly by the control.
  • Template visual states resolve against the applied template root, so setters can target named parts in the template tree.
  • Template teardown clears active visual-state-owned values when the control leaves a state or swaps templates.

Template example

<ControlTemplate TargetType="{x:Type ProgressBar}">
  <Border x:Name="RootChrome">
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="CommonStates">
        <VisualState x:Name="Determinate" />
        <VisualState x:Name="Indeterminate">
          <VisualState.Setters>
            <Setter TargetName="PART_GlowRect" Property="Opacity" Value="0.8" />
          </VisualState.Setters>
        </VisualState>
      </VisualStateGroup>
      <VisualStateGroup x:Name="ValidationStates">
        <VisualState x:Name="Valid" />
        <VisualState x:Name="InvalidFocused" />
        <VisualState x:Name="InvalidUnfocused" />
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <Grid x:Name="PART_Track">
      <Border x:Name="PART_Indicator" />
      <Border x:Name="PART_GlowRect" Opacity="0" />
    </Grid>
  </Border>
</ControlTemplate>

Layout and sizing

ProgressBar participates in normal layout, but its fallback renderer maintains a few minimum expectations so a standalone bar remains legible even when the parent does not constrain it tightly.

Fallback desired size

Without template-part layout taking over, unconstrained horizontal bars ask for at least 120 units of width and 18 units of height.

Vertical desired size

Without template-part layout taking over, unconstrained vertical bars ask for at least 18 units of width and 120 units of height.

Border thickness

BorderThickness is a single float on ProgressBar, not a full Thickness. It defaults to 1 and is coerced to zero or above.

Template-part sync

When template parts are present, the control updates their margin, width, height, and arrange state whenever range values, orientation, or indeterminate phase changes.

<StackPanel Orientation="Horizontal">
  <ProgressBar Width="240"
               Height="18"
               Minimum="0"
               Maximum="100"
               Value="35" />

  <ProgressBar Width="18"
               Height="180"
               Margin="16,0,0,0"
               Orientation="Vertical"
               Minimum="0"
               Maximum="100"
               Value="65" />
</StackPanel>

Property and event reference

Range surface

Minimum: lower bound of the range. Default: 0.

Maximum: upper bound of the range. Default: 100.

Value: current effective progress value. Default: 0.

Orientation: Horizontal or Vertical. Default: Horizontal.

Display behavior

IsIndeterminate: switches from normalized fill to frame-driven activity animation. Default: false.

IsHitTestVisible: set to false by the control constructor so the bar remains passive by default.

ValueChanged: inherited routed event raised when the effective value changes.

Appearance

Background: background fill color. Default: (24, 24, 24).

Foreground: indicator fill color. Default: (72, 146, 210).

BorderBrush: outline color. Default: (98, 98, 98).

BorderThickness: single float outline thickness. Default: 1.

Automation and validation

The automation peer exposes RangeValue as a read-only pattern.

Validation.HasError and focus state feed the template's validation visual states.

The default theme expects validation chrome to be driven by state setters, not by direct range changes.

Patterns and examples

Task completion bar

<StackPanel>
  <TextBlock Text="Exporting assets" Margin="0,0,0,6" />
  <ProgressBar Width="300"
               Minimum="0"
               Maximum="500"
               Value="315" />
</StackPanel>

Compact vertical meters

<StackPanel Orientation="Horizontal">
  <ProgressBar Width="10" Height="120" Orientation="Vertical" Value="15" Maximum="100" />
  <ProgressBar Width="10" Height="120" Orientation="Vertical" Margin="8,0,0,0" Value="55" Maximum="100" />
  <ProgressBar Width="10" Height="120" Orientation="Vertical" Margin="8,0,0,0" Value="88" Maximum="100" />
</StackPanel>

Indeterminate worker status

<Border Padding="12"
        Background="#101923"
        BorderBrush="#334A61"
        BorderThickness="1">
  <StackPanel>
    <TextBlock Text="Polling remote workers" Margin="0,0,0,8" />
    <ProgressBar Height="10"
                 IsIndeterminate="True"
                 Foreground="#F0B45A"
                 Background="#223344" />
  </StackPanel>
</Border>

Validation-aware template usage

<ProgressBar Width="260"
             Height="12"
             Minimum="0"
             Maximum="100"
             Value="40"
             Foreground="{StaticResource OrangePrimaryBrush}"
             Background="{StaticResource DarkElevatedBrush}" />

When the control enters an invalid state, the default template switches its validation visual state without changing the range value itself.

Code-driven setup

var progressBar = new ProgressBar
{
    Width = 280f,
    Height = 18f,
    Minimum = 0f,
    Maximum = 1f,
    Value = 0f,
    Background = new Color(24, 24, 24),
    Foreground = new Color(72, 146, 210),
    BorderBrush = new Color(98, 98, 98),
    BorderThickness = 1f
};

progressBar.ValueChanged += (_, _) =>
{
    statusText.Text = $"{progressBar.Value:P0}";
};

progressBar.Value = 0.58f;

Switching between measurable and unknown work

progressBar.IsIndeterminate = true;

// Later, once the backend reports real progress:
progressBar.IsIndeterminate = false;
progressBar.Minimum = 0f;
progressBar.Maximum = totalSteps;
progressBar.Value = completedSteps;

Notes and pitfalls

ProgressBar is intentionally passive. It inherits from RangeBase, but unlike Slider it does not use drag, click, or keyboard stepping to change the value.

IsIndeterminate changes display, not stored range state. The control still keeps Minimum, Maximum, and Value coherent even while the activity animation is active.

Custom templates should preserve the visual-state contract if they want the shipped behavior. Keep PART_Track, PART_Indicator, and the CommonStates/ValidationStates groups if you want determinate fill, indeterminate glow, and validation chrome to behave like the default theme.

Background, Foreground, and BorderBrush are colors, not brush objects. Theme setters and examples for this control should resolve to Color values.

Avoid assigning conflicting local values to state-owned template properties. If a custom template wants a visual state to control glow opacity or validation chrome, do not also hard-code competing local values on the same target property.

Set an explicit length on the progress axis for standalone bars. A horizontal bar is easiest to reason about with a clear Width; a vertical bar is easiest with a clear Height.