The 2023-to-2026 Canvas Control Diff: What Microsoft Added Without Telling Anyone
Two apps, three years apart, same control templates. Here is the diff.
Canvas apps embed the full definition of every control they use. Not a reference — the actual template, in References/Templates.json, versioned. Which means an old .msapp is a fossil: a byte-exact snapshot of what the Power Apps control library looked like on the day it was saved.
We had two lying around.
- App A, saved 14 February 2023. 13 classic control templates.
- App B, saved 16 February 2026. 10 classic templates plus 10 modern ones.
Three years, near enough to the day. Same file format. So we diffed them.
Finding 1: the gallery has Copilot hooks built in
The gallery control went from 2.13.2 to 2.15.0. Four new properties:
+ #CopilotOverlayLabel
+ #CopilotOverlayVisible
+ AllItemsCount
+ MaxTemplateSize
Note the # prefix. That is the convention for internal properties in these templates — not surfaced in the property panel, not in the docs, not something you set. #CopilotOverlayVisible and #CopilotOverlayLabel are the platform’s own hooks for rendering a Copilot affordance on top of your gallery.
They are not a feature flag or a runtime injection. They are properties on the control template itself, which means the Copilot overlay is part of what a gallery is now, and every gallery in every app carries the attachment points whether the feature is enabled for that tenant or not.
AllItemsCount is the more immediately useful one — a count companion to AllItems, wired into the declared dependency graph:
<appMagic:propertyDependency input="Items" output="AllItemsCount" />
MaxTemplateSize joins the existing hidden sizing family and caps how large a gallery template can grow.
Finding 2: five new icons, and no version bump
The icon control is version 2.5.0 in the 2023 app and version 2.5.0 in the 2026 app. Identical version string.
Its icon enumeration is not identical. It grew from 176 to 181 values:
+ GlobeChangesPending
+ GlobeError
+ GlobeNotConnected
+ GlobeRefresh
+ GlobeWarning
Five icons, all of them about network state. Disconnected, failed, degraded, queued-for-upload, syncing — a complete offline-sync status vocabulary.
They were added to ship offline support. And they were added inside a version number that did not change, which is worth pausing on: if you are building tooling that keys off control template versions to decide whether it understands a control, that assumption is already broken. Content changed, version did not.
For the record, the 2026 icon enum carries 181 values from Add to ZoomOut, including such deep cuts as Printing3D, RadarActivityMonitor, ListWatchlistRemind, EmojiFrown and ForkKnife.
Finding 3: the modern control system did not exist
The 2023 file has no PcfTemplates key. Not an empty array — the key is absent.
The 2026 file has ten entries in it, each with a full property-level migration history: TextCanvas, ButtonCanvas, TabList, ComboboxCanvas, TextInputCanvas, DatePickerCanvas, DropdownCanvas, CheckboxCanvas, Toggle, LinkCanvas.
That is the single biggest change between the two files, and it is a change in kind. The 2023 app describes its controls one way. The 2026 app describes some controls the old way, some a completely new way, and carries a migration engine’s worth of version-conversion data for the new ones. (That data deserves its own post — 77 version hops and 298 property changes — and it has one.)
There is a third category we did not expect. Templates named:
PowerApps_CoreControls_ComboboxCanvasTemplate_dataField
PowerApps_CoreControls_DropdownCanvasTemplate_dataField
PowerApps_CoreControls_TabListTemplate_dataField
They live in the classic UsedTemplates list, they carry jsClass="AppMagic.Controls.PcfDataFieldControl", and they are the shim that lets a modern control participate in the classic data-field plumbing. The bridge between the two eras, shipped as a control.
Finding 4: what the composition change looks like
Same file format, three years apart, and the control mix inverted. The 2026 app’s own census, straight from Properties.json:
"ControlCount": {
"groupContainer": 414,
"PowerApps_CoreControls_TextCanvas": 240,
"PowerApps_CoreControls_TextInputCanvas": 112,
"PowerApps_CoreControls_ButtonCanvas": 36,
"PowerApps_CoreControls_ComboboxCanvas": 29,
"htmlViewer": 21,
"PowerApps_CoreControls_DatePickerCanvas": 19,
"PowerApps_CoreControls_DropdownCanvas": 18,
"attachments": 13,
"icon": 11,
"gallery": 8,
"PowerApps_CoreControls_CheckboxCanvas": 8,
"label": 7,
"screen": 7
}
Seven classic labels. 240 modern text controls. In an app whose file format still treats the classic label as a first-class citizen.
The 2023 app, by contrast, uses label, circle, rectangle, button, gallery, image, datepicker, timer, fluidGrid, text, combobox, htmlViewer and icon — the entire classic set, no modern controls at all, because there were none to use.
Finding 5: a third app, and a third generation
We later got hold of a third export of the same app family — August 2026, six months after App B. It revises two conclusions below, so it belongs here rather than in a footnote.
There is now a third control generation. Alongside the classic templates and the PowerApps_CoreControls_* set, the August export carries four templates with a new naming scheme:
| Template | Version |
|---|---|
modernText | 1.0.0 |
modernTextInput | 1.1.1 |
modernCombobox | 1.1.1 |
modernDatePicker | 1.0.1 |
They live in UsedTemplates — the classic list — with jsClass="AppMagic.Controls.ModernTextControl" and the same OpenAJAX widget XML the 2013-era controls use. After moving text, input, combobox and date picker into PCF, Microsoft moved them back into the native template format. The storage difference is not subtle: a PowerApps_CoreControls_TextCanvas costs 71,393 bytes per instance; modernText costs about 300, because the definition is shared again.
Three of the four are feature-gated (enabledForFeatureGate="Controls.ModernTextInputControl", Controls.ModernComboboxControl, Controls.ModernDatePickerControl); modernText is not.
And the classic library is not as frozen as App A vs App B suggested. In six months, groupContainer moved 1.4.0 → 1.5.0, gaining two properties:
+ LayoutGridColumnMinWidth
+ LayoutGridRowMinHeight
Grid-layout minimums on the container control — active development on a classic template, in 2026.
More same-version content changes. The image template is 2.2.3 in both the February and August exports, and the two are not byte-identical (17,300 vs 17,378 characters).
In fact, of the three templates carrying the same version number across the 2023 and 2026 exports, only one is genuinely unchanged:
| Template | Version in both | Byte-identical? | Size |
|---|---|---|---|
htmlViewer | 2.1.0 | yes | 6,940 → 6,940 |
icon | 2.5.0 | no | 49,760 → 51,025 |
label | 2.5.1 | no | 15,869 → 15,751 |
Add image 2.2.3 between February and August 2026 and that is three independent cases of content moving under a frozen version string. Treat the version attribute as advisory. It is not an identity, and tooling that caches or compares on it is already wrong.
Finding 6: what did not change
Worth stating, because it is reassuring.
htmlViewer is 2.1.0 in both files and byte-for-byte identical. circle, rectangle, timer, fluidGrid, combobox, datepicker, button — unchanged versions across three years. image moved 2.2.2 → 2.2.3 with no property changes at all.
label deserves a caveat. Its version is 2.5.1 in both, and its property surface is unchanged — but the file is not identical, and the difference is the most interesting thing in this diff. The 2023 template loaded two scripts behind a feature gate:
<require type="javascript" src="js/label.js" excludeOnFeatureGate="controls.reactLabel" />
<require type="javascript" src="js/labelReactProxy.js" includeOnFeatureGate="controls.reactLabel" />
The 2026 template loads one:
<require type="javascript" src="js/label.js" />
A React implementation of the label control existed in 2023, gated behind controls.reactLabel, and by 2026 it had been removed — the control reverted to Knockout only. (It also gained <appMagic:insertMetadata isDeviceOptimized="true">.)
So the label did not sit still for three years. It started a React migration, and that migration was abandoned.
The classic control library is in deep maintenance. Not deprecated, not removed — the templates still ship, still render, still work. Across three years the only additions were the gallery’s Copilot hooks, the icon control’s offline glyphs, and (in 2026) the group container’s grid minimums.
That is the real story of the diff. Every unit of engineering in this window went into the modern controls — and it went in twice. First into PCF, which brought a new format, a new migration mechanism, a shim layer to talk to the old one and, as its own conversion table admits, a lot of churn. Then back out of PCF into the native template format, which is where modernText and its three siblings now live.
The classic controls did not move much because they did not need to. The modern ones moved twice because the first answer was wrong.
Why do this yourself
Any .msapp you have kept is a dated snapshot of the platform. If you have exports going back years, you have a longitudinal record of the control library that Microsoft does not publish. Diffing them tells you:
- which properties appeared before they were documented,
- which internal
#-prefixed hooks got wired into controls you use, - whether a version number can be trusted (it cannot),
- and roughly where the platform team was spending its time.
Go find your oldest .msapp.
Methodology
Both files are .msapp archives; References/Templates.json is plain JSON containing each template as an XML string.
unzip -q old.msapp -d old/ && unzip -q new.msapp -d new/
python3 - <<'EOF'
import json, re
load = lambda p: {t['Name']: t for t in json.load(open(p))['UsedTemplates']}
a = load('old/References/Templates.json')
b = load('new/References/Templates.json')
for n in sorted(set(a) & set(b)):
pa = set(re.findall(r'name="([^"]+)"', a[n]['Template']))
pb = set(re.findall(r'name="([^"]+)"', b[n]['Template']))
if a[n]['Version'] != b[n]['Version'] or pa != pb:
print(f"{n}: {a[n]['Version']} -> {b[n]['Version']}")
if pb - pa: print(' + ', sorted(pb - pa))
if pa - pb: print(' - ', sorted(pa - pb))
EOF
Two-app sample. The diff is exact for the 13 classic templates both apps share; it says nothing about controls neither app uses.