|
| 1 | +--- |
| 2 | +id: responsive-layouts |
| 3 | +title: Responsive layouts |
| 4 | +description: Create layouts that adapt to different sizes using container queries, form-factor extensions, and reflowing panels. |
| 5 | +doc-type: explanation |
| 6 | +--- |
| 7 | + |
| 8 | +Avalonia provides techniques for building layouts that adapt when the available space changes. You can respond to the size of a container, the type of device, or the dimensions of a reflowing panel. This page explains each approach and when to choose it. |
| 9 | + |
| 10 | +## Approaches at a glance |
| 11 | + |
| 12 | +| Technique | Responds to | Resolves | Best for | |
| 13 | +|-----------|-------------|----------|----------| |
| 14 | +| [Container queries](#container-queries) | Size of an ancestor control | Live, as the control resizes | Reusable components that appear in panels of varying width | |
| 15 | +| [`OnFormFactor`](#onformfactor) | Device type (desktop, mobile) | Once, at startup | Platform-specific layout differences | |
| 16 | +| [Reflowing panels](#reflowing-panels) | Available width | Live, as the panel resizes | Card grids and flowing content | |
| 17 | +| [Breakpoint view models](#breakpoint-view-models) | Window width (or any measured value) | Live, via property change | Complex multi-property transitions driven by code | |
| 18 | + |
| 19 | +## Container queries |
| 20 | + |
| 21 | +Container queries let you activate styles when an ancestor control reaches a specific size. Because the query targets a control rather than the window, the same component adapts correctly whether it appears in a full-width page, a narrow sidebar, or a dialog. |
| 22 | + |
| 23 | +### Declaring a container |
| 24 | + |
| 25 | +Mark any ancestor as a container by setting the `Container.Name` and `Container.Sizing` attached properties: |
| 26 | + |
| 27 | +```xml |
| 28 | +<Border Container.Name="main" |
| 29 | + Container.Sizing="Width"> |
| 30 | + <!-- child content here --> |
| 31 | +</Border> |
| 32 | +``` |
| 33 | + |
| 34 | +`Container.Sizing` determines which dimensions are tracked: |
| 35 | + |
| 36 | +| Value | Tracked dimensions | |
| 37 | +|-------|--------------------| |
| 38 | +| `Normal` | None (default) | |
| 39 | +| `Width` | Width only | |
| 40 | +| `Height` | Height only | |
| 41 | +| `WidthAndHeight` | Both width and height | |
| 42 | + |
| 43 | +### Writing a container query |
| 44 | + |
| 45 | +A `ContainerQuery` element lives inside the `Styles` collection of a control that is an ancestor of the container. It activates its child styles when the query condition is met: |
| 46 | + |
| 47 | +```xml |
| 48 | +<Window> |
| 49 | + <Window.Styles> |
| 50 | + <ContainerQuery Name="main" Query="max-width:600"> |
| 51 | + <Style Selector="StackPanel#sidebar"> |
| 52 | + <Setter Property="IsVisible" Value="False" /> |
| 53 | + </Style> |
| 54 | + </ContainerQuery> |
| 55 | + </Window.Styles> |
| 56 | + |
| 57 | + <Grid ColumnDefinitions="200,*"> |
| 58 | + <StackPanel x:Name="sidebar" Grid.Column="0"> |
| 59 | + <!-- sidebar content --> |
| 60 | + </StackPanel> |
| 61 | + <ContentControl Grid.Column="1" |
| 62 | + Content="{Binding CurrentPage}" /> |
| 63 | + </Grid> |
| 64 | +</Window> |
| 65 | +``` |
| 66 | + |
| 67 | +In this example, the sidebar hides when the container named `main` is 600 pixels wide or narrower. |
| 68 | + |
| 69 | +### Adjusting layout structure with breakpoints |
| 70 | + |
| 71 | +You can use multiple container queries on the same container to define breakpoint tiers. The following example changes the number of columns in a `UniformGrid` as the container grows: |
| 72 | + |
| 73 | +```xml |
| 74 | +<Panel Container.Name="content" Container.Sizing="Width"> |
| 75 | + <Panel.Styles> |
| 76 | + <ContainerQuery Name="content" Query="max-width:400"> |
| 77 | + <Style Selector="UniformGrid#cards"> |
| 78 | + <Setter Property="Columns" Value="1" /> |
| 79 | + </Style> |
| 80 | + </ContainerQuery> |
| 81 | + <ContainerQuery Name="content" Query="min-width:400"> |
| 82 | + <Style Selector="UniformGrid#cards"> |
| 83 | + <Setter Property="Columns" Value="2" /> |
| 84 | + </Style> |
| 85 | + </ContainerQuery> |
| 86 | + <ContainerQuery Name="content" Query="min-width:800"> |
| 87 | + <Style Selector="UniformGrid#cards"> |
| 88 | + <Setter Property="Columns" Value="3" /> |
| 89 | + </Style> |
| 90 | + </ContainerQuery> |
| 91 | + </Panel.Styles> |
| 92 | + |
| 93 | + <UniformGrid x:Name="cards"> |
| 94 | + <!-- card items --> |
| 95 | + </UniformGrid> |
| 96 | +</Panel> |
| 97 | +``` |
| 98 | + |
| 99 | +### Customising non-layout properties |
| 100 | + |
| 101 | +Container queries are not limited to layout properties. You can adjust any property that a `Style` can set, including font sizes, spacing, visibility, and colours: |
| 102 | + |
| 103 | +```xml |
| 104 | +<Panel Container.Name="content" Container.Sizing="Width"> |
| 105 | + <Panel.Styles> |
| 106 | + <!-- Default heading size --> |
| 107 | + <Style Selector="TextBlock.heading"> |
| 108 | + <Setter Property="FontSize" Value="24" /> |
| 109 | + </Style> |
| 110 | + |
| 111 | + <!-- Smaller heading when the container is narrow --> |
| 112 | + <ContainerQuery Name="content" Query="max-width:500"> |
| 113 | + <Style Selector="TextBlock.heading"> |
| 114 | + <Setter Property="FontSize" Value="18" /> |
| 115 | + </Style> |
| 116 | + <Style Selector="StackPanel.toolbar"> |
| 117 | + <Setter Property="Orientation" Value="Vertical" /> |
| 118 | + </Style> |
| 119 | + </ContainerQuery> |
| 120 | + </Panel.Styles> |
| 121 | + |
| 122 | + <StackPanel> |
| 123 | + <TextBlock Classes="heading" Text="Dashboard" /> |
| 124 | + <StackPanel Classes="toolbar" Orientation="Horizontal" Spacing="8"> |
| 125 | + <Button Content="New" /> |
| 126 | + <Button Content="Refresh" /> |
| 127 | + </StackPanel> |
| 128 | + </StackPanel> |
| 129 | +</Panel> |
| 130 | +``` |
| 131 | + |
| 132 | +### Combining queries |
| 133 | + |
| 134 | +Combine multiple conditions in a single query using `and` (all conditions must match) or `,` (any condition can match): |
| 135 | + |
| 136 | +```xml |
| 137 | +<!-- Both conditions must be true --> |
| 138 | +<ContainerQuery Name="main" Query="min-width:400 and max-width:800"> |
| 139 | + <!-- styles for medium widths --> |
| 140 | +</ContainerQuery> |
| 141 | + |
| 142 | +<!-- Either condition can be true --> |
| 143 | +<ContainerQuery Name="main" Query="max-width:300,min-height:600"> |
| 144 | + <!-- styles for narrow OR tall containers --> |
| 145 | +</ContainerQuery> |
| 146 | +``` |
| 147 | + |
| 148 | +For the full query syntax, available query types, and restrictions, see [Container queries](/docs/styling/container-queries). |
| 149 | + |
| 150 | +:::tip |
| 151 | +When the `TopLevel` (your window or main view) is set as a container, container queries behave like CSS media queries, responding to the window size itself. |
| 152 | +::: |
| 153 | + |
| 154 | +## OnFormFactor |
| 155 | + |
| 156 | +The `OnFormFactor` markup extension selects a value based on the device type. It resolves once at startup, so it does not respond to window resizing at runtime: |
| 157 | + |
| 158 | +```xml |
| 159 | +<Grid ColumnDefinitions="{OnFormFactor Desktop='250,*', Mobile='*'}"> |
| 160 | + <Border Grid.Column="0" |
| 161 | + IsVisible="{OnFormFactor Desktop=True, Mobile=False}"> |
| 162 | + <ListBox ItemsSource="{Binding MenuItems}" /> |
| 163 | + </Border> |
| 164 | + <ContentControl Grid.Column="{OnFormFactor Desktop=1, Mobile=0}" |
| 165 | + Content="{Binding CurrentPage}" /> |
| 166 | +</Grid> |
| 167 | +``` |
| 168 | + |
| 169 | +Use `OnFormFactor` when your desktop and mobile layouts are structurally different and you do not need to respond to live resizing. For layouts that must adapt as the user resizes the window, use container queries instead. |
| 170 | + |
| 171 | +## Reflowing panels |
| 172 | + |
| 173 | +Some panels automatically reflow their children based on available space without requiring queries or code. |
| 174 | + |
| 175 | +**`WrapPanel`** arranges children in a row and wraps to the next line when the edge of the panel is reached: |
| 176 | + |
| 177 | +```xml |
| 178 | +<WrapPanel Orientation="Horizontal"> |
| 179 | + <Button Content="One" Margin="4" /> |
| 180 | + <Button Content="Two" Margin="4" /> |
| 181 | + <Button Content="Three" Margin="4" /> |
| 182 | + <!-- wraps to the next row when the panel is too narrow --> |
| 183 | +</WrapPanel> |
| 184 | +``` |
| 185 | + |
| 186 | +**`UniformGridLayout`** (used with `ItemsRepeater`) calculates column count from the available width and a minimum item size: |
| 187 | + |
| 188 | +```xml |
| 189 | +<ItemsRepeater ItemsSource="{Binding Cards}"> |
| 190 | + <ItemsRepeater.Layout> |
| 191 | + <UniformGridLayout MinItemWidth="280" |
| 192 | + MinItemHeight="200" |
| 193 | + MinColumnSpacing="12" |
| 194 | + MinRowSpacing="12" /> |
| 195 | + </ItemsRepeater.Layout> |
| 196 | + <ItemsRepeater.ItemTemplate> |
| 197 | + <DataTemplate> |
| 198 | + <Border Padding="16" CornerRadius="8" |
| 199 | + Background="White" |
| 200 | + BorderBrush="#E5E7EB" BorderThickness="1"> |
| 201 | + <TextBlock Text="{Binding Title}" /> |
| 202 | + </Border> |
| 203 | + </DataTemplate> |
| 204 | + </ItemsRepeater.ItemTemplate> |
| 205 | +</ItemsRepeater> |
| 206 | +``` |
| 207 | + |
| 208 | +These panels are a good choice when you need flowing content without explicit breakpoints. |
| 209 | + |
| 210 | +## Breakpoint view models |
| 211 | + |
| 212 | +When your responsive logic involves multiple coordinated property changes or conditions beyond size (such as combining orientation and platform checks), you can observe the window width in your view model and expose boolean properties for each tier: |
| 213 | + |
| 214 | +```csharp |
| 215 | +public partial class MainViewModel : ObservableObject |
| 216 | +{ |
| 217 | + [ObservableProperty] |
| 218 | + private bool _isCompact; |
| 219 | + |
| 220 | + [ObservableProperty] |
| 221 | + private bool _isWide; |
| 222 | + |
| 223 | + public void UpdateLayout(double windowWidth) |
| 224 | + { |
| 225 | + IsCompact = windowWidth < 640; |
| 226 | + IsWide = windowWidth >= 1024; |
| 227 | + } |
| 228 | +} |
| 229 | +``` |
| 230 | + |
| 231 | +Call `UpdateLayout` from the window's size-changed handler: |
| 232 | + |
| 233 | +```csharp |
| 234 | +protected override void OnSizeChanged(SizeChangedEventArgs e) |
| 235 | +{ |
| 236 | + base.OnSizeChanged(e); |
| 237 | + if (DataContext is MainViewModel vm) |
| 238 | + vm.UpdateLayout(e.NewSize.Width); |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | +Then bind layout properties to the breakpoint flags: |
| 243 | + |
| 244 | +```xml |
| 245 | +<StackPanel IsVisible="{Binding IsCompact}" Spacing="8"> |
| 246 | + <views:SidebarView /> |
| 247 | + <views:ContentView /> |
| 248 | +</StackPanel> |
| 249 | + |
| 250 | +<Grid IsVisible="{Binding !IsCompact}" ColumnDefinitions="280,*"> |
| 251 | + <views:SidebarView Grid.Column="0" /> |
| 252 | + <views:ContentView Grid.Column="1" /> |
| 253 | +</Grid> |
| 254 | +``` |
| 255 | + |
| 256 | +This approach provides full programmatic control but requires code-behind or view model wiring. Prefer container queries when your transitions are purely size-based and can be expressed in XAML. |
| 257 | + |
| 258 | +## Choosing an approach |
| 259 | + |
| 260 | +Use the following decision process to select the right technique: |
| 261 | + |
| 262 | +1. **Does your component need to adapt based on its own size (not the window)?** Use container queries. This keeps the component self-contained and reusable. |
| 263 | +2. **Are desktop and mobile layouts structurally different, with no need for live resizing?** Use `OnFormFactor`. |
| 264 | +3. **Do you have a collection of items that should reflow into rows?** Use `WrapPanel` or `UniformGridLayout`. |
| 265 | +4. **Does your transition logic involve multiple conditions, platform checks, or non-size triggers?** Use breakpoint view models. |
| 266 | + |
| 267 | +You can combine these techniques. For example, use `OnFormFactor` for a top-level structural difference (sidebar vs. bottom tabs), then use container queries within individual panels so they adapt to their actual rendered size. |
| 268 | + |
| 269 | +## See also |
| 270 | + |
| 271 | +- [Container queries](/docs/styling/container-queries): Full query syntax, container sizing modes, and restrictions. |
| 272 | +- [How to: Build responsive layouts](/docs/how-to/responsive-layout-how-to): Step-by-step recipes for common responsive patterns. |
| 273 | +- [Layout](/docs/layout/layout): How the Avalonia measure and arrange system works. |
| 274 | +- [Choosing a layout panel](/docs/layout/choosing-a-layout-panel): Picking the right panel for your scenario. |
0 commit comments