Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

ParallaxCollectionView

A scrollable collection with a hero header that translates at a configurable fraction of the scroll offset, producing the familiar “parallax” effect seen in app store and profile pages. Pure cross-platform implementation — no platform handlers on either host.

On MAUI the control is ParallaxCollectionView, a ContentView that wraps a real CollectionView plus a hero host and drives the translation from CollectionView.Scrolled. On Blazor it is ParallaxList<TItem>, a scrollable container with a JS scroll listener that mutates the hero transform directly (requestAnimationFrame-throttled) so the parallax runs at native scroll framerate without going through component re-renders.

  • NuGet downloads for Shiny.Maui.Controls
  • NuGet downloads for Shiny.Blazor.Controls
Frameworks
.NET MAUI
Blazor
  • Hero header that translates on scroll — Set HeaderTemplate (MAUI) / HeroTemplate (Blazor) to any view; it moves at ParallaxFactor × scroll offset (default 0.5 = half speed).
  • Collapse to sticky — Enable CollapseToSticky with a non-zero MinHeaderHeight and the hero stops translating once it reaches the minimum height, leaving a sticky strip pinned at the top of the list.
  • Fade on scroll — Enable FadeHeaderOnScroll to fade the hero out as it is scrolled past, useful when combining with a separate fixed title.
  • Real CollectionView underneath (MAUI) — Full ItemTemplate / ItemTemplateSelector / EmptyView / SelectionMode / SelectedItem / ScrollTo support; behaves like any MAUI CollectionView.
  • Scrolled event — Both hosts emit ParallaxScrollEventArgs(verticalOffset, headerTranslation, headerVisibleHeight) so you can drive secondary UI (a sticky title that appears once the hero collapses, a fading nav bar, etc.).
  • No platform code — MAUI uses only CollectionView + Grid + transforms. Blazor uses CSS + a tiny JS scroll listener (no Blazor render loop on scroll).
claude plugin marketplace add shinyorg/skills
claude plugin install shiny-client@shiny
BLE, GPS, Jobs, Notifications, Push, HTTP Transfers, OBD, Music, Health, DataSync — iOS, Android, Windows, MacOS, Linux, Web
claude plugin install shiny-maui@shiny
Shell, Contact Store
claude plugin install controls@shiny
TableView, BottomSheet, PillView, ImageViewer, Scheduler, Markdown, Mermaid Diagrams — MAUI and Blazor
claude plugin install shiny-mediator@shiny
Mediator/CQRS with middleware and source generators
claude plugin install shiny-data@shiny
DocumentDB and Spatial data libraries
claude plugin install shiny-aspire@shiny
Orleans and Gluetun Aspire integrations
claude plugin install shiny-extensions@shiny
DI, Stores, Reflector, Localization, Hosting modules
copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny-client@shiny
BLE, GPS, Jobs, Notifications, Push, HTTP Transfers, OBD, Music, Health, DataSync — iOS, Android, Windows, MacOS, Linux, Web
copilot plugin install shiny-maui@shiny
Shell, Contact Store
copilot plugin install controls@shiny
TableView, BottomSheet, PillView, ImageViewer, Scheduler, Markdown, Mermaid Diagrams — MAUI and Blazor
copilot plugin install shiny-mediator@shiny
Mediator/CQRS with middleware and source generators
copilot plugin install shiny-data@shiny
DocumentDB and Spatial data libraries
copilot plugin install shiny-aspire@shiny
Orleans and Gluetun Aspire integrations
copilot plugin install shiny-extensions@shiny
DI, Stores, Reflector, Localization, Hosting modules
View Skills Repository
<shiny:ParallaxCollectionView ItemsSource="{Binding Items}"
HeaderHeight="260"
MinHeaderHeight="96"
ParallaxFactor="0.5"
CollapseToSticky="True"
FadeHeaderOnScroll="False"
SelectionMode="Single"
ItemSelectedCommand="{Binding ItemSelectedCommand}">
<shiny:ParallaxCollectionView.HeaderTemplate>
<DataTemplate>
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#7C3AED" Offset="0.0" />
<GradientStop Color="#2563EB" Offset="0.5" />
<GradientStop Color="#0EA5E9" Offset="1.0" />
</LinearGradientBrush>
</Grid.Background>
<Label Text="Destinations"
FontSize="28"
FontAttributes="Bold"
TextColor="White"
VerticalOptions="Center"
HorizontalOptions="Center" />
</Grid>
</DataTemplate>
</shiny:ParallaxCollectionView.HeaderTemplate>
<shiny:ParallaxCollectionView.ItemTemplate>
<DataTemplate>
<Border Margin="16,6" Padding="16">
<Label Text="{Binding Title}" FontAttributes="Bold" />
</Border>
</DataTemplate>
</shiny:ParallaxCollectionView.ItemTemplate>
</shiny:ParallaxCollectionView>
<div style="height:600px;">
<ParallaxList TItem="DestinationItem"
Items="@items"
HeaderHeight="260"
MinHeaderHeight="96"
ParallaxFactor="0.5"
CollapseToSticky="true"
FadeHeaderOnScroll="false"
ItemSelected="OnSelected"
Scrolled="OnScrolled">
<HeroTemplate>
<div style="height:100%;background:linear-gradient(135deg,#7C3AED,#2563EB,#0EA5E9);
color:white;display:flex;align-items:center;justify-content:center;
font-size:28px;font-weight:700;">
Destinations
</div>
</HeroTemplate>
<ItemTemplate Context="item">
<div style="margin:6px 16px;padding:16px;background:white;border-radius:14px;">
<strong>@item.Title</strong>
</div>
</ItemTemplate>
</ParallaxList>
</div>
@code {
record DestinationItem(string Title);
List<DestinationItem> items = [];
void OnSelected(DestinationItem item) { }
void OnScrolled(ParallaxScrollEventArgs e) { /* e.HeaderVisibleHeight, etc. */ }
}

Blazor: the <ParallaxList> fills its parent. Place it in a container with a fixed height (px, vh, etc.) so it has something to scroll inside.

PropertyMAUI TypeBlazor TypeDefaultDescription
ItemsSource / ItemsIEnumerableIReadOnlyList<TItem>Collection of items to display
ItemTemplateDataTemplateRenderFragment<TItem>Template for each row
HeaderTemplate / HeroTemplateDataTemplateRenderFragmentTemplate for the parallax hero header
EmptyView / EmptyTemplateobject / DataTemplateRenderFragmentShown when the source is null or empty
HeaderHeightdoubledouble240Height of the hero header in px
MinHeaderHeightdoubledouble0Minimum visible height when CollapseToSticky is true
ParallaxFactordoubledouble0.5Fraction of the scroll offset applied to the hero translation (0 = pinned, 1 = scrolls with content)
CollapseToStickyboolboolfalseWhen true, the hero stops translating at MinHeaderHeight and stays pinned
FadeHeaderOnScrollboolboolfalseFade the hero from 100% → 0% opacity as it scrolls past
SelectionModeSelectionModeNoneMAUI only — passthrough to inner CollectionView
SelectedItemobjectMAUI only — TwoWay selected item
ItemSelectedCommandICommandMAUI only — fired on selection change
ItemSelectedEventCallback<TItem>Blazor only — fired on row click
HeightstringBlazor only — CSS height for the scroll container; omit to fill parent

Both hosts fire a Scrolled event with ParallaxScrollEventArgs:

  • VerticalOffset — current CollectionView/scroll-container scrollTop in px
  • HeaderTranslation — negative px translation currently applied to the hero (clamped if CollapseToSticky)
  • HeaderVisibleHeight — how many px of the hero are still visible (HeaderHeight + HeaderTranslation, floored at MinHeaderHeight)

Use this to drive a sticky title that appears once the hero is mostly hidden, fade a navigation chrome, or update progress indicators.

  • The hero is laid out at row 0 of the host grid (MAUI) / absolutely positioned at the top of the scroll container (Blazor). The list content sits below it via a transparent CollectionView header (MAUI) or a margin-top matching HeaderHeight (Blazor), so items scroll over the hero when it has been pushed out of view.
  • ParallaxFactor = 0 pins the hero in place (no parallax).
  • ParallaxFactor = 1 makes the hero scroll with the content (no parallax). Use this if you want a normal scrolling header.
  • ParallaxFactor = 0.5 (default) gives a smooth half-speed effect that reads as parallax on most screens.
  • CollapseToSticky requires MinHeaderHeight > 0 to be meaningful — set both together.
  • FadeHeaderOnScroll and CollapseToSticky are independent and can be combined: the hero collapses to its minimum height and then fades out.
  • MAUI: setting ItemsLayout (e.g. <GridItemsLayout Span="2" />) on the control passes through to the inner CollectionView, so the list portion can be a multi-column grid while the hero stays full-width.
  • Blazor: the scroll listener runs in JS and updates transform/opacity on the hero element directly using requestAnimationFrame, so 60 fps scroll behavior is preserved without re-rendering Razor components.
  • Wrap the hero in a Grid (MAUI) or <div> (Blazor) and use a gradient Background or CSS background instead of a static image — gradients render cheaply during the translation.
  • For the “sticky title that appears once the hero collapses” pattern, listen to Scrolled and toggle the visibility of a separate header element when HeaderVisibleHeight drops below a threshold (e.g. MinHeaderHeight + 8).
  • If you need a multi-column grid of items under the hero, set ItemsLayout (MAUI) to a GridItemsLayout. The hero remains a full-width header.