Data displayCanvas appAdvanced

Kanban board

A board with a column per status, card counts, cards with owner and due date, an overdue badge and buttons to move cards between columns.

Kanban board in Power Apps Studio
Version
1.0.0
Updated
Verified in Studio
Not yet
Tags
kanbanboardstatusworkflow

Overview

A task board in the style of Planner and Trello: one column per status, with the number of cards next to its name, and white cards with a title, a site, the owner's initials and the due date. A card that is past its due date and not done gets a red Overdue badge.

Each card has arrow buttons to move it to the previous or next column, so the board works with touch and keyboard, without drag and drop. Columns share the board's width (at least 260 pixels each) and are as tall as the fullest column, so the board never scrolls. In the example, moves are kept in a collection and are lost when the app closes; with your data, the arrow buttons patch the status.

When to use

  • Work that moves through a few fixed stages, such as inspections, requests or onboarding tasks.
  • Up to about five columns and twenty cards: the whole board fits on a tablet or desktop screen.

When not to use

  • Long lists that people search and sort: use a task table.
  • Phones: three columns don't fit side by side. Show one status at a time with a tab bar instead.

Controls used

ControlVersion
GroupContainer1.5.0
ModernText1.0.0
ModernButton1.0.0
Gallery2.15.0
ModernBadge1.0.0
ModernAvatar1.1.0

YAML

Works as is, with example data inline. Paste it into a screen or container; the steps are underHow to paste.

Not yet verified in Power Apps Studio. The control versions are drafted from documentation. If Studio flags one, see How to paste for the fix.

component.yaml
- KanbanBoardRoot:
    Control: GroupContainer@1.5.0
    Variant: AutoLayout
    Properties:
      DropShadow: =DropShadow.None
      Height: =KanbanBoardHeader.Height + KanbanBoardColumns.Height + 16 + 32
      LayoutDirection: =LayoutDirection.Vertical
      LayoutGap: =16
      PaddingBottom: =16
      PaddingLeft: =16
      PaddingRight: =16
      PaddingTop: =16
      Width: =Parent.Width
    Children:
      - KanbanBoardHeader:
          Control: GroupContainer@1.5.0
          Variant: AutoLayout
          Properties:
            AlignInContainer: =AlignInContainer.Stretch
            DropShadow: =DropShadow.None
            FillPortions: =0
            Height: =40
            LayoutAlignItems: =LayoutAlignItems.Center
            LayoutDirection: =LayoutDirection.Horizontal
            LayoutGap: =12
            LayoutMinHeight: =40
            LayoutMinWidth: =120
          Children:
            - KanbanBoardTitle:
                Control: ModernText@1.0.0
                Properties:
                  AccessibleLabel: =Self.Text
                  FontWeight: =FontWeight.Semibold
                  Height: =30
                  LayoutMinHeight: =0
                  LayoutMinWidth: =80
                  PaddingBottom: =0
                  PaddingLeft: =4
                  PaddingTop: =0
                  Size: =18
                  Text: ="Inspection board"
                  Width: =200
                  Wrap: =false
            - KanbanBoardSummary:
                Control: ModernText@1.0.0
                Properties:
                  AccessibleLabel: =Self.Text
                  Color: =ColorFade(App.Theme.Colors.Darker70, 45%)
                  FillPortions: =1
                  Height: =30
                  LayoutMinHeight: =0
                  LayoutMinWidth: =80
                  PaddingBottom: =0
                  PaddingTop: =0
                  Size: =13
                  Text: =Sum(KanbanBoardColumns.AllItems, CountRows(Cards)) & " inspections"
                  Wrap: =false
            - KanbanBoardAdd:
                Control: ModernButton@1.0.0
                Properties:
                  AccessibleLabel: =Self.Text
                  Appearance: =ButtonAppearance.Primary
                  Height: =36
                  Icon: ="Add"
                  LayoutMinHeight: =36
                  LayoutMinWidth: =96
                  OnSelect: =Notify("Add a card (example only).", NotificationType.Information)
                  Size: =14
                  Text: ="Add card"
                  Width: =120
      - KanbanBoardColumns:
          Control: Gallery@2.15.0
          Variant: Horizontal
          Properties:
            AccessibleLabel: ="Board columns"
            AlignInContainer: =AlignInContainer.Stretch
            BorderThickness: =0
            FillPortions: =0
            Height: =68 + Max(1, Max(Self.AllItems, CountRows(Cards))) * 114
            Items: |
              =With(
                  {
                      lanes: ["To do", "In progress", "Done"],
                      cards: Table(
                          {Id: 1, Title: "Fire safety audit", Site: "Warehouse Ghent", Owner: "Sara Peeters", Due: Today() + 2, Lane: "To do"},
                          {Id: 2, Title: "Forklift inspection", Site: "Depot Antwerp", Owner: "Tom Janssens", Due: Today() + 5, Lane: "To do"},
                          {Id: 3, Title: "Ladder certification", Site: "Office Leuven", Owner: "An De Smet", Due: Today() - 1, Lane: "To do"},
                          {Id: 4, Title: "Emergency lighting test", Site: "Warehouse Ghent", Owner: "Lucas Maes", Due: Today() + 1, Lane: "In progress"},
                          {Id: 5, Title: "First aid kit check", Site: "Depot Antwerp", Owner: "Sara Peeters", Due: Today() + 3, Lane: "In progress"},
                          {Id: 6, Title: "Sprinkler maintenance", Site: "Office Leuven", Owner: "Tom Janssens", Due: Today() - 4, Lane: "Done"}
                      )
                  },
                  ForAll(
                      Sequence(CountRows(lanes)) As lane,
                      {
                          Lane: Index(lanes, lane.Value).Value,
                          Cards: Filter(
                              ForAll(
                                  cards As c,
                                  {
                                      Id: c.Id,
                                      Title: c.Title,
                                      Site: c.Site,
                                      Owner: c.Owner,
                                      Due: c.Due,
                                      Lane: Coalesce(Last(Filter(colKanbanBoardMoves, Id = c.Id)).Lane, c.Lane),
                                      LaneIndex: lane.Value
                                  }
                              ),
                              Lane = Index(lanes, lane.Value).Value
                          )
                      }
                  )
              )
            LayoutMinHeight: =0
            LayoutMinWidth: =120
            ShowScrollbar: =false
            TabIndex: =0
            TemplatePadding: =8
            TemplateSize: =Max(260, (Self.Width - 8) / Max(1, Self.AllItemsCount) - 8)
          Children:
            - KanbanBoardColumn:
                Control: GroupContainer@1.5.0
                Variant: AutoLayout
                Properties:
                  DropShadow: =DropShadow.None
                  Fill: =ColorFade(App.Theme.Colors.Darker70, 93%)
                  Height: =Parent.TemplateHeight
                  LayoutDirection: =LayoutDirection.Vertical
                  LayoutGap: =8
                  PaddingBottom: =10
                  PaddingLeft: =10
                  PaddingRight: =10
                  PaddingTop: =10
                  RadiusBottomLeft: =14
                  RadiusBottomRight: =14
                  RadiusTopLeft: =14
                  RadiusTopRight: =14
                  Width: =Parent.TemplateWidth
                  X: =0
                  Y: =0
                Children:
                  - KanbanBoardColumnHeader:
                      Control: GroupContainer@1.5.0
                      Variant: AutoLayout
                      Properties:
                        AlignInContainer: =AlignInContainer.Stretch
                        DropShadow: =DropShadow.None
                        FillPortions: =0
                        Height: =32
                        LayoutAlignItems: =LayoutAlignItems.Center
                        LayoutDirection: =LayoutDirection.Horizontal
                        LayoutGap: =8
                        LayoutMinHeight: =32
                        LayoutMinWidth: =80
                      Children:
                        - KanbanBoardColumnTitle:
                            Control: ModernText@1.0.0
                            Properties:
                              AccessibleLabel: =Self.Text
                              FontWeight: =FontWeight.Semibold
                              Height: =30
                              LayoutMinHeight: =0
                              LayoutMinWidth: =40
                              PaddingBottom: =0
                              PaddingLeft: =4
                              PaddingTop: =0
                              Size: =14
                              Text: =ThisItem.Lane
                              Width: =Len(Self.Text) * 8 + 12
                              Wrap: =false
                        - KanbanBoardColumnCount:
                            Control: ModernBadge@1.0.0
                            Properties:
                              AccessibleLabel: =CountRows(ThisItem.Cards) & " cards"
                              Appearance: =BadgeAppearance.Tint
                              Content: =Text(CountRows(ThisItem.Cards))
                              Height: =22
                              Intent: =BadgeIntent.Neutral
                              LayoutMinHeight: =22
                              LayoutMinWidth: =28
                              Size: =12
                              Width: =28
                  - KanbanBoardEmpty:
                      Control: ModernText@1.0.0
                      Properties:
                        AccessibleLabel: =Self.Text
                        Align: =Align.Center
                        AlignInContainer: =AlignInContainer.Stretch
                        BorderColor: =ColorFade(App.Theme.Colors.Darker70, 80%)
                        BorderStyle: =BorderStyle.Dashed
                        BorderThickness: =1
                        Color: =ColorFade(App.Theme.Colors.Darker70, 45%)
                        Height: =64
                        LayoutMinHeight: =0
                        LayoutMinWidth: =80
                        RadiusBottomLeft: =10
                        RadiusBottomRight: =10
                        RadiusTopLeft: =10
                        RadiusTopRight: =10
                        Size: =13
                        Text: ="Nothing here yet"
                        Visible: =IsEmpty(ThisItem.Cards)
                  - KanbanBoardCards:
                      Control: Gallery@2.15.0
                      Variant: Vertical
                      Properties:
                        AccessibleLabel: =ThisItem.Lane & " cards"
                        AlignInContainer: =AlignInContainer.Stretch
                        BorderThickness: =0
                        FillPortions: =0
                        Height: =Self.AllItemsCount * (Self.TemplateHeight + Self.TemplatePadding) + Self.TemplatePadding
                        Items: =ThisItem.Cards
                        LayoutMinHeight: =0
                        LayoutMinWidth: =80
                        ShowScrollbar: =false
                        TabIndex: =0
                        TemplatePadding: =4
                        TemplateSize: =110
                        Visible: =Not(IsEmpty(ThisItem.Cards))
                      Children:
                        - KanbanBoardCard:
                            Control: GroupContainer@1.5.0
                            Variant: AutoLayout
                            Properties:
                              BorderColor: =ColorFade(App.Theme.Colors.Darker70, 88%)
                              BorderThickness: =1
                              DropShadow: =DropShadow.Light
                              Fill: =Color.White
                              Height: =Parent.TemplateHeight
                              LayoutDirection: =LayoutDirection.Vertical
                              LayoutGap: =2
                              PaddingBottom: =10
                              PaddingLeft: =12
                              PaddingRight: =8
                              PaddingTop: =10
                              RadiusBottomLeft: =10
                              RadiusBottomRight: =10
                              RadiusTopLeft: =10
                              RadiusTopRight: =10
                              Width: =Parent.TemplateWidth
                              X: =0
                              Y: =0
                            Children:
                              - KanbanBoardCardTitle:
                                  Control: ModernText@1.0.0
                                  Properties:
                                    AccessibleLabel: =Self.Text
                                    AlignInContainer: =AlignInContainer.Stretch
                                    FontWeight: =FontWeight.Semibold
                                    Height: =22
                                    LayoutMinHeight: =0
                                    LayoutMinWidth: =60
                                    PaddingBottom: =0
                                    PaddingLeft: =0
                                    PaddingTop: =0
                                    Size: =14
                                    Text: =ThisItem.Title
                                    Wrap: =false
                              - KanbanBoardCardSite:
                                  Control: ModernText@1.0.0
                                  Properties:
                                    AccessibleLabel: =Self.Text
                                    AlignInContainer: =AlignInContainer.Stretch
                                    Color: =ColorFade(App.Theme.Colors.Darker70, 45%)
                                    Height: =20
                                    LayoutMinHeight: =0
                                    LayoutMinWidth: =60
                                    PaddingBottom: =0
                                    PaddingLeft: =0
                                    PaddingTop: =0
                                    Size: =12
                                    Text: =ThisItem.Site
                                    Wrap: =false
                              - KanbanBoardCardFooter:
                                  Control: GroupContainer@1.5.0
                                  Variant: AutoLayout
                                  Properties:
                                    AlignInContainer: =AlignInContainer.Stretch
                                    DropShadow: =DropShadow.None
                                    FillPortions: =0
                                    Height: =36
                                    LayoutAlignItems: =LayoutAlignItems.Center
                                    LayoutDirection: =LayoutDirection.Horizontal
                                    LayoutGap: =6
                                    LayoutMinHeight: =36
                                    LayoutMinWidth: =60
                                  Children:
                                    - KanbanBoardCardOwner:
                                        Control: ModernAvatar@1.1.0
                                        Properties:
                                          AccessibleLabel: ="Owner " & ThisItem.Owner
                                          Badge: =PresenceBadge.None
                                          Height: =24
                                          Image: =Blank()
                                          LayoutMinHeight: =24
                                          LayoutMinWidth: =24
                                          Name: =ThisItem.Owner
                                          Width: =24
                                    - KanbanBoardCardDue:
                                        Control: ModernBadge@1.0.0
                                        Properties:
                                          AccessibleLabel: =If(ThisItem.Due < Today() && ThisItem.Lane <> "Done", "Overdue", "Due " & Text(ThisItem.Due, "d mmmm"))
                                          Appearance: =BadgeAppearance.Tint
                                          Content: =If(ThisItem.Due < Today() && ThisItem.Lane <> "Done", "Overdue", Text(ThisItem.Due, "d mmm"))
                                          Height: =22
                                          Intent: =If(ThisItem.Due < Today() && ThisItem.Lane <> "Done", BadgeIntent.Critical, BadgeIntent.Neutral)
                                          LayoutMinHeight: =22
                                          LayoutMinWidth: =60
                                          Size: =12
                                          Width: =68
                                    - KanbanBoardCardSpacer:
                                        Control: GroupContainer@1.5.0
                                        Variant: ManualLayout
                                        Properties:
                                          DropShadow: =DropShadow.None
                                          FillPortions: =1
                                          Height: =1
                                          LayoutMinHeight: =1
                                          LayoutMinWidth: =0
                                    - KanbanBoardCardBack:
                                        Control: ModernButton@1.0.0
                                        Properties:
                                          AccessibleLabel: ="Move " & ThisItem.Title & " back"
                                          Appearance: =ButtonAppearance.Subtle
                                          Height: =32
                                          Icon: ="ChevronRight"
                                          IconRotation: =180
                                          Layout: =ButtonLayout.IconOnly
                                          LayoutMinHeight: =32
                                          LayoutMinWidth: =32
                                          OnSelect: |
                                            =Collect(colKanbanBoardMoves, {Id: ThisItem.Id, Lane: Index(KanbanBoardColumns.AllItems, ThisItem.LaneIndex - 1).Lane})
                                          Size: =14
                                          Tooltip: ="Move back"
                                          Visible: =ThisItem.LaneIndex > 1
                                          Width: =32
                                    - KanbanBoardCardForward:
                                        Control: ModernButton@1.0.0
                                        Properties:
                                          AccessibleLabel: ="Move " & ThisItem.Title & " forward"
                                          Appearance: =ButtonAppearance.Subtle
                                          Height: =32
                                          Icon: ="ChevronRight"
                                          Layout: =ButtonLayout.IconOnly
                                          LayoutMinHeight: =32
                                          LayoutMinWidth: =32
                                          OnSelect: |
                                            =Collect(colKanbanBoardMoves, {Id: ThisItem.Id, Lane: Index(KanbanBoardColumns.AllItems, ThisItem.LaneIndex + 1).Lane})
                                          Size: =14
                                          Tooltip: ="Move forward"
                                          Visible: =ThisItem.LaneIndex < KanbanBoardColumns.AllItemsCount
                                          Width: =32

Make it yours

The example runs on sample data. Replace these properties to connect it to your app.

Items on KanbanBoardColumns

One record per column, each with its Cards. Keep the column names in lanes in board order and the card fields as they are (Id, Title, Site, Owner, Due, Lane, LaneIndex); map your own columns onto them.

In the YAML

YAML
Items: |
  =With(
      {
          lanes: ["To do", "In progress", "Done"],
          cards: Table(
              {Id: 1, Title: "Fire safety audit", Site: "Warehouse Ghent", Owner: "Sara Peeters", Due: Today() + 2, Lane: "To do"},
              {Id: 2, Title: "Forklift inspection", Site: "Depot Antwerp", Owner: "Tom Janssens", Due: Today() + 5, Lane: "To do"},
              {Id: 3, Title: "Ladder certification", Site: "Office Leuven", Owner: "An De Smet", Due: Today() - 1, Lane: "To do"},
              {Id: 4, Title: "Emergency lighting test", Site: "Warehouse Ghent", Owner: "Lucas Maes", Due: Today() + 1, Lane: "In progress"},
              {Id: 5, Title: "First aid kit check", Site: "Depot Antwerp", Owner: "Sara Peeters", Due: Today() + 3, Lane: "In progress"},
              {Id: 6, Title: "Sprinkler maintenance", Site: "Office Leuven", Owner: "Tom Janssens", Due: Today() - 4, Lane: "Done"}
          )
      },
      ForAll(
          Sequence(CountRows(lanes)) As lane,
          {
              Lane: Index(lanes, lane.Value).Value,
              Cards: Filter(
                  ForAll(
                      cards As c,
                      {
                          Id: c.Id,
                          Title: c.Title,
                          Site: c.Site,
                          Owner: c.Owner,
                          Due: c.Due,
                          Lane: Coalesce(Last(Filter(colKanbanBoardMoves, Id = c.Id)).Lane, c.Lane),
                          LaneIndex: lane.Value
                      }
                  ),
                  Lane = Index(lanes, lane.Value).Value
              )
          }
      )
  )

Your version

YAML
Items: |
  =With(
      {lanes: ["To do", "In progress", "Done"]},
      ForAll(
          Sequence(CountRows(lanes)) As lane,
          {
              Lane: Index(lanes, lane.Value).Value,
              Cards: ForAll(
                  Filter(Inspections, Status = Index(lanes, lane.Value).Value) As c,
                  {
                      Id: c.ID,
                      Title: c.Title,
                      Site: c.Site,
                      Owner: c.'Assigned to'.DisplayName,
                      Due: c.'Due date',
                      Lane: c.Status,
                      LaneIndex: lane.Value
                  }
              )
          }
      )
  )

OnSelect on KanbanBoardCardForward

Save the move. The example board keeps moves in the collection colKanbanBoardMoves; with real data, patch the status instead. Do the same in KanbanBoardCardBack with - 1.

In the YAML

YAML
OnSelect: |
  =Collect(colKanbanBoardMoves, {Id: ThisItem.Id, Lane: Index(KanbanBoardColumns.AllItems, ThisItem.LaneIndex + 1).Lane})

Your version

YAML
OnSelect: |
  =Patch(
      Inspections,
      LookUp(Inspections, ID = ThisItem.Id),
      {Status: Index(KanbanBoardColumns.AllItems, ThisItem.LaneIndex + 1).Lane}
  )

OnSelect on KanbanBoardAdd

Where a new card is created.

In the YAML

YAML
OnSelect: =Notify("Add a card (example only).", NotificationType.Information)

Your version

YAML
OnSelect: =NewForm(InspectionForm); Navigate(InspectionFormScreen, ScreenTransition.None)

How to paste

  1. Open your app in Power Apps Studio.
  2. Check that modern controls are on: Settings → Updates → Modern controls and themes. In newer environments they are on by default.
  3. Copy the YAML with the Copy button above the code.
  4. In the Tree view, select the screen or container that should hold the component and press Ctrl+V. The whole component arrives as one container, KanbanBoardRoot, with all its controls inside.
  5. Work through Make it yours to swap the example data for your own.

Where your version of Studio has a code view for a control or screen, you can paste the same YAML there instead of into the tree view.

When Studio flags a control version

Each control in the YAML names its version, like Control: ModernButton@1.0.0. If that version is newer than what your environment knows, Studio falls back to the version it has or flags the control. All controls of one type in an app must also use the same version, so an app that already has an older ModernButton can reject the paste. To fix it, insert the same control from the Insert pane, copy it as code, note the version on its Control: line, update every matching line in the YAML and paste again.