diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs
new file mode 100644
index 0000000000..ad8f66251a
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Creates an accent color for a given base color value and step parameter.
+ ///
+ public class AccentColorConverter : IValueConverter
+ {
+ ///
+ /// The amount to change the Value channel for each accent color step.
+ ///
+ public const double ValueDelta = 0.1;
+
+ ///
+ public object? Convert(
+ object? value,
+ Type targetType,
+ object? parameter,
+ CultureInfo culture)
+ {
+ int accentStep;
+ Color? rgbColor = null;
+ HsvColor? hsvColor = null;
+
+ // Get the current color in HSV
+ if (value is Color valueColor)
+ {
+ rgbColor = valueColor;
+ }
+ else if (value is HsvColor valueHsvColor)
+ {
+ hsvColor = valueHsvColor;
+ }
+ else if (value is SolidColorBrush valueBrush)
+ {
+ rgbColor = valueBrush.Color;
+ }
+ else
+ {
+ // Invalid color value provided
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ // Get the value component delta
+ try
+ {
+ accentStep = int.Parse(parameter?.ToString() ?? "", CultureInfo.InvariantCulture);
+ }
+ catch
+ {
+ // Invalid parameter provided, unable to convert to integer
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ if (hsvColor == null &&
+ rgbColor != null)
+ {
+ hsvColor = rgbColor.Value.ToHsv();
+ }
+
+ if (hsvColor != null)
+ {
+ return new SolidColorBrush(GetAccent(hsvColor.Value, accentStep).ToRgb());
+ }
+ else
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+ }
+
+ ///
+ public object? ConvertBack(
+ object? value,
+ Type targetType,
+ object? parameter,
+ CultureInfo culture)
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ ///
+ /// This does not account for perceptual differences and also does not match with
+ /// system accent color calculation.
+ ///
+ ///
+ /// Use the HSV representation as it's more perceptual.
+ /// In most cases only the value is changed by a fixed percentage so the algorithm is reproducible.
+ ///
+ /// The base color to calculate the accent from.
+ /// The number of accent color steps to move.
+ /// The new accent color.
+ public static HsvColor GetAccent(HsvColor hsvColor, int accentStep)
+ {
+ if (accentStep != 0)
+ {
+ double colorValue = hsvColor.V;
+ colorValue += (accentStep * AccentColorConverter.ValueDelta);
+ colorValue = Math.Round(colorValue, 2);
+
+ return new HsvColor(hsvColor.A, hsvColor.H, hsvColor.S, colorValue);
+ }
+ else
+ {
+ return hsvColor;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
new file mode 100644
index 0000000000..74c0943919
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
@@ -0,0 +1,69 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ public partial class ColorPreviewer
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorProperty =
+ AvaloniaProperty.Register(
+ nameof(Color),
+ Colors.White);
+
+ ///
+ /// Gets or sets the currently previewed color in the RGB color model.
+ ///
+ ///
+ /// For control authors use instead to avoid loss
+ /// of precision and color drifting.
+ ///
+ public Color Color
+ {
+ get => GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HsvColorProperty =
+ AvaloniaProperty.Register(
+ nameof(HsvColor),
+ Colors.Transparent.ToHsv());
+
+ ///
+ /// Gets or sets the currently previewed color in the HSV color model.
+ ///
+ ///
+ /// This should be used in all cases instead of the property.
+ /// Internally, the uses the HSV color model and using
+ /// this property will avoid loss of precision and color drifting.
+ ///
+ public HsvColor HsvColor
+ {
+ get => GetValue(HsvColorProperty);
+ set => SetValue(HsvColorProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ShowAccentColorsProperty =
+ AvaloniaProperty.Register(
+ nameof(ShowAccentColors),
+ true);
+
+ ///
+ /// Gets or sets a value indicating whether accent colors are shown along
+ /// with the preview color.
+ ///
+ public bool ShowAccentColors
+ {
+ get => (bool)this.GetValue(ShowAccentColorsProperty);
+ set => SetValue(ShowAccentColorsProperty, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
new file mode 100644
index 0000000000..35bd62601f
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls.Metadata;
+using Avalonia.Input;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Presents a preview color with optional accent colors.
+ ///
+ [TemplatePart(Name = nameof(AccentDec1Border), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(AccentDec2Border), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(AccentInc1Border), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(AccentInc2Border), Type = typeof(Border))]
+ public partial class ColorPreviewer : TemplatedControl
+ {
+ ///
+ /// Event for when the selected color changes within the previewer.
+ /// This happens when an accent color is pressed.
+ ///
+ public event EventHandler? ColorChanged;
+
+ private bool eventsConnected = false;
+
+ private Border? AccentDec1Border;
+ private Border? AccentDec2Border;
+ private Border? AccentInc1Border;
+ private Border? AccentInc2Border;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorPreviewer() : base()
+ {
+ }
+
+ ///
+ /// Connects or disconnects all control event handlers.
+ ///
+ /// True to connect event handlers, otherwise false.
+ private void ConnectEvents(bool connected)
+ {
+ if (connected == true && eventsConnected == false)
+ {
+ // Add all events
+ if (AccentDec1Border != null) { AccentDec1Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (AccentDec2Border != null) { AccentDec2Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (AccentInc1Border != null) { AccentInc1Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (AccentInc2Border != null) { AccentInc2Border.PointerPressed += AccentBorder_PointerPressed; }
+
+ eventsConnected = true;
+ }
+ else if (connected == false && eventsConnected == true)
+ {
+ // Remove all events
+ if (AccentDec1Border != null) { AccentDec1Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (AccentDec2Border != null) { AccentDec2Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (AccentInc1Border != null) { AccentInc1Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (AccentInc2Border != null) { AccentInc2Border.PointerPressed -= AccentBorder_PointerPressed; }
+
+ eventsConnected = false;
+ }
+
+ return;
+ }
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ // Remove any existing events present if the control was previously loaded then unloaded
+ ConnectEvents(false);
+
+ AccentDec1Border = e.NameScope.Find(nameof(AccentDec1Border));
+ AccentDec2Border = e.NameScope.Find(nameof(AccentDec2Border));
+ AccentInc1Border = e.NameScope.Find(nameof(AccentInc1Border));
+ AccentInc2Border = e.NameScope.Find(nameof(AccentInc2Border));
+
+ // Must connect after controls are found
+ ConnectEvents(true);
+
+ base.OnApplyTemplate(e);
+ }
+
+ ///
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ // Always keep the two color properties in sync
+ if (change.Property == ColorProperty)
+ {
+ HsvColor = Color.ToHsv();
+ }
+ else if (change.Property == HsvColorProperty)
+ {
+ Color = HsvColor.ToRgb();
+ }
+
+ base.OnPropertyChanged(change);
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// The newly selected color.
+ protected virtual void OnColorChanged(HsvColor newColor)
+ {
+ var oldColor = HsvColor;
+ HsvColor = newColor;
+
+ ColorChanged?.Invoke(this, new ColorChangedEventArgs(oldColor.ToRgb(), newColor.ToRgb()));
+
+ return;
+ }
+
+ ///
+ /// Event handler for when an accent color border is pressed.
+ /// This will update the color to the background of the pressed panel.
+ ///
+ private void AccentBorder_PointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ Border? border = sender as Border;
+ int accentStep = 0;
+ HsvColor hsvColor = HsvColor;
+
+ // Get the value component delta
+ try
+ {
+ accentStep = int.Parse(border?.Tag?.ToString() ?? "", CultureInfo.InvariantCulture);
+ }
+ catch { }
+
+ HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep);
+ OnColorChanged(newHsvColor);
+
+ return;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml
index fb656ce964..d96066e56b 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml
@@ -21,6 +21,7 @@
+
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
new file mode 100644
index 0000000000..a91da45578
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+