diff --git a/src/Avalonia.Controls/Primitives/UniformGrid.cs b/src/Avalonia.Controls/Primitives/UniformGrid.cs
new file mode 100644
index 0000000000..f3580eee10
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/UniformGrid.cs
@@ -0,0 +1,161 @@
+using System;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// A with uniform column and row sizes.
+ ///
+ public class UniformGrid : Panel
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty RowsProperty =
+ AvaloniaProperty.Register(nameof(Rows));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColumnsProperty =
+ AvaloniaProperty.Register(nameof(Columns));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty FirstColumnProperty =
+ AvaloniaProperty.Register(nameof(FirstColumn));
+
+ private int _rows;
+ private int _columns;
+
+ ///
+ /// Specifies the row count. If set to 0, row count will be calculated automatically.
+ ///
+ public int Rows
+ {
+ get => GetValue(RowsProperty);
+ set => SetValue(RowsProperty, value);
+ }
+
+ ///
+ /// Specifies the column count. If set to 0, column count will be calculated automatically.
+ ///
+ public int Columns
+ {
+ get => GetValue(ColumnsProperty);
+ set => SetValue(ColumnsProperty, value);
+ }
+
+ ///
+ /// Specifies, for the first row, the column where the items should start.
+ ///
+ public int FirstColumn
+ {
+ get => GetValue(FirstColumnProperty);
+ set => SetValue(FirstColumnProperty, value);
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ UpdateRowsAndColumns();
+
+ var maxWidth = 0d;
+ var maxHeight = 0d;
+
+ var childAvailableSize = new Size(availableSize.Width / _columns, availableSize.Height / _rows);
+
+ foreach (var child in Children)
+ {
+ child.Measure(childAvailableSize);
+
+ if (child.DesiredSize.Width > maxWidth)
+ {
+ maxWidth = child.DesiredSize.Width;
+ }
+
+ if (child.DesiredSize.Height > maxHeight)
+ {
+ maxHeight = child.DesiredSize.Height;
+ }
+ }
+
+ return new Size(maxWidth * _columns, maxHeight * _rows);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var x = FirstColumn;
+ var y = 0;
+
+ var width = finalSize.Width / _columns;
+ var height = finalSize.Height / _rows;
+
+ foreach (var child in Children)
+ {
+ if (!child.IsVisible)
+ {
+ continue;
+ }
+
+ child.Arrange(new Rect(x * width, y * height, width, height));
+
+ x++;
+
+ if (x >= _columns)
+ {
+ x = 0;
+ y++;
+ }
+ }
+
+ return finalSize;
+ }
+
+ private void UpdateRowsAndColumns()
+ {
+ _rows = Rows;
+ _columns = Columns;
+
+ if (FirstColumn >= Columns)
+ {
+ FirstColumn = 0;
+ }
+
+ var itemCount = FirstColumn;
+
+ foreach (var child in Children)
+ {
+ if (child.IsVisible)
+ {
+ itemCount++;
+ }
+ }
+
+ if (_rows == 0)
+ {
+ if (_columns == 0)
+ {
+ _rows = _columns = (int)Math.Ceiling(Math.Sqrt(itemCount));
+ }
+ else
+ {
+ _rows = Math.DivRem(itemCount, _columns, out int rem);
+
+ if (rem != 0)
+ {
+ _rows++;
+ }
+ }
+ }
+ else if (_columns == 0)
+ {
+ _columns = Math.DivRem(itemCount, _rows, out int rem);
+
+ if (rem != 0)
+ {
+ _columns++;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs
new file mode 100644
index 0000000000..340bd09611
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs
@@ -0,0 +1,144 @@
+using Avalonia.Controls.Primitives;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Primitives
+{
+ public class UniformGridTests
+ {
+ [Fact]
+ public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows()
+ {
+ var target = new UniformGrid()
+ {
+ Children =
+ {
+ new Border { Width = 50, Height = 70 },
+ new Border { Width = 30, Height = 50 },
+ new Border { Width = 80, Height = 90 }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ // 2 * 2 grid
+ Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size);
+ }
+
+ [Fact]
+ public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows()
+ {
+ var target = new UniformGrid()
+ {
+ Columns = 2,
+ Children =
+ {
+ new Border { Width = 50, Height = 70 },
+ new Border { Width = 30, Height = 50 },
+ new Border { Width = 80, Height = 90 },
+ new Border { Width = 20, Height = 30 },
+ new Border { Width = 40, Height = 60 }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ // 2 * 3 grid
+ Assert.Equal(new Size(2 * 80, 3 * 90), target.Bounds.Size);
+ }
+
+ [Fact]
+ public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows()
+ {
+ var target = new UniformGrid()
+ {
+ Columns = 3,
+ FirstColumn = 2,
+ Children =
+ {
+ new Border { Width = 50, Height = 70 },
+ new Border { Width = 30, Height = 50 },
+ new Border { Width = 80, Height = 90 },
+ new Border { Width = 20, Height = 30 },
+ new Border { Width = 40, Height = 60 }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ // 3 * 3 grid
+ Assert.Equal(new Size(3 * 80, 3 * 90), target.Bounds.Size);
+ }
+
+ [Fact]
+ public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns()
+ {
+ var target = new UniformGrid()
+ {
+ Rows = 2,
+ Children =
+ {
+ new Border { Width = 50, Height = 70 },
+ new Border { Width = 30, Height = 50 },
+ new Border { Width = 80, Height = 90 },
+ new Border { Width = 20, Height = 30 },
+ new Border { Width = 40, Height = 60 }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ // 3 * 2 grid
+ Assert.Equal(new Size(3 * 80, 2 * 90), target.Bounds.Size);
+ }
+
+ [Fact]
+ public void Grid_Size_Is_Limited_By_Rows_And_Columns()
+ {
+ var target = new UniformGrid()
+ {
+ Columns = 2,
+ Rows = 2,
+ Children =
+ {
+ new Border { Width = 50, Height = 70 },
+ new Border { Width = 30, Height = 50 },
+ new Border { Width = 80, Height = 90 },
+ new Border { Width = 20, Height = 30 },
+ new Border { Width = 40, Height = 60 }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ // 2 * 2 grid
+ Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size);
+ }
+
+ [Fact]
+ public void Not_Visible_Children_Are_Ignored()
+ {
+ var target = new UniformGrid()
+ {
+ Children =
+ {
+ new Border { Width = 50, Height = 70 },
+ new Border { Width = 30, Height = 50 },
+ new Border { Width = 80, Height = 90, IsVisible = false },
+ new Border { Width = 20, Height = 30 },
+ new Border { Width = 40, Height = 60 }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ // 2 * 2 grid
+ Assert.Equal(new Size(2 * 50, 2 * 70), target.Bounds.Size);
+ }
+ }
+}