Build a Reusable Nested Gallery in Minutes with YAML in PowerApps

Person playing a red accordion

Build a Reusable Nested Gallery in Minutes with YAML in PowerApps

Nested galleries (sometimes called accordion galleries) are a great way to group related data into a hierarchical view. In this post I’ll explain the logic behind a nested gallery and give you YAML you can paste straight into PowerApps Studio to get your own nested gallery structure in seconds.

Animated demo of a PowerApps nested gallery expanding and collapsing client rows
PowerApps nested gallery in action!

What We’re Building

My example data is a list of clients who each have many bookings. In SharePoint you would have two lists: one for Clients and one for Bookings, with a lookup column on Bookings that points back to its client.

The parent gallery shows one row per client. Selecting a row expands it to show that client’s bookings underneath: Status, Start and End. Selecting it again collapses it, and an icon in the header expands or collapses every row at once.

To keep this easy to follow, I’ll use two collections as the sample data. At the end I’ll show how to swap them for your own lists.

Sample Data

First, create the sample data. In your app, set the OnStart property of the App. It creates a colClients collection with six clients and a colBookings collection with their bookings:

ClearCollect(
    colClients,
    {Client: "Ava Morrison"},
    {Client: "Jackson Reed"},
    {Client: "Lena Patel"},
    {Client: "Marcus Nguyen"},
    {Client: "Sophie Bennett"},
    {Client: "Ethan Brooks"}
);
ClearCollect(
    colBookings,
    {Client: Index(colClients, 1).Client, Status: "Cancelled", StartDate: "8/18/2024", EndDate: "8/25/2024"},
    {Client: Index(colClients, 2).Client, Status: "Cancelled", StartDate: "8/20/2024", EndDate: "8/21/2024"},
    {Client: Index(colClients, 3).Client, Status: "Booked", StartDate: "11/13/2024", EndDate: "11/13/2024"},
    {Client: Index(colClients, 4).Client, Status: "Booked", StartDate: "8/18/2024", EndDate: "8/18/2024"},
    {Client: Index(colClients, 4).Client, Status: "Booked", StartDate: "11/13/2024", EndDate: "11/13/2024"},
    {Client: Index(colClients, 5).Client, Status: "Booked", StartDate: "11/14/2024", EndDate: "11/14/2024"}
)

The dates are plain text here to keep the example simple. In a real app they would be date columns. The bookings pull each client name from colClients with Index(colClients, 1).Client, Index(colClients, 2).Client and so on, so every name is typed only once. Index looks the record up by its position in the collection, which is fine for sample data like this. Both collections end up with a Client column, and that’s the link between the parent and child galleries. (Ethan Brooks has no bookings, so his row will open to an empty list.)

Gallery Setup

1. Add the header

In a vertical container, add a horizontal container for the headers. Add three labels (Status, Start and End) and an icon for expanding and collapsing all rows.

2. Add the parent gallery

Below the header, add a blank flexible height gallery. This is the Clients gallery. Set its Items property to colClients and set its TemplatePadding to 0.

3. Add the parent row

In the Clients gallery, add a horizontal container and set its Width to Parent.TemplateWidth and its Height to 40. Inside it, add a modern button and set:

  • Width to Parent.Width and Height to Parent.Height
  • Appearance to Subtle and Align to Left, so it looks like a row instead of a button

4. Add the child gallery

Inside the same Clients gallery, add a second blank flexible height gallery. This one shows the bookings. Set its Width to Parent.TemplateWidth and its Y to the height of the parent row (40 in this case), so it sits directly under it.

5. Filter the child gallery

The child gallery’s Items property should return only the bookings for the current client:

Filter(colBookings, Client = ThisItem.Client)

6. Add the child row

In the child gallery, add a horizontal container with a Width of Parent.TemplateWidth and a Height of 40. Add three labels for ThisItem.Status, ThisItem.StartDate and ThisItem.EndDate, lined up with the header labels from step 1.

7. Show the booking count

Back on the parent button, set the Text property to:

$"Client: {ThisItem.Client} ({CountRows(galChild.AllItems)})"

This shows the client name and the number of bookings under that client.

8. Size the child gallery to fit

A flexible height gallery inside a gallery doesn’t grow on its own, so set the child gallery’s Height from its item count:

CountRows(Self.AllItems) * Self.TemplateHeight + (Self.TemplatePadding * 3)

Toggling Gallery Items

The expand and collapse behavior comes from two context variables:

  • ctxRowSelect holds the client whose row is currently open.
  • ctxExpandGal is true when the expand-all icon has been used.

Opening one row. The parent button’s OnSelect sets ctxRowSelect to the selected client, or clears it if that client was already open, and turns expand-all off:

UpdateContext(
    {
        ctxRowSelect: If(
            ctxRowSelect = ThisItem.Client,
            Blank(),
            ThisItem.Client
        ),
        ctxExpandGal: false
    }
)

Selecting a different client replaces ctxRowSelect, so the previous row collapses automatically.

Showing the child gallery. The child gallery’s Visible property shows it for the open row, or for every row when expand-all is on:

ctxRowSelect = ThisItem.Client || ctxExpandGal

The chevron. The parent button’s Icon property uses the same test, so the chevron points down when the row is open:

If(ctxRowSelect = ThisItem.Client || ctxExpandGal, "ChevronDown", "ChevronRight")

Expand or collapse everything. The icon in the header flips ctxExpandGal and clears the single-row selection:

UpdateContext(
    {
        ctxExpandGal: !ctxExpandGal,
        ctxRowSelect: Blank()
    }
)

Working with Your Own Data

To connect your own data, replace the two collections:

  • Replace colClients in the parent gallery’s Items with your Clients list.
  • Replace colBookings in the child gallery’s Items with your Bookings list. With a SharePoint lookup column, the filter would look like Filter(Bookings, Client.Value = ThisItem.Title).
  • Update the labels, and the ThisItem.Client references, to match your column names.

A few delegation notes:

  • Keep the parent gallery on a plain list (your Clients list) and avoid building it with GroupBy over the Bookings list. GroupBy isn’t delegable, so it only processes the first 500 rows (up to 2,000 if you raise the data row limit).
  • Each parent row runs its own query for its child gallery, so a very large number of clients means a lot of queries. This pattern works best for dozens of parents, not thousands.
  • Make sure the filter on the child gallery is delegable for your data source.

Get the Code

To use it, first create the sample collections from the Sample Data section above. Then add a blank screen to a canvas app, select it, and paste this YAML with Ctrl+V.

- con_Flex_Gal_Main:
    Control: GroupContainer@1.3.0
    Variant: AutoLayout
    Properties:
      DropShadow: =DropShadow.None
      Height: =Parent.Height
      LayoutDirection: =LayoutDirection.Vertical
      PaddingBottom: =15
      PaddingLeft: =15
      PaddingRight: =15
      PaddingTop: =15
      RadiusBottomLeft: =0
      RadiusBottomRight: =0
      RadiusTopLeft: =0
      RadiusTopRight: =0
      Width: =Parent.Width
    Children:
      - con_Flex_Gal_Header:
          Control: GroupContainer@1.3.0
          Variant: AutoLayout
          Properties:
            DropShadow: =DropShadow.None
            Fill: =App.Theme.Colors.Darker20
            FillPortions: =0
            Height: =35
            LayoutAlignItems: =LayoutAlignItems.Center
            LayoutDirection: =LayoutDirection.Horizontal
            LayoutGap: =5
            PaddingBottom: =Self.LayoutGap
            PaddingLeft: =Self.LayoutGap
            PaddingRight: =Self.LayoutGap
            PaddingTop: =Self.LayoutGap
            RadiusBottomLeft: =0
            RadiusBottomRight: =0
            RadiusTopLeft: =0
            RadiusTopRight: =0
          Children:
            - txt_Header_Status:
                Control: Text@0.0.51
                Properties:
                  Align: ='TextCanvas.Align'.Center
                  FillPortions: =1
                  FontColor: =RGBA(255, 255, 255, 1)
                  PaddingBottom: =0
                  PaddingLeft: =5
                  PaddingRight: =5
                  PaddingTop: =0
                  Size: =18
                  Text: ="Status"
                  VerticalAlign: =VerticalAlign.Middle
                  Weight: ='TextCanvas.Weight'.Semibold
            - txt_Header_Start:
                Control: Text@0.0.51
                Properties:
                  Align: ='TextCanvas.Align'.Center
                  FillPortions: =1
                  FontColor: =RGBA(255, 255, 255, 1)
                  PaddingBottom: =0
                  PaddingLeft: =5
                  PaddingRight: =5
                  PaddingTop: =0
                  Size: =18
                  Text: ="Start"
                  VerticalAlign: =VerticalAlign.Middle
                  Weight: ='TextCanvas.Weight'.Semibold
            - txt_Header_End:
                Control: Text@0.0.51
                Properties:
                  Align: ='TextCanvas.Align'.Center
                  FillPortions: =1
                  FontColor: =RGBA(255, 255, 255, 1)
                  PaddingBottom: =0
                  PaddingLeft: =5
                  PaddingRight: =5
                  PaddingTop: =0
                  Size: =18
                  Text: ="End"
                  VerticalAlign: =VerticalAlign.Middle
                  Weight: ='TextCanvas.Weight'.Semibold
            - ico_Expand:
                Control: Classic/Icon@2.5.0
                Properties:
                  Color: =Color.White
                  Height: =Parent.Height
                  Icon: =Icon.ExpandView
                  OnSelect: |-
                    =UpdateContext(
                        {
                            ctxExpandGal: !ctxExpandGal,
                            ctxRowSelect: Blank()
                        }
                    )
                  PaddingBottom: =5
                  PaddingLeft: =5
                  PaddingRight: =5
                  PaddingTop: =5
                  Width: =Self.Height
                  X: =730
                  Y: =40
      - con_Flex_Gal_Body:
          Control: GroupContainer@1.3.0
          Variant: AutoLayout
          Properties:
            DropShadow: =DropShadow.None
            LayoutDirection: =LayoutDirection.Vertical
            PaddingBottom: =5
            PaddingLeft: =5
            PaddingRight: =5
            PaddingTop: =5
            RadiusBottomLeft: =0
            RadiusBottomRight: =0
            RadiusTopLeft: =0
            RadiusTopRight: =0
          Children:
            - galParent:
                Control: Gallery@2.15.0
                Variant: VariableHeight
                Properties:
                  BorderColor: =App.Theme.Colors.Primary
                  Items: =colClients
                  TemplatePadding: =0
                  X: =726
                  Y: =136
                Children:
                  - galChild:
                      Control: Gallery@2.15.0
                      Variant: VariableHeight
                      Properties:
                        BorderColor: =App.Theme.Colors.Primary
                        DelayItemLoading: =false
                        Height: =CountRows(Self.AllItems) * Self.TemplateHeight + (Self.TemplatePadding *3)
                        Items: =Filter(colBookings, Client = ThisItem.Client)
                        LoadingSpinner: =LoadingSpinner.None
                        TemplateSize: =40
                        Visible: =ctxRowSelect = ThisItem.Client || ctxExpandGal
                        Width: =Parent.TemplateWidth
                        Y: =btn_Parent_Client.Height
                      Children:
                        - con_Child_Row:
                            Control: GroupContainer@1.3.0
                            Variant: AutoLayout
                            Properties:
                              DropShadow: =DropShadow.None
                              Fill: =Color.White
                              Height: =40
                              LayoutAlignItems: =LayoutAlignItems.Center
                              LayoutDirection: =LayoutDirection.Horizontal
                              LayoutGap: =5
                              PaddingBottom: =5
                              PaddingLeft: =5
                              PaddingRight: =5
                              PaddingTop: =5
                              RadiusBottomLeft: =0
                              RadiusBottomRight: =0
                              RadiusTopLeft: =0
                              RadiusTopRight: =0
                              Width: =Parent.TemplateWidth
                            Children:
                              - txt_Child_Status:
                                  Control: Text@0.0.51
                                  Properties:
                                    Align: ='TextCanvas.Align'.Center
                                    FillPortions: =1
                                    PaddingBottom: =0
                                    PaddingLeft: =5
                                    PaddingRight: =5
                                    PaddingTop: =0
                                    Text: =ThisItem.Status
                                    VerticalAlign: =VerticalAlign.Middle
                              - txt_Child_Start:
                                  Control: Text@0.0.51
                                  Properties:
                                    Align: ='TextCanvas.Align'.Center
                                    FillPortions: =1
                                    PaddingBottom: =0
                                    PaddingLeft: =5
                                    PaddingRight: =5
                                    PaddingTop: =0
                                    Text: =ThisItem.StartDate
                                    VerticalAlign: =VerticalAlign.Middle
                              - txt_Child_End:
                                  Control: Text@0.0.51
                                  Properties:
                                    Align: ='TextCanvas.Align'.Center
                                    FillPortions: =1
                                    PaddingBottom: =0
                                    PaddingLeft: =5
                                    PaddingRight: =5
                                    PaddingTop: =0
                                    Text: =ThisItem.EndDate
                                    VerticalAlign: =VerticalAlign.Middle
                              - btn_Row_Edit:
                                  Control: Button@0.0.45
                                  Properties:
                                    Appearance: ='ButtonCanvas.Appearance'.Transparent
                                    Height: =Parent.Height
                                    Icon: ="ChevronRight"
                                    Layout: ='ButtonCanvas.Layout'.IconOnly
                                    OnSelect: =Notify($"{ThisItem.Client} record selected",NotificationType.Information,2000)
                                    Text: ="Edit"
                                    Width: =Self.Height
                  - con_Parent_Row:
                      Control: GroupContainer@1.3.0
                      Variant: AutoLayout
                      Properties:
                        BorderColor: =RGBA(244,244,244,1)
                        Fill: =RGBA(244,244,244,1)
                        Height: =40
                        LayoutDirection: =LayoutDirection.Horizontal
                        RadiusBottomLeft: =0
                        RadiusBottomRight: =0
                        RadiusTopLeft: =0
                        RadiusTopRight: =0
                        Width: =Parent.TemplateWidth
                      Children:
                        - btn_Parent_Client:
                            Control: Button@0.0.45
                            Properties:
                              Align: =Align.Left
                              Appearance: ='ButtonCanvas.Appearance'.Subtle
                              BorderRadiusBottomLeft: '=0   '
                              BorderRadiusBottomRight: =0
                              BorderRadiusTopLeft: =0
                              BorderRadiusTopRight: =0
                              FontSize: =15
                              Height: =Parent.Height
                              Icon: =If(ctxRowSelect = ThisItem.Client || ctxExpandGal,"ChevronDown","ChevronRight")
                              IconStyle: ='ButtonCanvas.IconStyle'.Filled
                              Layout: ='ButtonCanvas.Layout'.IconBefore
                              OnSelect: |-
                                =
                                UpdateContext(
                                    {
                                        ctxRowSelect: If(
                                            ctxRowSelect = ThisItem.Client,
                                            Blank(),
                                            ThisItem.Client
                                        ),
                                        ctxExpandGal: false
                                    }
                                )
                              Text: |-
                                =$"Client: {ThisItem.Client} ({CountRows(galChild.AllItems)})"
                              Width: =Parent.Width

Wrap Up

A nested gallery gives you a clean parent and child view without extra screens: one gallery nested in another, a filter that links them, and two context variables that control what’s open. Swap in your own lists, adjust the columns, and you have a reusable accordion you can drop into any app.

Leave a Reply