Advanced PowerApps Controls and Galleries: Nested Patterns and Responsive Design
Introduction
Basic galleries display flat lists, but enterprise apps require master-detail views, dynamic item heights, and responsive layouts. This guide covers nested galleries for hierarchical data, flexible height galleries with wrapping content, responsive design using containers and formulas, custom components for reusable UI patterns, and modern controls for adaptive experiences.
Nested Galleries
Master-Detail Pattern
Scenario: Display customers with their orders
// Gallery_Customers (outer gallery)
Items: Customers
// Gallery_Orders (nested inside Gallery_Customers template)
Items: Filter(Orders, CustomerID = ThisItem.ID)
Layout:
┌─────────────────────────────────────┐
│ Customer: Contoso Ltd │
│ ├─ Order #1001 - $500 - 01/15/2025 │
│ ├─ Order #1002 - $750 - 01/18/2025 │
│ └─ Order #1003 - $300 - 01/20/2025 │
├─────────────────────────────────────┤
│ Customer: Fabrikam Inc │
│ ├─ Order #2001 - $1200 - 01/12/2025│
│ └─ Order #2002 - $900 - 01/19/2025 │
└─────────────────────────────────────┘
Implementation:
// Outer gallery: Gallery_Customers
Items: Customers
TemplateSize: 150 + Gallery_Orders.Height
Layout: Vertical
// Outer gallery template contains:
Label_CustomerName.Text: ThisItem.CompanyName
// Inner gallery: Gallery_Orders (inside Gallery_Customers template)
Items: Filter(Orders, CustomerID = Gallery_Customers.Selected.ID)
Height: CountRows(Self.AllItems) * 60 + 10
TemplateSize: 60
Layout: Vertical
// Inner gallery template contains:
Label_OrderNumber.Text: ThisItem.OrderNumber
Label_Amount.Text: Text(ThisItem.Amount, "$#,##0.00")
Label_Date.Text: Text(ThisItem.OrderDate, "mm/dd/yyyy")
Performance Optimization
Problem: Nested galleries can be slow with large datasets
Solution 1: Limit inner gallery items
// Show only first 5 orders per customer
Gallery_Orders.Items:
FirstN(
Filter(Orders, CustomerID = Gallery_Customers.Selected.ID),
5
)
// Add "Show More" button if more than 5
Button_ShowMore.Visible:
CountRows(Filter(Orders, CustomerID = Gallery_Customers.Selected.ID)) > 5
Button_ShowMore.OnSelect:
Navigate(Screen_CustomerOrders, ScreenTransition.Fade, {selectedCustomer: Gallery_Customers.Selected})
Solution 2: Lazy loading with collections
// On customer selection, load orders into collection
Gallery_Customers.OnSelect:
ClearCollect(
colCustomerOrders,
Filter(Orders, CustomerID = ThisItem.ID)
)
// Inner gallery uses collection (faster than Filter each time)
Gallery_Orders.Items: colCustomerOrders
Solution 3: Delegation-friendly structure
// Instead of nested galleries, use single gallery with grouping
Gallery_OrdersGrouped.Items:
AddColumns(
Customers,
"Orders",
Filter(Orders, CustomerID = ID)
)
// Display customer name as header, orders as HTML table
HtmlText_OrdersList.HtmlText:
"<h3>" & ThisItem.CompanyName & "</h3>" &
"<table>" &
Concat(
ThisItem.Orders,
"<tr><td>" & OrderNumber & "</td><td>" & Text(Amount, "$#,##0") & "</td></tr>"
) &
"</table>"
Three-Level Nesting
Scenario: Categories → Products → Reviews
// Level 1: Gallery_Categories
Items: Categories
TemplateSize: 200 + Gallery_Products.Height
Label_CategoryName.Text: ThisItem.Name
// Level 2: Gallery_Products (inside Gallery_Categories)
Items: Filter(Products, CategoryID = Gallery_Categories.Selected.ID)
Height: CountRows(Self.AllItems) * 120 + 10
TemplateSize: 120
Label_ProductName.Text: ThisItem.Name
Label_Rating.Text: ThisItem.AverageRating & " ⭐"
// Level 3: Gallery_Reviews (inside Gallery_Products)
Items: FirstN(Filter(Reviews, ProductID = Gallery_Products.Selected.ID), 3)
Height: CountRows(Self.AllItems) * 80 + 10
TemplateSize: 80
Label_ReviewerName.Text: ThisItem.Reviewer
Label_ReviewText.Text: ThisItem.Comment
Label_ReviewDate.Text: Text(ThisItem.Date, "mm/dd/yyyy")
Warning: Three-level nesting is computationally expensive. Consider alternatives:
- Expandable/collapsible sections (only load on expand)
- Navigate to separate screen for third level
- Use HTML text for static display (no interactivity)
Flexible Height Galleries
Dynamic Item Height
Problem: Gallery items have variable content (short vs long descriptions)
Solution: TemplateHeight formula
Gallery_Products.Items: Products
Gallery_Products.TemplateSize:
40 + // Header height (name, price)
Label_Description.Height + // Dynamic description
20 // Padding
// Description label with AutoHeight
Label_Description.AutoHeight: true
Label_Description.Text: ThisItem.Description
Label_Description.Width: Gallery_Products.Width - 40
Label_Description.Height:
// Calculated by PowerApps based on text length
Result:
┌─────────────────────────────────┐
│ Product A - $50 │
│ Short description text. │
└─────────────────────────────────┘ ← Height: 80px
┌─────────────────────────────────┐
│ Product B - $75 │
│ This is a much longer │
│ description that wraps across │
│ multiple lines and requires │
│ more vertical space. │
└─────────────────────────────────┘ ← Height: 140px
Wrapping Content
Horizontal gallery with wrap:
// Use Horizontal Wrap layout (modern galleries)
Gallery_Tags.Layout: Layout.Wrap
Gallery_Tags.Items: ["Azure", "PowerApps", "Dataverse", "Power Automate", "SharePoint"]
// Each item is a tag button
Button_Tag.Width: Self.Width // Auto-sized to content
Button_Tag.Height: 40
Button_Tag.Text: ThisItem.Value
Button_Tag.AutoWidth: true
Result:
[Azure] [PowerApps] [Dataverse]
[Power Automate] [SharePoint]
Card-based layout:
// Responsive card gallery
Gallery_Products.Items: Products
Gallery_Products.TemplateSize: App.Width / 3 - 20 // 3 columns on desktop
Gallery_Products.Layout: Horizontal Wrap
// Each card
Rectangle_Card.Width: Gallery_Products.TemplateSize - 10
Rectangle_Card.Height: 250
Image_Product.Width: Rectangle_Card.Width - 20
Label_Name.Text: ThisItem.Name
Label_Price.Text: Text(ThisItem.Price, "$#,##0.00")
Result (desktop 1200px width):
┌────┐ ┌────┐ ┌────┐
│ A │ │ B │ │ C │
└────┘ └────┘ └────┘
┌────┐ ┌────┐ ┌────┐
│ D │ │ E │ │ F │
└────┘ └────┘ └────┘
Responsive Design
Container Controls
Horizontal container (row layout):
Container_Header:
├── LayoutDirection: Horizontal
├── FlexWrap: Wrap
├── AlignInContainer: Stretch
└── Children:
├── Image_Logo (Width: 100, AlignSelf: Start)
├── Label_Title (Width: Fill, AlignSelf: Center)
└── Button_Profile (Width: 80, AlignSelf: End)
Result (desktop):
[Logo] [Dashboard Title..................] [Profile]
Result (mobile, wraps at 600px):
[Logo] [Dashboard Title]
[Profile]
Vertical container (column layout):
Container_Form:
├── LayoutDirection: Vertical
├── Gap: 10
└── Children:
├── TextInput_Name (Height: 40, Width: Fill)
├── TextInput_Email (Height: 40, Width: Fill)
├── Dropdown_Department (Height: 40, Width: Fill)
└── Button_Submit (Height: 50, Width: Fill)
Responsive Formulas
Screen size detection:
// Global variables on App.OnStart
Set(varScreenWidth, App.Width);
Set(varScreenHeight, App.Height);
Set(varIsMobile, App.Width < 768);
Set(varIsTablet, App.Width >= 768 && App.Width < 1024);
Set(varIsDesktop, App.Width >= 1024);
// Responsive layout properties
Gallery_Products.Columns:
If(varIsMobile, 1, varIsTablet, 2, 3)
Container_Sidebar.Visible: !varIsMobile
Label_Title.Size:
If(varIsMobile, 18, varIsTablet, 22, 28)
Container_Header.LayoutDirection:
If(varIsMobile, Layout.Vertical, Layout.Horizontal)
Breakpoint-based styling:
// Responsive padding
Container_Main.PaddingLeft:
If(App.Width < 768, 10, App.Width < 1024, 20, 40)
// Responsive font sizes
Label_Heading.Size:
Switch(
true,
App.Width < 600, 16,
App.Width < 900, 20,
App.Width < 1200, 24,
28 // Default for >1200px
)
// Responsive gallery template size
Gallery_Items.TemplateSize:
If(App.Width < 768, 80, 120)
Orientation Changes
Detect and respond:
// App.OnStart
Set(varOrientation, If(App.Width > App.Height, "Landscape", "Portrait"));
// Screen.OnVisible
If(
(App.Width > App.Height && varOrientation = "Portrait") ||
(App.Width < App.Height && varOrientation = "Landscape"),
Set(varOrientation, If(App.Width > App.Height, "Landscape", "Portrait"));
// Refresh gallery layouts
Reset(Gallery_Products)
)
// Layout adjustments
Gallery_Products.Layout:
If(varOrientation = "Landscape",
Layout.Horizontal,
Layout.Vertical
)
Custom Components
Reusable Card Component
Component: ProductCard
Input properties:
ProductID (Number)
ProductName (Text)
ProductPrice (Number)
ProductImage (Text)
ProductRating (Number)
Output properties:
OnCardClick (Action)
SelectedProduct (Record)
Component implementation:
// Inside ProductCard component
Rectangle_Container:
├── Width: 200
├── Height: 280
└── OnSelect: Set(locSelectedProduct, {ID: ProductID, Name: ProductName, Price: ProductPrice})
Image_Product:
├── Image: ProductImage
├── Width: 180
└── Height: 150
Label_Name:
├── Text: ProductName
├── Width: 180
└── Font: Font.'Segoe UI Semibold'
Label_Price:
├── Text: Text(ProductPrice, "$#,##0.00")
└── Color: RGBA(0, 120, 212, 1)
Rating_Stars:
├── Default: ProductRating
└── ReadOnly: true
Button_AddToCart:
├── Text: "Add to Cart"
└── OnSelect: ProductCard.OnCardClick
Using the component:
// In gallery
Gallery_Products.Items: Products
// ProductCard instance inside gallery template
ProductCard_1.ProductID: ThisItem.ID
ProductCard_1.ProductName: ThisItem.Name
ProductCard_1.ProductPrice: ThisItem.Price
ProductCard_1.ProductImage: ThisItem.ImageURL
ProductCard_1.ProductRating: ThisItem.Rating
ProductCard_1.OnCardClick:
Patch(
ShoppingCart,
Defaults(ShoppingCart),
{
ProductID: ProductCard_1.SelectedProduct.ID,
Quantity: 1,
User: User().Email
}
);
Notify("Added to cart", NotificationType.Success)
Form Component
Component: DynamicForm
Input properties:
FormSchema (Table) - Column definitions
FormData (Record) - Initial values
FormMode (Text) - "New", "Edit", "View"
Output properties:
FormValues (Record) - Current form data
IsValid (Boolean) - All validations passed
OnSubmit (Action)
Component logic:
// Generate fields dynamically from schema
Gallery_FormFields.Items: FormSchema
// Each field renders based on type
If(
ThisItem.Type = "Text",
TextInput_Field,
ThisItem.Type = "Number",
TextInput_Number,
ThisItem.Type = "Date",
DatePicker_Field,
ThisItem.Type = "Choice",
Dropdown_Field,
Label_Unknown
)
// Collect form values
Button_Submit.OnSelect:
Set(
locFormValues,
ForAll(
Gallery_FormFields.AllItems,
{
Field: ThisRecord.FieldName,
Value: Switch(
ThisRecord.Type,
"Text", TextInput_Field.Text,
"Number", Value(TextInput_Number.Text),
"Date", DatePicker_Field.SelectedDate,
"Choice", Dropdown_Field.Selected.Value
)
}
)
);
DynamicForm.OnSubmit
Usage:
// Define schema
ClearCollect(
colCaseSchema,
{FieldName: "Title", Label: "Case Title", Type: "Text", Required: true},
{FieldName: "Priority", Label: "Priority", Type: "Choice", Required: true, Choices: ["High", "Normal", "Low"]},
{FieldName: "Description", Label: "Description", Type: "Text", Required: false},
{FieldName: "DueDate", Label: "Due Date", Type: "Date", Required: false}
)
// Use component
DynamicForm_1.FormSchema: colCaseSchema
DynamicForm_1.FormMode: "New"
DynamicForm_1.OnSubmit:
Patch(
Cases,
Defaults(Cases),
DynamicForm_1.FormValues
);
Navigate(Screen_Success)
Modern Controls
Modern Gallery
Features:
- Built-in responsive layout
- Flex container support
- Better performance than classic gallery
ModernGallery_Products:
├── Items: Products
├── Layout: Flex (responsive columns)
├── MinItemWidth: 200
├── MaxItemWidth: 400
└── Gap: 20
// Automatically adjusts columns based on screen width
Desktop (1200px): 4 columns
Tablet (768px): 3 columns
Mobile (375px): 1 column
Modern Dropdown
Enhanced features:
ModernDropdown_Department:
├── Items: Departments
├── DefaultSelectedItems: LookUp(Departments, ID = User().DepartmentID)
├── MultiSelect: false
├── SearchFields: ["Name", "Code"]
├── OnChange: Set(varSelectedDept, Self.Selected)
Flex Container
Advanced layout:
FlexContainer_Dashboard:
├── LayoutDirection: Vertical
├── Gap: 20
├── AlignItems: Stretch
└── Children:
├── FlexContainer_Row1 (Horizontal, 3 stat cards)
├── FlexContainer_Row2 (Horizontal, chart + table)
└── FlexContainer_Row3 (Horizontal, 2 action buttons)
FlexContainer_Row1:
├── LayoutDirection: Horizontal
├── Gap: 15
├── AlignItems: Stretch
└── Children:
├── StatCard_TotalCases (Flex: 1)
├── StatCard_OpenCases (Flex: 1)
└── StatCard_ResolvedCases (Flex: 1)
// Each stat card takes equal width
Data Visualization Patterns
Custom Chart with Gallery
Bar chart:
// Data
ClearCollect(
colSalesData,
{Month: "Jan", Sales: 45000},
{Month: "Feb", Sales: 52000},
{Month: "Mar", Sales: 48000},
{Month: "Apr", Sales: 61000},
{Month: "May", Sales: 55000}
)
// Find max value for scaling
Set(varMaxSales, Max(colSalesData, Sales));
// Gallery with horizontal bars
Gallery_BarChart.Items: colSalesData
Gallery_BarChart.TemplateSize: 60
// Inside gallery template:
Label_Month.Text: ThisItem.Month
Rectangle_Bar:
├── Width: (ThisItem.Sales / varMaxSales) * 500
├── Height: 40
└── Fill: RGBA(0, 120, 212, 1)
Label_Value.Text: Text(ThisItem.Sales, "$#,##0")
Progress Indicator
// Circle progress
Circle_Outer (radius: 100, border: 10, color: LightGray)
Circle_Progress:
├── Arc shape (CSS clip-path emulation with rectangles)
├── Angle: (varProgress / 100) * 360
└── Color: If(varProgress < 50, Red, varProgress < 80, Orange, Green)
Label_Percentage:
├── Text: varProgress & "%"
└── Position: Center of circle
Accessibility
Focus Order
// Set TabIndex for logical navigation
TextInput_Name.TabIndex: 1
TextInput_Email.TabIndex: 2
Dropdown_Department.TabIndex: 3
Button_Submit.TabIndex: 4
Button_Cancel.TabIndex: 5
Screen Reader Labels
Gallery_Products.AccessibleLabel: "Product list, " & CountRows(Products) & " items"
Icon_Edit.AccessibleLabel: "Edit product " & Gallery_Products.Selected.Name
Button_Submit.AccessibleLabel: "Submit form, creates new case"
Keyboard Navigation
// Handle Enter key in text input
TextInput_Search.OnChange:
If(
IsMatch(TextInput_Search.Text, "\n$"), // Enter key pressed
Select(Button_Search) // Trigger search
)
// Escape key closes modal
Screen_Modal.OnVisible:
Set(varModalOpen, true)
If(varModalOpen && /* Escape key detection logic */,
Set(varModalOpen, false);
Back()
)
Best Practices
- Nested Galleries: Limit to 2 levels, use lazy loading
- Flexible Height: Enable AutoHeight sparingly (performance cost)
- Responsive Design: Define breakpoints (mobile <768, tablet <1024, desktop ≥1024)
- Components: Create for repeated patterns (cards, forms, buttons)
- Modern Controls: Use for new apps (better performance)
- Accessibility: Always set TabIndex and AccessibleLabel
Troubleshooting
Nested gallery not displaying:
- Check inner gallery Items formula references outer gallery correctly (
Gallery_Outer.Selected.ID) - Verify inner gallery Height is sufficient (should be dynamic based on item count)
- Ensure inner gallery TemplatePadding doesn't hide content
Flexible height gallery jumps/flickers:
- Reduce AutoHeight labels (CPU intensive)
- Use fixed heights where possible
- Implement virtualization (only render visible items)
Responsive layout breaks on device:
- Test App.Width values on actual devices (not just browser resize)
- Use Container controls instead of absolute positioning
- Check for hardcoded widths/heights
Key Takeaways
- Nested galleries enable master-detail views but require performance optimization
- Flexible height galleries with AutoHeight adapt to dynamic content
- Responsive design uses App.Width/App.Height with breakpoint formulas
- Custom components encapsulate reusable UI patterns
- Modern controls provide better performance and built-in responsiveness
- Accessibility requires TabIndex, AccessibleLabel, and keyboard support
Next Steps
- Explore Power Apps Component Framework (PCF) for code components
- Implement infinite scroll for large datasets
- Add swipe gestures for mobile interactions
- Build dashboard templates with modern controls
Additional Resources
Design once, adapt everywhere.