diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
index 473190fe59..b6aa1fc9a4 100644
--- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
+++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
@@ -23,6 +23,7 @@
+
diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
index a92ba0dc83..948c413c82 100644
--- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
+++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
@@ -23,6 +23,7 @@
+
diff --git a/tests/Avalonia.Controls.UnitTests/ColorPickerTests/ColorPickerHelpersTests.cs b/tests/Avalonia.Controls.UnitTests/ColorPickerTests/ColorPickerHelpersTests.cs
new file mode 100644
index 0000000000..605c48caf6
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/ColorPickerTests/ColorPickerHelpersTests.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Collections.Pooled;
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.ColorPickerTests;
+
+///
+/// Tests that produces
+/// accurate pixel output when isPerceptive=false — the mode the ColorSpectrum's
+/// third-component slider should use.
+///
+/// Regression tests for https://github.com/AvaloniaUI/Avalonia/issues/20925
+///
+public class ColorPickerHelpersTests : ScopedTestBase
+{
+ ///
+ /// When sweeping Hue at a fixed desaturated selection (S=0.2, V=0.5),
+ /// every pixel's brightest channel should be bounded by V — not boosted to 255.
+ ///
+ [Fact]
+ public async Task Hue_Slider_Bitmap_Reflects_Actual_Saturation_And_Value()
+ {
+ var selection = new HsvColor(alpha: 1.0, hue: 180, saturation: 0.2, value: 0.5);
+
+ using var pixels = await GenerateSliderBitmap(ColorComponent.Component1, selection, height: 360);
+
+ // Sample mid-strip. In HSV, max RGB channel = V for any hue.
+ var (r, g, b) = ReadPixel(pixels, row: 180);
+ var maxChannel = Math.Max(r, Math.Max(g, b));
+ double expectedMax = selection.V * 255;
+
+ Assert.InRange(maxChannel, expectedMax - 10, expectedMax + 10);
+ }
+
+ ///
+ /// When sweeping Saturation, the brightest channel at full saturation is bounded
+ /// by the current Value — not forced to 255 by perceptive V=1.0 override.
+ ///
+ [Theory]
+ [InlineData(0.1)]
+ [InlineData(0.5)]
+ [InlineData(0.9)]
+ public async Task Saturation_Slider_Bitmap_Respects_Current_Value(double currentValue)
+ {
+ var selection = new HsvColor(alpha: 1.0, hue: 200, saturation: 0.8, value: currentValue);
+
+ using var pixels = await GenerateSliderBitmap(ColorComponent.Component2, selection);
+
+ // Row 0 = highest saturation (bitmap sweeps high→low top-to-bottom).
+ var (r, g, b) = ReadPixel(pixels, row: 0);
+ var maxChannel = Math.Max(r, Math.Max(g, b));
+ double expectedMax = currentValue * 255;
+
+ Assert.InRange(maxChannel, expectedMax - 20, expectedMax + 20);
+ }
+
+ ///
+ /// When sweeping Value at low Saturation (S=0.1), the top pixel should be
+ /// near-grey (low chroma) — not vivid from a perceptive S=1.0 override.
+ ///
+ [Fact]
+ public async Task Value_Slider_Bitmap_Respects_Current_Saturation()
+ {
+ var selection = new HsvColor(alpha: 1.0, hue: 30, saturation: 0.1, value: 0.8);
+
+ using var pixels = await GenerateSliderBitmap(ColorComponent.Component3, selection);
+
+ // Row 0 = highest Value. In HSV, chroma = V * S * 255 ≤ S * 255.
+ var (r, g, b) = ReadPixel(pixels, row: 0);
+ int chroma = Math.Max(r, Math.Max(g, b)) - Math.Min(r, Math.Min(g, b));
+
+ // At S=0.1 the max possible chroma is 25.5 — near-grey for any V.
+ // With perceptive S=1.0 override, chroma would be ~252.
+ Assert.InRange(chroma, 0, selection.S * 255 + 10);
+ }
+
+ private static async Task> GenerateSliderBitmap(
+ ColorComponent component,
+ HsvColor baseColor,
+ int height = 100)
+ {
+ const int width = 1;
+ var buffer = new PooledList(width * height * 4, ClearMode.Never, sizeToCapacity: true);
+
+ await ColorPickerHelpers.CreateComponentBitmapAsync(
+ buffer, width, height,
+ Orientation.Vertical,
+ ColorModel.Hsva,
+ component,
+ baseColor,
+ isAlphaVisible: false,
+ isPerceptive: false);
+
+ return buffer;
+ }
+
+ ///
+ /// Reads a pixel from a 1-pixel-wide vertical BGRA bitmap at the given row.
+ ///
+ private static (byte R, byte G, byte B) ReadPixel(PooledList bgraData, int row)
+ {
+ int offset = row * 4;
+ return (R: bgraData[offset + 2], G: bgraData[offset + 1], B: bgraData[offset]);
+ }
+}