Advanced PowerApps Controls and Galleries: Nested Patterns and Responsive Design

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

  1. Nested Galleries: Limit to 2 levels, use lazy loading
  2. Flexible Height: Enable AutoHeight sparingly (performance cost)
  3. Responsive Design: Define breakpoints (mobile <768, tablet <1024, desktop ≥1024)
  4. Components: Create for repeated patterns (cards, forms, buttons)
  5. Modern Controls: Use for new apps (better performance)
  6. 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.