Photo Upload

Two-section photo capture component for field reporting apps. Each section has a configurable slot grid with live camera overlay, view mode for reviewing captures, replace/remove per slot, and per-slot progress badges. Fully themeable via StyleConfig.

=cmpPhotoUpload.yaml
  1. 1. Go to the Components tab (right side panel, next to Screens)
  2. 2. Click New component — this creates a blank component
  3. 3. Click outside the new component to deselect it
  4. 4. Paste the YAML with Ctrl+V / ⌘V

Preview

Top3
Bot3
Cols4
Top Photos — Report Header0 / 3

All image types · Max 5 MB per photo

Bottom Photos — Report Footer0 / 3

All image types · Max 5 MB per photo

Tap an empty slot to open the camera overlay · Tap a filled slot to view/manage

Installation

This component requires Modern controls to be enabled. Settings → Updates → Preview → Modern controls and themes

# In Power Apps Studio:
1. Components tab → New component → Import from code
2. Paste the full YAML definition
3. Component appears as "cmpPhotoUpload" in library

# Host screen OnVisible — required to initialize collections:
ClearCollect(_cmpTopPhotos,    {SlotIndex: 0, Photo: Blank(), Name: ""});
Clear(_cmpTopPhotos);
ClearCollect(_cmpBottomPhotos, {SlotIndex: 0, Photo: Blank(), Name: ""});
Clear(_cmpBottomPhotos);
Set(_cmpShowOverlay,    false);
Set(_cmpOverlaySection, "top");
Set(_cmpOverlayMode,    "capture");
Set(_cmpViewSlot,       0)

Usage

Basic setup

// Set on the component instance
cmpPhotoUpload1.Width             = Parent.Width
cmpPhotoUpload1.MaxTopPhotos      = 3
cmpPhotoUpload1.MaxBottomPhotos   = 3
cmpPhotoUpload1.MaxColumns        = 4
cmpPhotoUpload1.TopSectionLabel   = "Header Photos"
cmpPhotoUpload1.BottomSectionLabel = "Footer Photos"
cmpPhotoUpload1.ShowActions       = false  // host screen handles Save

// Read output collections
cmpPhotoUpload1.OnSave =
    EMS_UploadPhotos.Run(
        Text(gSelectedProject.ID),
        JSON(_cmpTopPhotos,    JSONFormat.IncludeBinaryData),
        JSON(_cmpBottomPhotos, JSONFormat.IncludeBinaryData)
    )

Send photos to Power Automate

// Flow trigger: PowerApps V2
// Inputs: ProjectID (text), TopPhotos (text), BottomPhotos (text)
//
// In your Save button OnSelect:
EMS_UploadPhotos.Run(
    Text(gSelectedProject.ID),
    JSON(_cmpTopPhotos,    JSONFormat.IncludeBinaryData),
    JSON(_cmpBottomPhotos, JSONFormat.IncludeBinaryData)
)

// Flow: Parse JSON both arrays, then Create File (SharePoint)
// Schema: [{ SlotIndex: Number, Photo: string, Name: string }]
// File content: base64ToBinary(item()?['Photo'])

Custom theme (dark mode)

cmpPhotoUpload1.StyleConfig = {
    colors: {
        cardBg:         ColorValue("#16223A"),
        cardBorder:     ColorValue("#2A3855"),
        cardBorderSlot: ColorValue("#374A73"),
        divider:        ColorValue("#2A3855"),
        camPillBg:      ColorValue("#23304B"),
        camPillIcon:    ColorValue("#588CDC"),
        titleText:      ColorValue("#DCE4F5"),
        slotEmptyBg:    ColorValue("#1C263C"),
        slotIconBg:     ColorValue("#263452"),
        slotIconColor:  ColorValue("#5878AF"),
        helperText:     ColorValue("#506C9B"),
        cancelBorder:   ColorValue("#374869"),
        imageBorder:    ColorValue("#4A6A8A"),
        overlayBg:      ColorValue("#0A1020"),
        overlayHeader:  ColorValue("#111E35")
    },
    radius: { card: 16, slot: 12, pill: 8 }
}

Properties

Input

PropertyTypeDefault
TopSectionLabel

Header label for the top photo section.

Text"Top Photos — Report Header"
BottomSectionLabel

Header label for the bottom photo section.

Text"Bottom Photos — Report Footer"
MaxTopPhotos

Total photo slots in the top section — wraps at MaxColumns per row.

Number3
MaxBottomPhotos

Total photo slots in the bottom section.

Number3
MaxColumns

Slots per row before wrapping. Default 4 gives 3 items comfortable width on mobile.

Number4
MaxFileSizeMB

Informational max file size shown in helper text. Actual enforcement in the flow.

Number5
ShowActions

Show or hide Cancel / Save buttons. Set false when host screen handles actions.

Booleantrue
StyleConfig

Color and radius tokens as a Record. Override any field to theme the component.

RecordSee defaults

Events

EventDescription
OnSaveFires when the Save Photos button is tapped. Read _cmpTopPhotos and _cmpBottomPhotos to get photo data.

Output Collections

CollectionSchemaDescription
_cmpTopPhotos{ SlotIndex: Number, Photo: Image, Name: Text }All captured top section photos. SlotIndex is 1-based.
_cmpBottomPhotos{ SlotIndex: Number, Photo: Image, Name: Text }All captured bottom section photos.

StyleConfig Tokens

Override any color or radius without redefining the whole record. Unspecified fields fall back to defaults.

cmpPhotoUpload1.StyleConfig = {
    colors: {
        cardBg:         ColorValue("#FFFFFF"),  // Section card background
        cardBorder:     ColorValue("#DCE4F2"),  // Section card border
        cardBorderSlot: ColorValue("#B0C0D8"),  // Empty slot dashed border
        divider:        ColorValue("#E8EDF5"),  // Horizontal divider lines
        camPillBg:      ColorValue("#EEF3FB"),  // Camera icon pill background
        camPillIcon:    ColorValue("#3A6BC4"),  // Camera icon color
        titleText:      ColorValue("#1E3A6B"),  // Section title text
        slotEmptyBg:    ColorValue("#F7F9FC"),  // Empty slot fill
        slotIconBg:     ColorValue("#DCE4F2"),  // Camera icon circle fill
        slotIconColor:  ColorValue("#6482B4"),  // Camera icon + "Add Photo" text
        helperText:     ColorValue("#9BACC4"),  // "All image types · Max X MB" text
        cancelBorder:   ColorValue("#D1DAEB"),  // Cancel button border
        imageBorder:    ColorValue("#A8C5EA"),  // Filled slot image border
        overlayBg:      ColorValue("#0F1826"),  // Camera overlay background
        overlayHeader:  ColorValue("#1A2740")   // Camera overlay header bar
    },
    radius: {
        card: 16,   // Section card corner radius
        slot: 12,   // Individual slot corner radius
        pill: 8     // Camera icon pill corner radius
    }
}

Examples

Field report — water damage (EMS)

Header Photos2/3
Footer Photos0/3
Cancel
Save Photos
// scrNewReport — Step 4: Photos
cmpPhotoUpload1.Width              = Parent.Width
cmpPhotoUpload1.TopSectionLabel    = "Header Photos"
cmpPhotoUpload1.BottomSectionLabel = "Footer Photos"
cmpPhotoUpload1.MaxTopPhotos       = 3
cmpPhotoUpload1.MaxBottomPhotos    = 3
cmpPhotoUpload1.MaxColumns         = 3
cmpPhotoUpload1.ShowActions        = false

// btnNextStep.OnSelect
If(
    CountRows(_cmpTopPhotos) = 0 And
    CountRows(_cmpBottomPhotos) = 0,
    Notify("Add at least one photo", NotificationType.Warning),
    Navigate(scrExportPreview)
)

Profile photo capture — single slot

Profile Photo0/1
Add Photo
// Single slot, top section only
// Hide bottom section by setting MaxBottomPhotos = 0
// is not supported — instead set the label and use
// CountRows to gate on just the top collection

cmpPhotoUpload1.TopSectionLabel    = "Profile Photo"
cmpPhotoUpload1.BottomSectionLabel = "Profile Photo"
cmpPhotoUpload1.MaxTopPhotos       = 1
cmpPhotoUpload1.MaxBottomPhotos    = 0
cmpPhotoUpload1.MaxColumns         = 1
cmpPhotoUpload1.ShowActions        = false

// Read the single captured photo
First(_cmpTopPhotos).Photo

Dark theme with custom accent

Site Photos1/4
cmpPhotoUpload1.MaxTopPhotos    = 4
cmpPhotoUpload1.MaxColumns      = 4
cmpPhotoUpload1.TopSectionLabel = "Site Photos"

cmpPhotoUpload1.StyleConfig = {
    colors: {
        cardBg:         ColorValue("#16223A"),
        cardBorder:     ColorValue("#2A3855"),
        cardBorderSlot: ColorValue("#374A73"),
        divider:        ColorValue("#2A3855"),
        camPillBg:      ColorValue("#23304B"),
        camPillIcon:    ColorValue("#588CDC"),
        titleText:      ColorValue("#DCE4F5"),
        slotEmptyBg:    ColorValue("#1C263C"),
        slotIconBg:     ColorValue("#263452"),
        slotIconColor:  ColorValue("#5878AF"),
        helperText:     ColorValue("#506C9B"),
        imageBorder:    ColorValue("#4A6A8A"),
        overlayBg:      ColorValue("#0A1020"),
        overlayHeader:  ColorValue("#111E35")
    },
    radius: { card: 16, slot: 10, pill: 8 }
}

Power Automate upload flow

// btnSaveDraft.OnSelect — trigger the upload flow
Set(varIsSaving, true);

IfError(
    EMS_UploadPhotos.Run(
        Text(gSelectedProject.ID),
        JSON(_cmpTopPhotos,    JSONFormat.IncludeBinaryData),
        JSON(_cmpBottomPhotos, JSONFormat.IncludeBinaryData)
    ),
    Notify("Upload failed: " & FirstError.Message, NotificationType.Error);
    Set(varIsSaving, false);
    false
);

Clear(_cmpTopPhotos);
Clear(_cmpBottomPhotos);
Set(varIsSaving, false);
Navigate(scrProjects, ScreenTransition.None)

// ─────────────────────────────────────────────────
// Power Automate flow structure:
//   Trigger: PowerApps V2
//     Inputs: ProjectID (text), TopPhotos (text), BottomPhotos (text)
//
//   Parse JSON — TopPhotos
//     Schema: array of { SlotIndex: number, Photo: string, Name: string }
//
//   Apply to each (TopPhotos)
//     Create file (SharePoint)
//       Site: your SharePoint site
//       Folder: /EMS/Projects/@{triggerBody()?['ProjectID']}/Header
//       File Name: @{item()?['Name']}
//       File Content: @{base64ToBinary(item()?['Photo'])}
//
//   (repeat Parse JSON + Apply to each for BottomPhotos → /Footer)

Implementation Details

Camera overlay pattern

The overlay is a ManualLayout GroupContainer at X=0, Y=0 inside the component, Height=App.Height. The host screen's container clips it to the remaining screen space. This means no screen-level controls or coordinate math — the component is fully self-contained.

Slot index tracking

Rather than tracking which slot is active in a variable, capture mode uses Min(Filter(Sequence(maxSlots), !(Value in col.SlotIndex)), Value) to find the first genuinely empty slot. This correctly handles gaps after removal — if the user removes slot 2 and captures again, the photo fills slot 2 not slot 4.

View mode

Tapping a filled slot sets _cmpOverlayMode = "view" and _cmpViewSlot to that slot's index. The camera panel is hidden and replaced by imgViewFull. Tapping any filled thumbnail in the right strip switches the large view. The Replace button removes the current slot's photo and flips back to capture mode — the first-empty-slot logic then fills that same index.

Auto-close on full

After every capture, camOverlay.OnSelect checks if CountRows(collection) >= MaxPhotos. If all slots are filled the overlay closes automatically — the user lands back on the grid with all thumbnails populated.

SVG badges

Check marks and remove icons use static SVG data URIs on Image@2.2.3 controls rather than Classic/Icon. This avoids the Power Apps SVG caching bug (dynamic colors in data URIs don't re-evaluate reactively) and gives consistent rendering on both iOS and Android. Colors are baked into the SVG string.

Collection initialization

The ClearCollect + Clear pattern in OnVisible is required. The ClearCollect with a typed dummy record establishes the column schema so Power Apps resolves SlotIndex, Photo, and Name as typed columns. The Clear then empties it. Skipping this step causes LookUp and Filter to fail at runtime with type errors.

Architecture

~45 controls — GroupContainers, Galleries, Images, Buttons, Text, Rectangle, Badge, Camera.

Self-contained overlay — camera UI lives inside the component, no screen-level controls needed.

Zero cross-control references — all height math uses inlined With() formulas to survive copy/paste.

cmpPhotoUpload
├── conTopSection (AutoLayout Vertical)
│   ├── conTopHeader (AutoLayout Horizontal)
│   │   ├── conTopCamPill → icnTopCam
│   │   ├── lblTopTitle
│   │   └── badgeTopCount (Badge@0.0.24)
│   ├── recTopDivider
│   ├── galTopSlots (Vertical, WrapCount=MaxColumns)
│   │   ├── conTopEmpty (dashed placeholder)
│   │   │   ├── conTopEmptyIcon → icnTopEmptyCam
│   │   │   └── lblTopAddPhoto
│   │   ├── btnTopTap (transparent, opens overlay — capture)
│   │   ├── imgTopPreview (filled slot photo)
│   │   ├── btnTopView (transparent, opens overlay — view)
│   │   ├── imgTopCheck (SVG green check badge)
│   │   └── imgTopRemove (SVG X button)
│   └── lblTopHelper
│
├── conBottomSection (mirrors top section)
│
├── conActions (AutoLayout Horizontal)
│   ├── btnCancel
│   └── btnSave → OnSave()
│
└── conCamOverlay (ManualLayout, X=0 Y=0, Height=App.Height)
    ├── conOverlayHeader (AutoLayout Horizontal)
    │   ├── btnOverlayBack
    │   ├── lblOverlayTitle (section + mode label)
    │   └── btnOverlayConfirm
    ├── camOverlay (Camera@2.4.0, visible in capture mode)
    ├── imgViewFull (Image@2.2.3, visible in view mode)
    ├── btnReplacePhoto (visible in view mode)
    └── galOverlayThumbs (Vertical, right 38%)
        ├── btnThumbSelect (view mode tap target)
        ├── imgThumb (photo or SampleImage placeholder)
        ├── lblThumbNum
        └── imgThumbRemove (SVG red X, centered bottom)

Community

Use the toolbar to format · or type markdown directly