diff --git a/components/Ribbon/OpenSolution.bat b/components/Ribbon/OpenSolution.bat
new file mode 100644
index 000000000..814a56d4b
--- /dev/null
+++ b/components/Ribbon/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/Ribbon/samples/Assets/Ribbon.png b/components/Ribbon/samples/Assets/Ribbon.png
new file mode 100644
index 000000000..8435bcaa9
Binary files /dev/null and b/components/Ribbon/samples/Assets/Ribbon.png differ
diff --git a/components/Ribbon/samples/Dependencies.props b/components/Ribbon/samples/Dependencies.props
new file mode 100644
index 000000000..e622e1df4
--- /dev/null
+++ b/components/Ribbon/samples/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Ribbon/samples/Ribbon.Samples.csproj b/components/Ribbon/samples/Ribbon.Samples.csproj
new file mode 100644
index 000000000..082bdb735
--- /dev/null
+++ b/components/Ribbon/samples/Ribbon.Samples.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+ Ribbon
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/components/Ribbon/samples/Ribbon.md b/components/Ribbon/samples/Ribbon.md
new file mode 100644
index 000000000..20f80d107
--- /dev/null
+++ b/components/Ribbon/samples/Ribbon.md
@@ -0,0 +1,67 @@
+---
+title: Ribbon
+author: vgromfeld
+description: An office like ribbon.
+keywords: Ribbon, Control
+dev_langs:
+ - csharp
+category: Controls
+subcategory: Layout
+experimental: true
+discussion-id: 544
+issue-id: 545
+icon: Assets/Ribbon.png
+---
+
+# Ribbon
+
+An Office like Ribbon control which displays groups of commands. If there is not enough space to display all the groups,
+some of them can be collapsed based on a priority order.
+
+> [!Sample RibbonCustomSample]
+
+## RibbonGroup
+
+A basic group displayed in a Ribbon.
+It mostly adds a *label* to some content and will not collapse if there is not enough space available.
+
+
+## RibbonCollapsibleGroup
+
+A `RibbonGroup` which can be collapsed if its content does not fit in the available Ribbon's space.
+When collapsed, the group is displayed as a single icon button. Clicking on this button opens
+a flyout containing the group's content.
+
+### IconSource
+The icon to display when the group is collapsed.
+
+### AutoCloseFlyout
+Set to true to automatically close the overflow flyout if one interactive element is clicked.
+Note that the logic to detect the click is very basic. It will request the flyout to close
+for all the handled pointer released events. It assumes that if the pointer has been handled
+something reacted to it. It works well for buttons or check boxes but does not work for text
+or combo boxes.
+
+### Priority
+The priority of the group.
+The group with the lower priority will be the first one to be collapsed.
+
+### CollapsedAccessKey
+The access key to access the collapsed button and open the flyout when the group is collapsed.
+
+### RequestedWidths
+
+The list of requested widths for the group.
+If null or empty, the group will automatically use the size of its content.
+If set, the group will use the smallest provided width fitting in the ribbon.
+This is useful if the group contains a variable size control which can adjust
+its width (like a GridView with several items).
+
+### State
+The state of the group (collapsed or visible). This property is used by the `RibbonPanel`.
+
+## RibbonPanel
+
+The inner panel of the Ribbon control. It displays the groups inside the `Ribbon` and
+automatically collapse the `CollapsibleGroup` elements based on their priority order if
+there is not enough space available.
diff --git a/components/Ribbon/samples/RibbonCustomSample.xaml b/components/Ribbon/samples/RibbonCustomSample.xaml
new file mode 100644
index 000000000..e7d6a27bf
--- /dev/null
+++ b/components/Ribbon/samples/RibbonCustomSample.xaml
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Ribbon/samples/RibbonCustomSample.xaml.cs b/components/Ribbon/samples/RibbonCustomSample.xaml.cs
new file mode 100644
index 000000000..05c63b61c
--- /dev/null
+++ b/components/Ribbon/samples/RibbonCustomSample.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Controls;
+
+namespace RibbonExperiment.Samples;
+
+///
+/// An example of the control.
+///
+[ToolkitSample(id: nameof(RibbonCustomSample), "Ribbon control sample", description: $"A sample for showing how to create and use a {nameof(Ribbon)} custom control.")]
+public sealed partial class RibbonCustomSample : Page
+{
+ public RibbonCustomSample() => InitializeComponent();
+}
diff --git a/components/Ribbon/src/CommunityToolkit.WinUI.Controls.Ribbon.csproj b/components/Ribbon/src/CommunityToolkit.WinUI.Controls.Ribbon.csproj
new file mode 100644
index 000000000..894e3e5ab
--- /dev/null
+++ b/components/Ribbon/src/CommunityToolkit.WinUI.Controls.Ribbon.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+ Ribbon
+ This package contains Ribbon.
+
+
+ CommunityToolkit.WinUI.Controls.RibbonRns
+
+
+
+
+
+
+
+
diff --git a/components/Ribbon/src/Dependencies.props b/components/Ribbon/src/Dependencies.props
new file mode 100644
index 000000000..e622e1df4
--- /dev/null
+++ b/components/Ribbon/src/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Ribbon/src/DoubleList.cs b/components/Ribbon/src/DoubleList.cs
new file mode 100644
index 000000000..182ca3eeb
--- /dev/null
+++ b/components/Ribbon/src/DoubleList.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.Foundation.Metadata;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// A list of values.
+///
+[CreateFromString(MethodName = "CommunityToolkit.WinUI.Controls.DoubleList.CreateFromString")]
+public class DoubleList : List
+{
+ ///
+ /// Initializes a new instance of that is empty and has the default
+ /// initial capacity.
+ ///
+ public DoubleList()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of that contains elements copied from
+ /// the specified collection and has sufficient capacity to accommodate the number of elements
+ /// copied.
+ ///
+ /// The collection whose elements are copied to the new list.
+ public DoubleList(IEnumerable values)
+ : base(values)
+ {
+ }
+
+ ///
+ /// Create a from the string.
+ ///
+ /// The list of doubles separated by ','.
+ /// A instance with the content of .
+ public static DoubleList CreateFromString(string value)
+ {
+ var list = value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(double.Parse);
+ return new DoubleList(list);
+ }
+}
diff --git a/components/Ribbon/src/MultiTarget.props b/components/Ribbon/src/MultiTarget.props
new file mode 100644
index 000000000..b11c19426
--- /dev/null
+++ b/components/Ribbon/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;
+
+
\ No newline at end of file
diff --git a/components/Ribbon/src/Ribbon.cs b/components/Ribbon/src/Ribbon.cs
new file mode 100644
index 000000000..ba36ef79c
--- /dev/null
+++ b/components/Ribbon/src/Ribbon.cs
@@ -0,0 +1,201 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Specialized;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// An Office like Ribbon control which displays groups of commands. If there is not enough space to display all the groups,
+/// some of them can be collapsed based on a priority order.
+///
+[ContentProperty(Name = nameof(Items))]
+[TemplatePart(Name = PanelTemplatePart, Type = typeof(Panel))]
+[TemplatePart(Name = ScrollViewerTemplatePart, Type = typeof(ScrollViewer))]
+[TemplatePart(Name = ScrollDecrementButtonTempatePart, Type = typeof(ButtonBase))]
+[TemplatePart(Name = ScrollIncrementButtonTempatePart, Type = typeof(ButtonBase))]
+[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = NoButtonsStateTemplatePart)]
+[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = DecrementButtonStateTemplatePart)]
+[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = IncrementButtonStateTemplatePart)]
+[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = BothButtonsStateTemplatePart)]
+public sealed partial class Ribbon : Control
+{
+ private const string PanelTemplatePart = "Panel";
+ private const string ScrollViewerTemplatePart = "ScrollViewer";
+ private const string ScrollDecrementButtonTempatePart = "ScrollDecrementButton";
+ private const string ScrollIncrementButtonTempatePart = "ScrollIncrementButton";
+ private const string ScrollButtonGroupNameTemplatePart = "ScrollButtonGroup";
+ private const string NoButtonsStateTemplatePart = "NoButtons";
+ private const string DecrementButtonStateTemplatePart = "DecrementButton";
+ private const string IncrementButtonStateTemplatePart = "IncrementButton";
+ private const string BothButtonsStateTemplatePart = "BothButtons";
+
+ private Panel? _panel;
+ private ScrollViewer? _scrollViewer;
+ private ButtonBase? _decrementButton;
+ private ButtonBase? _incrementButton;
+ private readonly ObservableCollection _items;
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty ScrollStepProperty = DependencyProperty.Register(
+ nameof(ScrollStep),
+ typeof(double),
+ typeof(Ribbon),
+ new PropertyMetadata(20.0));
+
+ ///
+ /// The amount to add or remove from the current scroll position.
+ ///
+ public double ScrollStep
+ {
+ get => (double)GetValue(ScrollStepProperty);
+ set => SetValue(ScrollStepProperty, value);
+ }
+
+ public Ribbon()
+ {
+ DefaultStyleKey = typeof(Ribbon);
+
+ _items = [];
+ _items.CollectionChanged += OnItemsCollectionChanged;
+ }
+
+ public IList Items => _items;
+
+ protected override void OnApplyTemplate()
+ {
+ _panel = GetTemplateChild(PanelTemplatePart) as Panel;
+ if (_panel is not null)
+ {
+ foreach (var item in _items)
+ {
+ _panel.Children.Add(item);
+ }
+
+ _panel.SizeChanged += OnSizeChanged;
+ }
+
+ _decrementButton = GetTemplateChild(ScrollDecrementButtonTempatePart) as ButtonBase;
+ if (_decrementButton is not null)
+ {
+ _decrementButton.Click += OnDecrementScrollViewer;
+ }
+
+ _incrementButton = GetTemplateChild(ScrollIncrementButtonTempatePart) as ButtonBase;
+ if (_incrementButton is not null)
+ {
+ _incrementButton.Click += OnIncrementScrollViewer;
+ }
+
+ _scrollViewer = GetTemplateChild(ScrollViewerTemplatePart) as ScrollViewer;
+ if (_scrollViewer is not null)
+ {
+ _scrollViewer.ViewChanged += OnViewChanged;
+ _scrollViewer.SizeChanged += OnSizeChanged;
+ UpdateScrollButtonsState();
+ }
+ }
+
+ private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (_panel is null)
+ {
+ return;
+ }
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ if (e.NewItems is not null)
+ {
+ for (var i = 0; i < e.NewItems.Count; i++)
+ {
+ var element = (UIElement?)e.NewItems[i] ?? throw new ArgumentException("Item must not be null");
+ _panel.Children.Insert(e.NewStartingIndex + i, element);
+ }
+ }
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ if (e.OldItems is not null)
+ {
+ for (var i = 0; i < e.OldItems.Count; i++)
+ {
+ var element = (UIElement?)e.OldItems[i] ?? throw new ArgumentException("Item must not be null");
+ _panel.Children.Insert(e.OldStartingIndex, element);
+ }
+ }
+ break;
+ case NotifyCollectionChangedAction.Replace:
+ if (e.NewItems is not null)
+ {
+ for (var i = 0; i < e.NewItems.Count; i++)
+ {
+ var element = (UIElement?)e.NewItems[i] ?? throw new ArgumentException("Item must not be null");
+ _panel.Children[e.NewStartingIndex + i] = element;
+ }
+ }
+ break;
+ case NotifyCollectionChangedAction.Move:
+ _panel.Children.Move((uint)e.OldStartingIndex, (uint)e.NewStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Reset:
+ _panel.Children.Clear();
+ if (e.NewItems is not null)
+ {
+ foreach (var newItem in e.NewItems)
+ {
+ _panel.Children.Add((UIElement)newItem);
+ }
+ }
+ break;
+ default:
+ throw new ArgumentException("Invalid value for NotifyCollectionChangedAction");
+ }
+ }
+
+ private void OnViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) => UpdateScrollButtonsState();
+
+ private void OnSizeChanged(object? sender, SizeChangedEventArgs e) => UpdateScrollButtonsState();
+
+ private void UpdateScrollButtonsState()
+ {
+ if (_scrollViewer is null)
+ {
+ return;
+ }
+
+ if (_scrollViewer.ExtentWidth <= _scrollViewer.ViewportWidth)
+ {
+ VisualStateManager.GoToState(this, NoButtonsStateTemplatePart, useTransitions: true);
+ return;
+ }
+
+ var showDecrement = _scrollViewer.HorizontalOffset >= 1;
+ var showIncrement = _scrollViewer.ExtentWidth - _scrollViewer.HorizontalOffset - _scrollViewer.ViewportWidth >= 1;
+ if (showDecrement && showIncrement)
+ {
+ VisualStateManager.GoToState(this, BothButtonsStateTemplatePart, useTransitions: true);
+ }
+ else if (showDecrement)
+ {
+ VisualStateManager.GoToState(this, DecrementButtonStateTemplatePart, useTransitions: true);
+ }
+ else if (showIncrement)
+ {
+ VisualStateManager.GoToState(this, IncrementButtonStateTemplatePart, useTransitions: true);
+ }
+ else
+ {
+ VisualStateManager.GoToState(this, NoButtonsStateTemplatePart, useTransitions: true);
+ }
+ }
+
+ private void OnDecrementScrollViewer(object sender, RoutedEventArgs e)
+ => _scrollViewer?.ChangeView(_scrollViewer.HorizontalOffset - ScrollStep, verticalOffset: null, zoomFactor: null);
+
+ private void OnIncrementScrollViewer(object sender, RoutedEventArgs e)
+ => _scrollViewer?.ChangeView(_scrollViewer.HorizontalOffset + ScrollStep, verticalOffset: null, zoomFactor: null);
+}
diff --git a/components/Ribbon/src/RibbonCollapsibleGroup.cs b/components/Ribbon/src/RibbonCollapsibleGroup.cs
new file mode 100644
index 000000000..b4812b00e
--- /dev/null
+++ b/components/Ribbon/src/RibbonCollapsibleGroup.cs
@@ -0,0 +1,289 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.System;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// A group which can be collapsed if its content does not fit in the 's available space.
+/// If the content does not fit, the group will display a single button. Clicking on this button will open
+/// a flyout containing the group's content.
+///
+[ContentProperty(Name = nameof(Content))]
+[TemplatePart(Name = VisibleContentContainerTemplatePart, Type = typeof(ContentPresenter))]
+[TemplatePart(Name = CollapsedButtonTemplatePart, Type = typeof(Button))]
+[TemplatePart(Name = CollapsedFlyoutTemplatePart, Type = typeof(Flyout))]
+[TemplatePart(Name = CollapsedContentPresenterTemplatePart, Type = typeof(ContentPresenter))]
+public partial class RibbonCollapsibleGroup : RibbonGroup
+{
+ private const string VisibleContentContainerTemplatePart = "VisibleContentContainer";
+ private const string CollapsedButtonTemplatePart = "CollapsedButton";
+ private const string CollapsedFlyoutTemplatePart = "CollapsedFlyout";
+ private const string CollapsedContentPresenterTemplatePart = "CollapsedContentPresenter";
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty IconSourceProperty = DependencyProperty.Register(
+ nameof(IconSource),
+ typeof(IconSource),
+ typeof(RibbonCollapsibleGroup),
+ new PropertyMetadata(null));
+
+ ///
+ /// The group icon. It will only be displayed when the group is in the collapsed state.
+ ///
+ public IconSource IconSource
+ {
+ get => (IconSource)GetValue(IconSourceProperty);
+ set => SetValue(IconSourceProperty, value);
+ }
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty StateProperty = DependencyProperty.Register(
+ nameof(State),
+ typeof(Visibility),
+ typeof(RibbonCollapsibleGroup),
+ new PropertyMetadata(Visibility.Visible, OnStatePropertyChanged));
+
+ ///
+ /// The state of the group.
+ ///
+ public Visibility State
+ {
+ get => (Visibility)GetValue(StateProperty);
+ set => SetValue(StateProperty, value);
+ }
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty AutoCloseFlyoutProperty = DependencyProperty.Register(
+ nameof(AutoCloseFlyout),
+ typeof(bool),
+ typeof(RibbonCollapsibleGroup),
+ new PropertyMetadata(true));
+
+ ///
+ /// True to automatically close the overflow flyout if one interactive element is clicked.
+ /// Note that the logic to detect the click is very basic. It will request the flyout to close
+ /// for all the handled pointer released events. It assumes that if the pointer has been handled
+ /// something reacted to it. It works well for buttons or check boxes but does not work for text
+ /// or combo boxes.
+ ///
+ public bool AutoCloseFlyout
+ {
+ get => (bool)GetValue(AutoCloseFlyoutProperty);
+ set => SetValue(AutoCloseFlyoutProperty, value);
+ }
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty PriorityProperty = DependencyProperty.Register(
+ nameof(Priority),
+ typeof(int),
+ typeof(RibbonCollapsibleGroup),
+ new PropertyMetadata(0));
+
+ ///
+ /// The priority of the group.
+ /// The group with the lower priority will be the first one to be collapsed.
+ ///
+ public int Priority
+ {
+ get => (int)GetValue(PriorityProperty);
+ set => SetValue(PriorityProperty, value);
+ }
+
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty CollapsedAccessKeyProperty = DependencyProperty.Register(
+ nameof(CollapsedAccessKey),
+ typeof(string),
+ typeof(RibbonCollapsibleGroup),
+ new PropertyMetadata(string.Empty));
+
+ ///
+ /// The access key to access the collapsed button and open the flyout.
+ ///
+ public string CollapsedAccessKey
+ {
+ get => (string)GetValue(CollapsedAccessKeyProperty);
+ set => SetValue(CollapsedAccessKeyProperty, value);
+ }
+
+
+ ///
+ /// The DP to store the property value.
+ ///
+ public static readonly DependencyProperty RequestedWidthsProperty = DependencyProperty.Register(
+ nameof(RequestedWidths),
+ typeof(DoubleList),
+ typeof(RibbonCollapsibleGroup),
+ new PropertyMetadata(null, OnRequestedWidthChanged));
+
+ private static void OnRequestedWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (e.NewValue != null)
+ {
+ ((DoubleList)e.NewValue).Sort();
+ }
+ }
+
+ ///
+ /// The list of requested widths for the group.
+ /// If null or empty, the group will automatically use the size of its content.
+ /// If set, the group will use the smallest provided width fitting in the ribbon.
+ /// This is useful if the group contains a variable size control which can adjust
+ /// its width (like a GridView with several items).
+ ///
+ public DoubleList RequestedWidths
+ {
+ get => (DoubleList)GetValue(RequestedWidthsProperty);
+ set => SetValue(RequestedWidthsProperty, value);
+ }
+
+ private ContentControl? _visibleContentContainer;
+ private ContentControl? _collapsedContentContainer;
+ private Button? _collapsedButton;
+ private Flyout? _collapsedFlyout;
+
+ public RibbonCollapsibleGroup()
+ => DefaultStyleKey = typeof(RibbonCollapsibleGroup);
+
+ protected override void OnApplyTemplate()
+ {
+ if (_collapsedFlyout is not null)
+ {
+ _collapsedFlyout.Opened -= OnFlyoutOpened;
+ }
+
+ if (_collapsedContentContainer is not null)
+ {
+ _collapsedContentContainer.RemoveHandler(PointerReleasedEvent, new PointerEventHandler(OnFlyoutPointerReleased));
+ _collapsedContentContainer.RemoveHandler(KeyUpEvent, new KeyEventHandler(OnFlyoutKeyUp));
+ }
+
+ _visibleContentContainer = Get(VisibleContentContainerTemplatePart);
+ _collapsedContentContainer = Get(CollapsedContentPresenterTemplatePart);
+ _collapsedButton = Get