Controls

ScrollViewer

ScrollViewer is a single-content viewport container. It measures oversized content, exposes scroll offsets and viewport metrics, clips the visible region, and shows built-in horizontal and vertical scroll bars when the configured visibility rules require them.

Quick start

Use ScrollViewer when you have one content root that may be larger than the available area. In practice, that usually means placing a panel such as StackPanel, Grid, or Canvas inside the viewer and putting the rest of your UI under that panel.

Minimal vertical scroller

<ScrollViewer Width="280"
              Height="180"
              VerticalScrollBarVisibility="Auto">
  <StackPanel>
    <TextBlock Text="Line 1" />
    <TextBlock Text="Line 2" />
    <TextBlock Text="Line 3" />
    <TextBlock Text="Line 4" />
    <TextBlock Text="Line 5" />
    <TextBlock Text="Line 6" />
    <TextBlock Text="Line 7" />
  </StackPanel>
</ScrollViewer>

Scrollable form body with visible border chrome

<ScrollViewer Width="420"
              Height="260"
              Background="#121C27"
              BorderBrush="#35516B"
              BorderThickness="1"
              VerticalScrollBarVisibility="Auto">
  <StackPanel>
    <TextBlock Text="Profile" Margin="0,0,0,12" />
    <TextBox Text="{Binding DisplayName}" Margin="0,0,0,8" />
    <TextBox Text="{Binding Email}" Margin="0,0,0,8" />
    <TextBox Text="{Binding Department}" Margin="0,0,0,8" />
    <TextBox Text="{Binding TimeZone}" Margin="0,0,0,8" />
    <TextBox Text="{Binding Notes}" Height="120" />
  </StackPanel>
</ScrollViewer>

Two-axis scrolling for a wide surface

<ScrollViewer Width="420"
              Height="220"
              HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto">
  <Canvas Width="900" Height="600">
    <Border Canvas.Left="24"
            Canvas.Top="24"
            Width="220"
            Height="120"
            Background="#193149"
            BorderBrush="#4C81B0"
            BorderThickness="1" />
  </Canvas>
</ScrollViewer>

Default behavior: HorizontalScrollBarVisibility defaults to Disabled and VerticalScrollBarVisibility defaults to Auto. If you need horizontal scrolling, opt into it explicitly.

Content model

ScrollViewer is a content control. It hosts one content root, not multiple direct children. If you need to scroll several elements together, place them inside a panel and make that panel the viewer's content.

Recommended

Use one inner layout container such as StackPanel, Grid, or Canvas, then place the actual child elements inside that container.

Not recommended

Do not think of ScrollViewer as a panel with multiple direct child slots. It is a viewport wrapped around a single content tree.

Explicit content property form

<ScrollViewer VerticalScrollBarVisibility="Auto">
  <ScrollViewer.Content>
    <StackPanel>
      <TextBlock Text="Alpha" />
      <TextBlock Text="Bravo" />
      <TextBlock Text="Charlie" />
    </StackPanel>
  </ScrollViewer.Content>
</ScrollViewer>

How layout works

The viewer computes a content viewport inside its own border, decides whether each scroll bar needs space, measures the hosted content, and then exposes the resulting metrics through the offset, extent, and viewport properties.

Measure

If scrolling is allowed on an axis, the content is measured with an effectively unbounded size on that axis. If scrolling is disabled, the content is measured against the current viewport size on that axis.

Arrange

The content is arranged inside the viewport, the offsets are clamped to the valid range, and the internal scroll bars are arranged in their reserved bands.

  • ExtentWidth and ExtentHeight describe the full measured content size.
  • ViewportWidth and ViewportHeight describe the visible content region after chrome and scroll bar reservations are applied.
  • HorizontalOffset and VerticalOffset are clamped to the scrollable range: 0 through Extent - Viewport.
  • When a scroll bar is visible, the viewer reserves that band plus a small gutter between the content and the bar.
<ScrollViewer Width="320"
              Height="180"
              BorderThickness="1"
              HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto">
  <Canvas Width="640" Height="360" />
</ScrollViewer>

In that example, the visible viewport is smaller than 320x180 because the border and any visible scroll bars reduce the content area.

Scroll bar visibility modes

The ScrollBarVisibility enum controls both whether an axis can scroll and whether a bar is shown for that axis.

Disabled

The content is constrained to the viewport on that axis and no scroll bar is shown. Use this when the axis should not scroll at all.

Hidden

The axis remains scrollable, but the scroll bar is not shown. This is useful when you want wheel or programmatic scrolling without visible chrome.

Auto

The viewer shows the bar only when the content extent is larger than the available viewport on that axis.

Visible

The bar is always shown and always reserves space, even if the content currently fits without scrolling.

Vertical scrolling without visible chrome

<ScrollViewer Width="320"
              Height="180"
              VerticalScrollBarVisibility="Hidden">
  <StackPanel>
    <TextBlock Text="Timeline item 1" />
    <TextBlock Text="Timeline item 2" />
    <TextBlock Text="Timeline item 3" />
    <TextBlock Text="Timeline item 4" />
    <TextBlock Text="Timeline item 5" />
    <TextBlock Text="Timeline item 6" />
    <TextBlock Text="Timeline item 7" />
    <TextBlock Text="Timeline item 8" />
  </StackPanel>
</ScrollViewer>

User interaction

ScrollViewer supports wheel scrolling, drag interaction on the built-in scroll bar thumbs, and track clicks on visible scroll bars.

Mouse wheel

The mouse wheel changes VerticalOffset by LineScrollAmount per input step. The default line amount is 24.

Thumb drag

When a scroll bar is visible, dragging the thumb moves the corresponding offset continuously within the valid extent.

Track click

Clicking on the track outside the thumb pages the offset toward the clicked position.

  • Wheel scrolling clamps automatically at the start and end of the content.
  • When the wheel actually changes the offset, hover state is refreshed for the element now under the pointer.
  • If the content is already at the limit and the offset does not change, the viewer does not force a hover transition.

Adjusting wheel speed

<ScrollViewer Width="320"
              Height="180"
              LineScrollAmount="64"
              VerticalScrollBarVisibility="Auto">
  <StackPanel>
        <Button Content="Button 1" Height="44" />
        <Button Content="Button 2" Height="44" />
        <Button Content="Button 3" Height="44" />
        <Button Content="Button 4" Height="44" />
        <Button Content="Button 5" Height="44" />
        <Button Content="Button 6" Height="44" />
  </StackPanel>
</ScrollViewer>

Programmatic scrolling

The viewer exposes runtime scroll state through its properties and provides methods for changing offsets in code. Offsets are clamped automatically, so calls beyond the valid range settle at the nearest legal value.

Read state

Use HorizontalOffset, VerticalOffset, ExtentWidth, ExtentHeight, ViewportWidth, and ViewportHeight to inspect the current scroll metrics after layout has run.

Change state

Use ScrollToHorizontalOffset(float) and ScrollToVerticalOffset(float) to move the viewport. Use InvalidateScrollInfo() when custom content logic needs the viewer to recompute its metrics.

var viewer = new ScrollViewer
{
    Width = 320f,
    Height = 200f,
    VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
    Content = BuildInspectorPanel()
};

viewer.ScrollToVerticalOffset(180f);

var visibleBottom = viewer.VerticalOffset + viewer.ViewportHeight;
var maxVertical = MathF.Max(0f, viewer.ExtentHeight - viewer.ViewportHeight);

Important: the metric properties are most useful after measure and arrange have completed. Before the first layout pass, their values can still be zero.

Transform-based content scrolling

For direct panel content, ScrollViewer usually scrolls by applying a local render transform instead of re-arranging the panel for every offset-only change. This keeps scrolling lightweight while preserving the same visible viewport.

Default path

If the direct content root is a panel such as StackPanel, Grid, Canvas, WrapPanel, or UniformGrid, transform-based scrolling is enabled by default.

Fallback path

If the direct content root is not a supported panel host, or if the content is a VirtualizingStackPanel, the viewer falls back to arrange-based scrolling.

  • The attached property ScrollViewer.UseTransformContentScrolling applies to the direct content host element.
  • Setting that property to False opts the direct content host out of transform-based scrolling and makes the viewer move the content through arrange instead.
  • The opt-out only applies to the direct content root under the viewer, not to deeper descendants.

Default transform-scrolling panel

<ScrollViewer Width="320"
              Height="180"
              VerticalScrollBarVisibility="Auto">
  <StackPanel>
    <TextBlock Text="Rendered via transform scrolling" />
    <TextBlock Text="Offset-only changes stay lightweight" />
    <TextBlock Text="Add enough content to overflow the viewport" />
  </StackPanel>
</ScrollViewer>

Opting out of transform scrolling

<ScrollViewer Width="320"
              Height="180"
              VerticalScrollBarVisibility="Auto">
  <StackPanel ScrollViewer.UseTransformContentScrolling="False">
    <TextBlock Text="This host panel is arranged with the offset applied." />
    <TextBlock Text="Use this when you explicitly need arrange-based movement." />
  </StackPanel>
</ScrollViewer>

Appearance and chrome

ScrollViewer paints its own background and border, then renders the internal scroll bars inside the reserved viewer area. The content viewport is the remaining space after chrome and bar reservations are applied.

Viewer chrome

Background fills the full viewer slot. BorderBrush and BorderThickness draw a rectangular outline around the control.

Scroll bar size

ScrollBarThickness controls the reserved thickness for the internal bars. The runtime enforces a practical minimum thickness of 8.

<ScrollViewer Width="360"
              Height="220"
              Background="#101821"
              BorderBrush="#40607F"
              BorderThickness="1"
              ScrollBarThickness="16"
              VerticalScrollBarVisibility="Visible">
  <StackPanel>
    <TextBlock Text="Styled viewer chrome" />
    <TextBlock Text="Scrollbar always present" />
    <TextBlock Text="Thicker interaction rail" />
    <TextBlock Text="Add more content here" />
    <TextBlock Text="to make the thumb meaningful" />
  </StackPanel>
</ScrollViewer>

Property and method reference

Scrolling configuration

HorizontalScrollBarVisibility: Disabled, Hidden, Auto, or Visible. Default: Disabled.

VerticalScrollBarVisibility: Disabled, Hidden, Auto, or Visible. Default: Auto.

LineScrollAmount: wheel step size in device-independent layout units. Default: 24.

ScrollBarThickness: thickness used by the built-in bars. Default: 12.

Runtime metrics

HorizontalOffset and VerticalOffset: current scroll position, read from the control and changed through methods.

ExtentWidth and ExtentHeight: total measured content size.

ViewportWidth and ViewportHeight: visible content area after layout.

Chrome

Background: a Color used to fill the viewer.

BorderBrush: a Color used for the rectangular outline.

BorderThickness: a single float value for the outline thickness.

Padding: inherited from the base control surface, but the current ScrollViewer layout logic does not use it when computing the viewport.

Methods and attached property

ScrollToHorizontalOffset(float): moves the horizontal viewport.

ScrollToVerticalOffset(float): moves the vertical viewport.

InvalidateScrollInfo(): requests metric recomputation.

ScrollViewer.UseTransformContentScrolling: attached property for the direct content host element.

Patterns and examples

Scrollable diagnostics panel

<Border Background="#0F1620"
        BorderBrush="#29435A"
        BorderThickness="1"
        Padding="8">
  <ScrollViewer Height="220"
                VerticalScrollBarVisibility="Auto">
    <StackPanel>
      <TextBlock Text="Build started..." />
      <TextBlock Text="Loading assets..." />
      <TextBlock Text="Compiling shaders..." />
      <TextBlock Text="Indexing content..." />
      <TextBlock Text="Diagnostics continue below..." />
    </StackPanel>
  </ScrollViewer>
</Border>

Large canvas workspace

<ScrollViewer Width="520"
              Height="320"
              HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto"
              Background="#0D141C"
              BorderBrush="#37536E"
              BorderThickness="1">
  <Canvas Width="1400" Height="900">
    <Border Canvas.Left="80"
            Canvas.Top="60"
            Width="260"
            Height="180"
            Background="#16304A"
            BorderBrush="#5B97CE"
            BorderThickness="1" />
  </Canvas>
</ScrollViewer>

Programmatic jump to a section

private void ShowValidationSummary()
{
    SummaryScrollViewer.ScrollToVerticalOffset(0f);
}

private void ShowLatestEntry()
{
    var bottom = MathF.Max(
        0f,
        SummaryScrollViewer.ExtentHeight - SummaryScrollViewer.ViewportHeight);

    SummaryScrollViewer.ScrollToVerticalOffset(bottom);
}

Host a virtualizing list

<ScrollViewer Width="320"
              Height="240"
              VerticalScrollBarVisibility="Auto">
  <VirtualizingStackPanel IsVirtualizing="True"
                          CacheLength="0.5"
                          CacheLengthUnit="Page">
    <!-- items -->
  </VirtualizingStackPanel>
</ScrollViewer>

This is the right pattern when you want a large list surface that still refreshes realization as the offset changes.

Notes and pitfalls

Background and BorderBrush are colors, not brush objects. Examples and styles for this control should use values that resolve to Color.

BorderThickness is a single float. Unlike the Border control, ScrollViewer does not expose per-side border thickness.

Padding is not part of the current viewport calculation. The property exists on the inherited control surface, but this control's layout code does not presently subtract it from the content area.

Horizontal scrolling is off by default. If your content can overflow horizontally, set HorizontalScrollBarVisibility to Auto, Hidden, or Visible.

Put multiple elements inside an inner panel. The viewer scrolls one content root; it is not the right place for multiple direct sibling elements.

The transform-scrolling opt-out belongs on the direct content host. Setting ScrollViewer.UseTransformContentScrolling="False" on a nested descendant does not disable transform scrolling for the outer content root.

Read viewport metrics after layout. If you inspect ExtentHeight or ViewportHeight before the first measure and arrange pass, they may still be zero.