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.
Features
Section titled “Features”- Same API as
FlexLayout.Direction,Wrap,JustifyContent,AlignItems,AlignContent, and the attachedGrow,Shrink,Basis,AlignSelfandOrder, using MAUI’s ownMicrosoft.Maui.Layoutsenums andFlexBasis. - Gaps.
RowSpacingandColumnSpacingfollow CSSrow-gapandcolumn-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.
AI Skill
Section titled “AI Skill”Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne 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.
Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny@shinyOne 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.
Quick Start
Section titled “Quick Start”<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);Why it is faster
Section titled “Why it is faster”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.
How it knows a child changed
Section titled “How it knows a child changed”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 callsInvalidateMeasure(); - 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 aGridthat is a flex child. MAUI raises no event for this path, so the layout listens onViewHandler.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".
Properties
Section titled “Properties”| 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 |
Attached Properties
Section titled “Attached Properties”| 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 |
Migrating from FlexLayout
Section titled “Migrating from FlexLayout”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
FlexLayouttakes the same amount from each child and can still spill past the container. - Stretch respects an explicit size. A child with a
HeightRequestin a row is not stretched. FlexLayout.Position(absolute) is not carried over. Use aGridoverlay orAbsoluteLayout.


