diff --git a/doc/controls/ResponsiveView.md b/doc/controls/ResponsiveView.md
new file mode 100644
index 000000000..2190efd82
--- /dev/null
+++ b/doc/controls/ResponsiveView.md
@@ -0,0 +1,133 @@
+---
+uid: Toolkit.Controls.ResponsiveView
+---
+# ResponsiveView
+
+## Summary
+
+The `ResponsiveView` provides the ability to display different content based on screen size, making it easy to adapt to various devices.
+
+## Remarks
+
+The ResponsiveView Control adapts to different screen sizes by dynamically choosing the right template. It looks at the current screen width and the defined templates. Since not all templates need a value, the control ensures a smooth user experience by picking the smallest defined template that satisfies the width requirements. If no match is found, it defaults to the largest defined template.
+
+**Initialization**: The `ResponsiveHelper` needs to be hooked up to the window's `SizeChanged` event in order for it to receive updates when the window size changes.
+This is typically done in the `OnLaunched` method in the `App` class, where you can get the current window and call the `HookupEvent` method on the `ResponsiveHelper`.
+
+Here is an example of how this might be achieved:
+
+```cs
+protected override void OnLaunched(LaunchActivatedEventArgs args)
+{
+#if NET6_0_OR_GREATER && WINDOWS && !HAS_UNO
+ MainWindow = new Window();
+#else
+ MainWindow = Microsoft.UI.Xaml.Window.Current;
+#endif
+ // ...
+ var helper = Uno.Toolkit.UI.ResponsiveHelper.GetForCurrentView();
+ helper.HookupEvent(MainWindow);
+}
+```
+
+## Inheritance
+Object → DependencyObject → UIElement → FrameworkElement → Control → ContentControl
+
+## Properties
+| Property | Type | Description |
+| ----------------- | ---------------- | ------------------------------------------------------- |
+| NarrowestTemplate | DataTemplate | Template to be displayed on the narrowest screen size. |
+| NarrowTemplate | DataTemplate | Template to be displayed on a narrow screen size. |
+| NormalTemplate | DataTemplate | Template to be displayed on a normal screen size. |
+| WideTemplate | DataTemplate | Template to be displayed on a wide screen size. |
+| WidestTemplate | DataTemplate | Template to be displayed on the widest screen size. |
+| ResponsiveLayout | ResponsiveLayout | Overrides the screen size threshold/breakpoints. |
+
+### ResponsiveLayout
+Provides the ability to override the MaxWidth for each screen size: `Narrowest`, `Narrow`, `Normal`, `Wide`, and `Widest`.
+
+### Properties
+| Property | Type | Description |
+| ---------- | ------ | ---------------------- |
+| Narrowest | double | Default value is 150. |
+| Narrow | double | Default value is 300. |
+| Normal | double | Default value is 600. |
+| Wide | double | Default value is 800. |
+| Widest | double | Default value is 1080. |
+
+## Usage
+
+```xml
+xmlns:utu="using:Uno.Toolkit.UI"
+...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Using `ResponsiveLayout` to customize the screen size breakpoints.
+
+```xml
+xmlns:utu="using:Uno.Toolkit.UI"
+...
+
+
+
+ 200
+ 350
+ 800
+ 1200
+ 1500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
\ No newline at end of file
diff --git a/doc/toc.yml b/doc/toc.yml
index 257cc6901..24b0836f3 100644
--- a/doc/toc.yml
+++ b/doc/toc.yml
@@ -38,6 +38,8 @@
href: controls/LoadingView.md
- name: NavigationBar
href: controls/NavigationBar.md
+ - name: ResponsiveView
+ href: controls/ResponsiveView.md
- name: SafeArea
href: controls/SafeArea.md
- name: ShadowContainer
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.cs
index 80a00535d..fcfe1abff 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.cs
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/App.xaml.cs
@@ -98,6 +98,8 @@ protected override async void OnLaunched(XamlLaunchActivatedEventArgs e)
#else
_window = XamlWindow.Current;
#endif
+ var helper = ResponsiveHelper.GetForCurrentView();
+ helper.HookupEvent(_window);
if (_window.Content is null)
{
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ResponsiveViewSamplePage.xaml b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ResponsiveViewSamplePage.xaml
new file mode 100644
index 000000000..653022291
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ResponsiveViewSamplePage.xaml
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+ #67E5AD
+ #7A67F8
+ #F85977
+ #159BFF
+
+
+
+
+ 200
+ 350
+ 800
+ 1200
+ 1500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ResponsiveViewSamplePage.xaml.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ResponsiveViewSamplePage.xaml.cs
new file mode 100644
index 000000000..08bb58614
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/ResponsiveViewSamplePage.xaml.cs
@@ -0,0 +1,18 @@
+using Uno.Toolkit.Samples.Entities;
+#if IS_WINUI
+using Microsoft.UI.Xaml.Controls;
+#else
+using Windows.UI.Xaml.Controls;
+#endif
+
+namespace Uno.Toolkit.Samples.Content.Controls
+{
+ [SamplePage(SampleCategory.Controls, "ResponsiveView")]
+ public sealed partial class ResponsiveViewSamplePage : Page
+ {
+ public ResponsiveViewSamplePage()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
index 12f87b4e6..344949c4e 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
@@ -25,6 +25,9 @@
AutoLayoutPage.xaml
+
+ ResponsiveViewSamplePage.xaml
+
CardSamplePage.xaml
@@ -221,6 +224,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/src/Uno.Toolkit.RuntimeTests/Tests/ResponsiveViewTests.cs b/src/Uno.Toolkit.RuntimeTests/Tests/ResponsiveViewTests.cs
new file mode 100644
index 000000000..bec047369
--- /dev/null
+++ b/src/Uno.Toolkit.RuntimeTests/Tests/ResponsiveViewTests.cs
@@ -0,0 +1,204 @@
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Uno.UI.RuntimeTests;
+using Uno.Toolkit.RuntimeTests.Helpers;
+using Windows.Foundation;
+using Uno.Toolkit.UI;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Shapes;
+#else
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Shapes;
+#endif
+
+namespace Uno.Toolkit.RuntimeTests.Tests;
+
+[TestClass]
+[RunsOnUIThread]
+internal class ResponsiveViewTests
+{
+ [TestMethod]
+ public async Task ResponsiveView_NarrowContent_TextBlock()
+ {
+ using (ResponsiveHelper.UsingDebuggableInstance())
+ {
+ ResponsiveHelper.SetDebugSize(new Size(300, 400));
+
+ var host = XamlHelper.LoadXaml("""
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(host);
+
+ var element = (TextBlock)host.Content;
+
+ Assert.AreEqual("Narrow", element.Text);
+ }
+ }
+
+ [TestMethod]
+ public async Task ResponsiveView_NormalContent_Rectangle()
+ {
+ using (ResponsiveHelper.UsingDebuggableInstance())
+ {
+ ResponsiveHelper.SetDebugSize(new Size(599, 400));
+
+ var host = XamlHelper.LoadXaml("""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(host);
+
+ Assert.AreEqual(typeof(Rectangle), host.Content.GetType());
+ }
+ }
+
+ [TestMethod]
+ public async Task ResponsiveView_NormalContent_ResponsiveLayout()
+ {
+ using (ResponsiveHelper.UsingDebuggableInstance())
+ {
+ ResponsiveHelper.SetDebugSize(new Size(322, 400));
+
+ var host = XamlHelper.LoadXaml("""
+
+
+
+ 350
+ 450
+ 800
+ 1200
+ 1500
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(host);
+
+ Assert.AreEqual(typeof(Ellipse), host.Content.GetType());
+ }
+ }
+
+ [TestMethod]
+ public async Task ResponsiveView_WidestContent_Ellipse()
+ {
+ using (ResponsiveHelper.UsingDebuggableInstance())
+ {
+ ResponsiveHelper.SetDebugSize(new Size(2000, 400));
+
+ var host = XamlHelper.LoadXaml("""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(host);
+
+ Assert.AreEqual(typeof(Ellipse), host.Content.GetType());
+ }
+ }
+
+ [TestMethod]
+ public async Task ResponsiveView_WideContent_SizeChanged()
+ {
+ using (ResponsiveHelper.UsingDebuggableInstance())
+ {
+ ResponsiveHelper.SetDebugSize(new Size(150, 400));
+
+ var host = XamlHelper.LoadXaml("""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+
+ await UnitTestUIContentHelperEx.SetContentAndWait(host);
+ Assert.AreEqual(typeof(TextBlock), host.Content.GetType());
+
+ ResponsiveHelper.SetDebugSize(new Size(800, 400));
+ Assert.AreEqual(typeof(TextBox), host.Content.GetType());
+ }
+ }
+}
diff --git a/src/Uno.Toolkit.UI/Controls/ResponsiveView/ResponsiveView.cs b/src/Uno.Toolkit.UI/Controls/ResponsiveView/ResponsiveView.cs
new file mode 100644
index 000000000..b4fd23fa5
--- /dev/null
+++ b/src/Uno.Toolkit.UI/Controls/ResponsiveView/ResponsiveView.cs
@@ -0,0 +1,163 @@
+using System.Linq;
+using Windows.Foundation;
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#endif
+
+namespace Uno.Toolkit.UI;
+
+public partial class ResponsiveView : ContentControl, IResponsiveCallback
+{
+ #region DependencyProperties
+
+ #region Narrowest DP
+ public DataTemplate NarrowestTemplate
+ {
+ get { return (DataTemplate)GetValue(NarrowestTemplateProperty); }
+ set { SetValue(NarrowestTemplateProperty, value); }
+ }
+
+ public static readonly DependencyProperty NarrowestTemplateProperty =
+ DependencyProperty.Register("NarrowestTemplate", typeof(DataTemplate), typeof(ResponsiveView), new PropertyMetadata(null, OnNarrowestTemplateChanged));
+
+ private static void OnNarrowestTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ => OnResponsiveTemplateChanged(d, e);
+
+ #endregion
+
+ #region Narrow DP
+ public DataTemplate NarrowTemplate
+ {
+ get { return (DataTemplate)GetValue(NarrowTemplateProperty); }
+ set { SetValue(NarrowTemplateProperty, value); }
+ }
+
+ public static readonly DependencyProperty NarrowTemplateProperty =
+ DependencyProperty.Register("NarrowTemplate", typeof(DataTemplate), typeof(ResponsiveView), new PropertyMetadata(null, OnNarrowTemplateChanged));
+
+ private static void OnNarrowTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ => OnResponsiveTemplateChanged(d, e);
+ #endregion
+
+ #region Normal DP
+ public DataTemplate NormalTemplate
+ {
+ get { return (DataTemplate)GetValue(NormalTemplateProperty); }
+ set { SetValue(NormalTemplateProperty, value); }
+ }
+
+ public static readonly DependencyProperty NormalTemplateProperty =
+ DependencyProperty.Register("NormalTemplate", typeof(DataTemplate), typeof(ResponsiveView), new PropertyMetadata(null, OnNormalTemplateChanged));
+
+ private static void OnNormalTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ => OnResponsiveTemplateChanged(d, e);
+ #endregion
+
+ #region Wide DP
+ public DataTemplate WideTemplate
+ {
+ get { return (DataTemplate)GetValue(WideTemplateProperty); }
+ set { SetValue(WideTemplateProperty, value); }
+ }
+
+ public static readonly DependencyProperty WideTemplateProperty =
+ DependencyProperty.Register("WideTemplate", typeof(DataTemplate), typeof(ResponsiveView), new PropertyMetadata(null, OnWideTemplateChanged));
+
+ private static void OnWideTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ => OnResponsiveTemplateChanged(d, e);
+ #endregion
+
+ #region Widest DP
+ public DataTemplate WidestTemplate
+ {
+ get { return (DataTemplate)GetValue(WidestTemplateProperty); }
+ set { SetValue(WidestTemplateProperty, value); }
+ }
+
+ public static readonly DependencyProperty WidestTemplateProperty =
+ DependencyProperty.Register("WidestTemplate", typeof(DataTemplate), typeof(ResponsiveView), new PropertyMetadata(null, OnWidestTemplateChanged));
+
+ private static void OnWidestTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ => OnResponsiveTemplateChanged(d, e);
+ #endregion
+
+ #region ResponsiveLayout DP
+ public static DependencyProperty ResponsiveLayoutProperty { get; } = DependencyProperty.Register(
+ nameof(ResponsiveLayout),
+ typeof(ResponsiveLayout),
+ typeof(ResponsiveView),
+ new PropertyMetadata(default));
+
+ public ResponsiveLayout ResponsiveLayout
+ {
+ get => (ResponsiveLayout)GetValue(ResponsiveLayoutProperty);
+ set => SetValue(ResponsiveLayoutProperty, value);
+ }
+ #endregion
+
+ private static void OnResponsiveTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ResponsiveView { IsLoaded: true } view)
+ {
+ var dataTemplate = view.GetInitialValue();
+ view.Content = dataTemplate?.LoadContent() as UIElement;
+ }
+ }
+ #endregion
+
+ private DataTemplate? _currentContent;
+
+ public ResponsiveView()
+ {
+ this.DefaultStyleKey = typeof(ResponsiveView);
+
+ ResponsiveHelper.GetForCurrentView().Register(this);
+
+ Loaded += ResponsiveView_Loaded;
+ }
+
+ private void ResponsiveView_Loaded(object sender, RoutedEventArgs e)
+ {
+ _currentContent = GetInitialValue();
+
+ Content = _currentContent?.LoadContent() as UIElement;
+ }
+
+ private DataTemplate? GetInitialValue()
+ {
+ var helper = ResponsiveHelper.GetForCurrentView();
+
+ return GetValueForSize(helper.WindowSize, ResponsiveLayout ?? helper.Layout);
+ }
+
+ private DataTemplate? GetValueForSize(Size size, ResponsiveLayout layout)
+ {
+ var defs = new (double MinWidth, DataTemplate? Value)?[]
+ {
+ (layout.Narrowest, NarrowestTemplate),
+ (layout.Narrow, NarrowTemplate),
+ (layout.Normal, NormalTemplate),
+ (layout.Wide, WideTemplate),
+ (layout.Widest, WidestTemplate),
+ }.Where(x => x?.Value != null).ToArray();
+
+ var match = defs.FirstOrDefault(y => y?.MinWidth >= size.Width) ?? defs.LastOrDefault();
+
+ return match?.Value;
+ }
+
+ public void OnSizeChanged(Size size, ResponsiveLayout layout)
+ {
+ var dataTemplate = GetValueForSize(size, ResponsiveLayout ?? layout);
+
+ if (dataTemplate != _currentContent)
+ {
+ _currentContent = dataTemplate;
+ Content = dataTemplate?.LoadContent() as UIElement;
+ }
+ }
+}
diff --git a/src/Uno.Toolkit.UI/Helpers/ResponsiveHelper.cs b/src/Uno.Toolkit.UI/Helpers/ResponsiveHelper.cs
new file mode 100644
index 000000000..925d53034
--- /dev/null
+++ b/src/Uno.Toolkit.UI/Helpers/ResponsiveHelper.cs
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System;
+using Windows.Foundation;
+using Uno.Disposables;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Core;
+#endif
+
+namespace Uno.Toolkit.UI;
+
+internal interface IResponsiveCallback
+{
+ void OnSizeChanged(Size size, ResponsiveLayout layout);
+}
+
+public partial class ResponsiveLayout : DependencyObject
+{
+ #region DependencyProperty: Narrowest
+
+ public static DependencyProperty NarrowestProperty { get; } = DependencyProperty.Register(
+ nameof(Narrowest),
+ typeof(double),
+ typeof(ResponsiveLayout),
+ new PropertyMetadata(0d));
+
+ public double Narrowest
+ {
+ get => (double)GetValue(NarrowestProperty);
+ set => SetValue(NarrowestProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: Narrow
+
+ public static DependencyProperty NarrowProperty { get; } = DependencyProperty.Register(
+ nameof(Narrow),
+ typeof(double),
+ typeof(ResponsiveLayout),
+ new PropertyMetadata(0d));
+
+ public double Narrow
+ {
+ get => (double)GetValue(NarrowProperty);
+ set => SetValue(NarrowProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: Normal
+
+ public static DependencyProperty NormalProperty { get; } = DependencyProperty.Register(
+ nameof(Normal),
+ typeof(double),
+ typeof(ResponsiveLayout),
+ new PropertyMetadata(0d));
+
+ public double Normal
+ {
+ get => (double)GetValue(NormalProperty);
+ set => SetValue(NormalProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: Wide
+
+ public static DependencyProperty WideProperty { get; } = DependencyProperty.Register(
+ nameof(Wide),
+ typeof(double),
+ typeof(ResponsiveLayout),
+ new PropertyMetadata(0d));
+
+ public double Wide
+ {
+ get => (double)GetValue(WideProperty);
+ set => SetValue(WideProperty, value);
+ }
+
+ #endregion
+ #region DependencyProperty: Widest
+
+ public static DependencyProperty WidestProperty { get; } = DependencyProperty.Register(
+ nameof(Widest),
+ typeof(double),
+ typeof(ResponsiveLayout),
+ new PropertyMetadata(0d));
+
+ public double Widest
+ {
+ get => (double)GetValue(WidestProperty);
+ set => SetValue(WidestProperty, value);
+ }
+
+ #endregion
+
+ public static ResponsiveLayout Create(double narrowest, double narrow, double normal, double wide, double widest) => new()
+ {
+ Narrowest = narrowest,
+ Narrow = narrow,
+ Normal = normal,
+ Wide = wide,
+ Widest = widest,
+ };
+}
+
+internal class ResponsiveHelper
+{
+ private static readonly Lazy _instance = new Lazy(() => new ResponsiveHelper());
+ private readonly List _references = new();
+ private static readonly ResponsiveHelper _debugInstance = new();
+ private static bool UseDebuggableInstance;
+
+ public ResponsiveLayout Layout { get; private set; } = ResponsiveLayout.Create(150, 300, 600, 800, 1080);
+ public Size WindowSize { get; private set; } = Size.Empty;
+
+ public static ResponsiveHelper GetForCurrentView() => UseDebuggableInstance ? _debugInstance : _instance.Value;
+
+ private ResponsiveHelper() { }
+
+ public void HookupEvent(Window window)
+ {
+ WindowSize = new Size(window.Bounds.Width, window.Bounds.Height);
+
+ window.SizeChanged += OnWindowSizeChanged;
+ }
+
+ internal static void SetDebugSize(Size size) => _debugInstance.OnWindowSizeChanged(size);
+ private void OnWindowSizeChanged(object sender, WindowSizeChangedEventArgs e) => OnWindowSizeChanged(e.Size);
+
+ private void OnWindowSizeChanged(Size size)
+ {
+ WindowSize = size;
+
+ _references.RemoveAll(reference => !reference.IsAlive);
+
+ foreach (var reference in _references.ToArray())
+ {
+ if (reference.IsAlive && reference.Target is IResponsiveCallback callback)
+ {
+ callback.OnSizeChanged(WindowSize, Layout);
+ }
+ }
+ }
+
+ internal void Register(IResponsiveCallback host)
+ {
+ var wr = new WeakReference(host);
+ _references.Add(wr);
+ }
+
+ internal static IDisposable UsingDebuggableInstance()
+ {
+ UseDebuggableInstance = true;
+
+ return Disposable.Create(() => UseDebuggableInstance = false);
+ }
+}