Skip to content
Shiny.NET

ShinyFlexLayout

A CSS-flexbox layout for .NET MAUI with the same properties as MAUI’s FlexLayout, plus RowSpacing and ColumnSpacing gaps. It is built to be fast. MAUI’s FlexLayout measures every child in every measure and arrange pass, and again whenever any one of them changes. ShinyFlexLayout measures a child once and reuses the result until that child changes.

  • NuGet downloads for Shiny.Maui.Controls
Frameworks
.NET MAUI
  • Same API as FlexLayout. Direction, Wrap, JustifyContent, AlignItems, AlignContent, and the attached Grow, Shrink, Basis, AlignSelf and Order, using MAUI’s own Microsoft.Maui.Layouts enums and FlexBasis.
  • Gaps. RowSpacing and ColumnSpacing follow CSS row-gap and column-gap. No more margins on every child.
  • Measure caching. A pass at the same size measures nothing, one changed child re-measures only itself, and a rotation reuses leaf measurements.
  • Allocation-free passes. Buffers are reused, and the pass uses no LINQ and no boxing.
  • CSS-correct shrink. An overflowing no-wrap line shrinks in proportion to each child’s size and fits.
  • Right-to-left mirrors the layout, including the cross axis of a column layout.
  • Works with BindableLayout.ItemsSource / ItemTemplate.
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 37 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 37 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View Skills Repository
<shiny:ShinyFlexLayout Wrap="Wrap"
JustifyContent="SpaceBetween"
AlignItems="Center"
ColumnSpacing="8"
RowSpacing="8">
<Label Text="One" />
<Label Text="Takes the rest" shiny:ShinyFlexLayout.Grow="1" />
<Label Text="Always first" shiny:ShinyFlexLayout.Order="-1" />
<BoxView shiny:ShinyFlexLayout.Basis="25%" shiny:ShinyFlexLayout.Shrink="0" />
</shiny:ShinyFlexLayout>
var flex = new ShinyFlexLayout { Wrap = FlexWrap.Wrap, ColumnSpacing = 6, RowSpacing = 6 };
var label = new Label { Text = "grow" };
ShinyFlexLayout.SetGrow(label, 1);
ShinyFlexLayout.SetBasis(label, new FlexBasis(0.25f, isRelative: true));
flex.Children.Add(label);
MAUI FlexLayout ShinyFlexLayout
Pass at the same size (the platform does this constantly) measures every child measures nothing; the resolved pass is reused
One child changes (a label’s text, an image loading) measures every child measures that child
Resize / rotation measures every child leaf children that still fit are not re-measured
Measure then arrange the flex is resolved twice resolved once; arrange is arithmetic
Allocations per pass engine objects every pass none

Measured on an iOS simulator with real Labels in a wrapped layout. Each pass alternates the width (like a rotation) and changes one label’s text. This is the benchmark on the sample app’s Flex Layout page:

Labels MAUI FlexLayout ShinyFlexLayout
300 6.7 ms / pass 0.47 ms / pass ~14×
1000 ~23 ms / pass 0.9 ms / pass ~25×

At 1000 children, MAUI’s measure pass alone is longer than a 60 fps frame.

A child’s cached measurement is thrown away when:

  • the child raises MeasureInvalidated. That covers the Controls path: Text, WidthRequest, Margin, IsVisible, and anything else that calls InvalidateMeasure();
  • a handler calls IView.InvalidateMeasure() on the child or on anything inside it. This is the native path, for example an image finishing loading inside a Grid that is a flex child. MAUI raises no event for this path, so the layout listens on ViewHandler.ViewCommandMapper;
  • the child’s handler changes.

A zero-size result is never cached (it usually means “not loaded yet”). Layout and content-host children (Grid, Border, ContentView) only reuse a measurement taken under exactly the same constraints. If a child changes size natively and nothing calls InvalidateMeasure(), call it yourself, or set IsMeasureCacheEnabled="False".

Property Type Default Description
Direction FlexDirection Row Main axis: Row, RowReverse, Column, ColumnReverse
Wrap FlexWrap NoWrap NoWrap, Wrap, or Reverse
JustifyContent FlexJustify Start Start, Center, End, SpaceBetween, SpaceAround, SpaceEvenly
AlignItems FlexAlignItems Stretch Stretch, Center, Start, End
AlignContent FlexAlignContent Stretch How wrapped lines share cross space; applies only when wrapping
RowSpacing double 0 CSS row-gap
ColumnSpacing double 0 CSS column-gap
IsMeasureCacheEnabled bool true Reuse child measurements and resolved passes
Property Type Default Description
ShinyFlexLayout.Grow float 0 Share of the line’s spare room
ShinyFlexLayout.Shrink float 1 Weighted give when the line overflows; 0 never shrinks
ShinyFlexLayout.Basis FlexBasis Auto Auto, a length (120) or a percentage ("25%")
ShinyFlexLayout.AlignSelf FlexAlignSelf Auto Overrides AlignItems for one child
ShinyFlexLayout.Order int 0 Visual order; ties keep child order

The enums and FlexBasis are MAUI’s own, so porting a page means changing the element name and the attached-property prefix:

<FlexLayout Wrap="Wrap" JustifyContent="SpaceAround">
<Label FlexLayout.Grow="1" Margin="0,0,8,8" />
<shiny:ShinyFlexLayout Wrap="Wrap" JustifyContent="SpaceAround" ColumnSpacing="8" RowSpacing="8">
<Label shiny:ShinyFlexLayout.Grow="1" />
  • Shrink follows CSS, so an overflowing line fits. MAUI’s FlexLayout takes the same amount from each child and can still spill past the container.
  • Stretch respects an explicit size. A child with a HeightRequest in a row is not stretched.
  • FlexLayout.Position (absolute) is not carried over. Use a Grid overlay or AbsoluteLayout.