<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Nelson Boruchowitch&#39;s blog</title>
    <link>https://nelsonboruchowitch.com/blog/en/</link>
    <atom:link href="https://nelsonboruchowitch.com/blog/en/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Power Platform, web development and small technical obsessions. Every post, in full.</description>
    <language>en-us</language>
    <copyright>&#169; 2026 Nelson Boruchowitch. All rights reserved.</copyright>
    <generator>nb-blog-builder (build-blog.mjs)</generator>
    <docs>https://www.rssboard.org/rss-specification</docs>
    <lastBuildDate>Thu, 27 Aug 2026 00:00:00 +0000</lastBuildDate>
    <image>
      <url>https://nelsonboruchowitch.com/img/logo.png</url>
      <title>Nelson Boruchowitch&#39;s blog</title>
      <link>https://nelsonboruchowitch.com/blog/en/</link>
    </image>
    <item>
      <title>SVG in PowerApps: what about accessibility?</title>
      <link>https://nelsonboruchowitch.com/blog/en/prefers-reduced-motion-power-apps/</link>
      <guid isPermaLink="true">https://nelsonboruchowitch.com/blog/en/prefers-reduced-motion-power-apps/</guid>
      <pubDate>Thu, 27 Aug 2026 00:00:00 +0000</pubDate>
      <dc:creator>Nelson Boruchowitch</dc:creator>
      <category>Power Apps</category>
      <category>Power Fx</category>
      <category>SVG</category>
      <category>Accessibility</category>
      <category>WCAG</category>
      <description>SVG has become a reflex for bringing Canvas Apps to life. Except that some users have asked their system to stop motion, and Power Fx has no idea. The CSS embedded in the SVG does.</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR</strong> : Using SVG has become a reflex for bringing our Canvas Apps to life. Except that some of your users have asked their system to stop animations, and Power Fx has no way of knowing. The CSS embedded in the SVG does: <code>@media (prefers-reduced-motion: reduce)</code> really is evaluated inside a Canvas App, <strong>every time a session opens</strong>. Here is the ready-to-paste pattern, the test bench that proves it, the precise reason why this reading is a snapshot rather than a stream, and the layered architecture that makes it genuinely usable in production.</p>
</blockquote>
<hr />
<div class="partie" id="partie-1">
<p class="partie__num">Part 1 of 5 · 4 min read</p>

<h2 class="partie__titre">The problem and the answer</h2>
<p class="partie__lead">The observation, and the code that settles it.</p>
</div>
<h2 id="a-trend-and-a-blind-spot">1. A trend and a blind spot</h2>
<p>SVGs are convenient, lightweight, crisp at any size, colourable from Power Fx, and given the sheer number of resources available online, we have plenty to work with.</p>
<p>But a trend gradually appeared: the <em>animated</em> SVG. A spinner that turns, a halo that pulses on today's date, an icon that draws itself when the screen opens. It instantly makes an app feel alive, for the trifling cost of a few lines.</p>
<p>Except that for some of your users, that motion is not a nice touch but a physical problem. Vestibular disorders, migraine sensitivity, certain attention disorders: those people have already told their operating system, in the Windows settings, that they no longer want animations, and your app is not listening.</p>
<p>This is not a matter of comfort, it is written into the standards:</p>
<ul>
<li>
<p><strong>WCAG 2.2.2: Pause, Stop, Hide (level A)</strong>: any moving content that starts automatically and lasts more than 5 seconds must be pausable.</p>
</li>
<li>
<p><strong>WCAG 2.3.3: Animation from Interactions (level AAA)</strong>: non-essential animations triggered by an interaction must be able to be disabled.</p>
</li>
</ul>
<p>When accessibility comes up in a Canvas App, we usually talk about <code>AccessibleLabel</code>, <code>TabIndex</code> (tab order), contrast and <code>Live</code> regions. All of that is documented, all of that shows up in the Accessibility Checker. Motion is nowhere to be found. It does not exist in Power Fx either: there is <code>Host.OSType</code>, <code>Host.BrowserUserAgent</code>, <code>App.Width</code>... and nothing at all about the system's accessibility preferences. No <code>Host.PrefersReducedMotion</code>.</p>
<p>In practice: your Timer sliding a panel in, your <code>Transition</code> on a button, your blinking badge, they all run regardless of what the user asked their OS for. And you have, apparently, no way of knowing.</p>
<p>Good news: you do not need <code>Host.PrefersReducedMotion</code>. The preference is already there, inside your SVGs. Three lines of CSS are enough.</p>
<h2 data-toc="2. Three lines of CSS" id="the-answer-fits-in-three-lines-of-css">2. The answer fits in three lines of CSS</h2>
<p>Let us take a real case. In my <code>cmpCalendar</code> component, today's date carries a gently pulsing halo. Here is the version that listens to the system:</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx"><pre><code class="lang-powerfx"><span class="hl-str">"data:image/svg+xml;utf8,"</span> <span class="hl-op">&amp;</span> <span class="hl-fn">EncodeUrl</span><span class="hl-punct">(</span>
    <span class="hl-str">"&lt;svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;style&gt;"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".halo{fill:none;stroke:#0078d4;stroke-width:2;transform-origin:20px 20px;"</span> <span class="hl-op">&amp;</span>
               <span class="hl-str">"animation:pulse 1.8s ease-out infinite}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@keyframes pulse{from{transform:scale(.78);opacity:.9}to{transform:scale(1.12);opacity:0}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (prefers-reduced-motion:reduce){"</span> <span class="hl-op">&amp;</span>
            <span class="hl-str">".halo{animation:none;opacity:1;stroke-width:2.5}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"}"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/style&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;circle class='halo' cx='20' cy='20' r='17'/&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/svg&gt;"</span>
<span class="hl-punct">)</span></code></pre></div></div>
<p>By default: a breathing halo. Under <code>reduce</code>: a crisp, motionless ring, slightly thicker so it stays just as visible. <strong>The &quot;this is today&quot; information is intact in both cases.</strong> No <code>If()</code> in the formula, no variable, no Power Fx logic: the CSS decides.</p>
<div class="rm-demo" data-rm-demo role="group" aria-label="The halo in its two states">
<p class="rm-demo__lead" id="rm-verdict">Reading your system preference...</p>
<div class="rm-demo__grid">
<div class="rm-demo__cell" id="rm-cell-a">
<p class="rm-demo__tag"><code>no-preference</code><span class="rm-demo__badge">your rendering</span></p>
<span class="rm-demo__stage"><span class="rm-demo__num">18</span><img class="rm-demo__svg" id="rm-img-a" alt="Blue halo around the date: it grows, fades out, then starts again."></span>
<p class="rm-demo__cap">The halo breathes. <code>stroke-width:2</code></p>
</div>
<div class="rm-demo__cell" id="rm-cell-b">
<p class="rm-demo__tag"><code>reduce</code><span class="rm-demo__badge">your rendering</span></p>
<span class="rm-demo__stage"><span class="rm-demo__num">18</span><img class="rm-demo__svg" id="rm-img-b" alt="Crisp, motionless blue ring around the date."></span>
<p class="rm-demo__cap">Crisp ring, motionless. <code>stroke-width:2.5</code></p>
</div>
</div>
<p class="rm-demo__foot"><button type="button" id="rm-play" hidden>Play the animation anyway</button></p>
</div>
<style>
.rm-demo{margin:0 0 26px;padding:18px;border:1px solid var(--border);border-radius:12px;background:var(--bg-panel)}
.rm-demo__lead{margin:0 0 16px;font-size:13.5px;line-height:1.6;color:var(--text-dim)}
.rm-demo__lead code,.rm-demo__note code{font-family:var(--mono);font-size:12px}
.rm-demo__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:16px}
.rm-demo__cell{padding:14px;border:1px solid var(--border);border-radius:10px;background:var(--bg-dark);text-align:center}
.rm-demo__cell--live{border-color:var(--primary)}
.rm-demo__tag{margin:0 0 12px;font-family:var(--mono);font-size:12px;color:var(--text-dim)}
.rm-demo__tag code{font-family:var(--mono);font-size:12px}
.rm-demo__badge{display:none;margin-left:8px;padding:1px 8px;border:1px solid var(--primary);border-radius:999px;font-family:var(--mono);font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--primary)}
.rm-demo__cell--live .rm-demo__badge{display:inline-block}
.rm-demo__stage{position:relative;display:block;width:104px;height:104px;margin:0 auto}
.rm-demo__num{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--text-light)}
.blog-post .rm-demo__svg{position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:0}
.rm-demo__cap{margin:12px 0 0;font-size:12.5px;line-height:1.5;color:var(--text-dim)}
.rm-demo__cap code{font-family:var(--mono);font-size:11.5px}
.rm-demo__foot{margin:16px 0 0;text-align:center}
.rm-demo__foot button{font:inherit;font-size:12.5px;padding:6px 14px;color:var(--text-light);background:transparent;border:1px solid var(--border);border-radius:8px;cursor:pointer}
.rm-demo__foot button:hover{border-color:var(--primary);color:var(--primary)}
.rm-demo__note{margin:16px 0 0;font-size:12.5px;line-height:1.6;color:var(--text-dim)}
</style>
<script>
(function(){
var d=document.querySelector('[data-rm-demo]');if(!d)return;
var K="@keyframes pulse{from{transform:scale(.78);opacity:.9}to{transform:scale(1.12);opacity:0}}";
var BASE=".halo{fill:none;stroke:#0078d4;transform-origin:20px 20px}";
var ANIM=".halo{stroke-width:2;animation:pulse 1.8s ease-out infinite}";
var STAT=".halo{animation:none;opacity:1;stroke-width:2.5}";
var FRZ=".halo{stroke-width:2;animation:none;opacity:.9;transform:scale(.78)}";
function uri(css){return "data:image/svg+xml;utf8,"+encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'><style>"+BASE+K+css+"</style><circle class='halo' cx='20' cy='20' r='17'/></svg>");}
var ia=d.querySelector('#rm-img-a'),ib=d.querySelector('#rm-img-b');
var ca=d.querySelector('#rm-cell-a'),cb=d.querySelector('#rm-cell-b');
var v=d.querySelector('#rm-verdict'),btn=d.querySelector('#rm-play');
ib.src=uri(ANIM+STAT);
var mq=window.matchMedia('(prefers-reduced-motion: reduce)');
function paint(){
var r=mq.matches;
ia.src=uri(r?FRZ:ANIM);
btn.hidden=!r;
ca.classList.toggle('rm-demo__cell--live',!r);
cb.classList.toggle('rm-demo__cell--live',r);
v.innerHTML=r?"Your system answers <code>reduce</code>: the tile marked &laquo; your rendering &raquo; is what you would get in the app. The other one is frozen on its first frame, because this article applies its own rule to itself."
:"Your system answers <code>no-preference</code>: the tile marked &laquo; your rendering &raquo; is what you would get in the app.";
}
btn.addEventListener('click',function(){ia.src=uri(ANIM);btn.hidden=true;});
if(mq.addEventListener)mq.addEventListener('change',paint);
paint();
})();
</script>

<p>One line is missing for this to be reliable, and only one. In <code>App.OnStart</code>:</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx" data-pfx="1"><pre><code class="lang-powerfx"><span class="hl-com">// App.OnStart</span>
<span class="hl-fn">Set</span><span class="hl-punct">(</span>gblBust<span class="hl-punct" data-sep="a">,</span> <span class="hl-fn">GUID</span><span class="hl-punct">(</span><span class="hl-punct">)</span><span class="hl-punct">)</span></code></pre></div></div>
<p>That variable does not appear in the code above, but it has to be there. Section 8 explains why it is not a debugging artefact but <strong>the reading mechanism itself</strong>.</p>
<p>That is all. No connector, no PCF, no component. A string of characters.</p>
<p>Now, why it works.</p>
<div class="partie" id="partie-2">
<p class="partie__num">Part 2 of 5 · 7 min read</p>

<h2 class="partie__titre">Why it works</h2>
<p class="partie__lead">This part explains which door the CSS uses to get into a Canvas App, and why the seemingly obvious route, the <code>&lt;style&gt;</code> tag of the HTML Text control, is not one.</p>
</div>
<h2 data-toc="3. The technical blind spot" id="the-technical-blind-spot-there-is-already-a-css-engine-in-your-canvas-app">3. The technical blind spot: there is already a CSS engine in your Canvas App</h2>
<p>A Canvas App is the web. It runs in Edge, in Chrome, in Teams (WebView2), in iOS (WKWebView). So somewhere under your controls sits a complete rendering engine with a complete CSS engine.</p>
<p>The trouble is that we have no access to it... except through one door: <strong>an image can be a data URI, a data URI can contain SVG, and an SVG can contain a <code>&lt;style&gt;</code> tag.</strong></p>
<p>That is the key point. An SVG displayed <em>as an image</em> is rendered in what browsers call &quot;secure static&quot; mode:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>What is <strong>disabled</strong> inside the image</th>
<th>What stays <strong>active</strong> inside the image</th>
</tr>
</thead>
<tbody>
<tr>
<td>JavaScript, <code>&lt;script&gt;</code></td>
<td>Full CSS: selectors, classes, cascade</td>
</tr>
<tr>
<td>External resources (fetch, remote fonts, remote <code>&lt;image href&gt;</code>)</td>
<td><code>@keyframes</code> and CSS animations</td>
</tr>
<tr>
<td>Interactivity, events, links: an <code>&lt;a&gt;</code> placed inside the SVG is not clickable</td>
<td><code>@media</code>, including user preference media features</td>
</tr>
<tr>
<td>Any information flowing back to the host page</td>
<td>Transforms, opacities, filters</td>
</tr>
</tbody>
</table></div>
<p>In other words: a perfectly sealed sandbox... that is still allowed to ask the operating system questions.</p>
<p>And that is exactly the &quot;loophole&quot; we need. Because the media features known as <em>user preference</em> features (<code>prefers-reduced-motion</code>, <code>prefers-color-scheme</code>, <code>forced-colors</code>, <code>prefers-contrast</code>) do not depend on the host document: they are supplied by the user agent itself, which gets them from the system. An isolated SVG image therefore has the same access to them as a full page.</p>
<p>That still had to be verified. Because &quot;it should work according to the spec&quot; and &quot;it works in PowerApps on a user's machine&quot; are two very different claims. That is what section 7 is for.</p>
<h2 data-toc="4. Why not a style tag" id="why-not-a-style-tag-in-the-html-text-control">4. Why not a <code>&lt;style&gt;</code> tag in the HTML Text control?</h2>
<p>The question is a fair one, since PowerApps has a control that accepts raw HTML, and raw HTML accepts the <code>&lt;style&gt;</code> tag. So why go through a data URI in an Image control?</p>
<p>The answer comes in two stages: <strong>the <code>&lt;style&gt;</code> tag does not survive, and yet this control can carry a media query.</strong></p>
<h3 id="what-does-not-work-the-controls-style-sheet">4.1. What does not work: the control's style sheet</h3>
<p>The HTML Text control passes its content through a sanitiser, which strips the <code>&lt;style&gt;</code> tag. What remains usable is the inline style, <code>style=&quot;...&quot;</code>.</p>
<p>It is worth being precise about what &quot;strips&quot; means, because the usual wording understates reality. I measured it in a published app, with the canary from section 7.2: I pasted into the control a style sheet containing an ordinary class selector and a <code>@media (min-width:1px)</code>, true anywhere CSS is evaluated. <strong>Neither of them lights up.</strong> So it is not a matter of advanced features being disabled, nor of some rules being filtered out: <strong>no style sheet remains at all.</strong></p>
<p>In other words, this is not a way to add a media query to the control:</p>
<div class="code-block"><div class="code-wrapper" data-lang="HTML"><pre><code class="lang-html"><span class="hl-tag">&lt;style</span><span class="hl-tag">&gt;</span>
@media (prefers-reduced-motion: reduce) {
    .halo { animation: none }
}
<span class="hl-tag">&lt;/style</span><span class="hl-tag">&gt;</span></code></pre></div></div>
<p>That tag never reaches the rendering engine.</p>
<h3 id="what-about-the-inline-style">4.2. What about the inline style?</h3>
<p>One might try to sidestep the obstacle by putting the CSS straight onto the elements:</p>
<div class="code-block"><div class="code-wrapper" data-lang="HTML"><pre><code class="lang-html"><span class="hl-tag">&lt;div</span> <span class="hl-attr">style</span>=<span class="hl-str">"animation: pulse 1.8s infinite"</span><span class="hl-tag">&gt;</span>...<span class="hl-tag">&lt;/div</span><span class="hl-tag">&gt;</span></code></pre></div></div>
<p>That settles the styling question, not ours. A <code>style</code> attribute only contains a list of <code>property: value</code> declarations separated by semicolons. A media query, on the other hand, is an <em>at-rule</em>:</p>
<div class="code-block"><div class="code-wrapper"><pre><code class="lang-css"><span class="hl-meta">@media</span> <span class="hl-punct">(</span><span class="hl-prop">prefers-reduced-motion</span><span class="hl-punct">:</span> reduce<span class="hl-punct">)</span> <span class="hl-punct">{</span>
    <span class="hl-sel">.halo</span> <span class="hl-punct">{</span> <span class="hl-prop">animation</span><span class="hl-punct">:</span> none <span class="hl-punct">}</span>
<span class="hl-punct">}</span></code></pre></div></div>
<p>That syntax has no place in an attribute. Even with a completely permissive sanitiser, even if Microsoft let <code>&lt;style&gt;</code> tags through tomorrow morning, an inline style would still be unable to carry an <code>@media</code>. <strong>This is not a PowerApps restriction, it is a property of CSS itself.</strong></p>
<p>So we are looking at two obstacles of very different natures: the first comes from the platform and might change one day, the second comes from the language and will not.</p>
<p>At this stage one would happily conclude that the HTML Text control cannot host a media query. <strong>That would be jumping ahead.</strong></p>
<h3 id="what-does-work-the-style-sheet-travels-inside-the-image">4.3. What does work: the style sheet travels inside the image</h3>
<p>The penny drops on a detail we forget: this control does not only receive text. It receives HTML, and HTML includes the <code>&lt;img&gt;</code> tag. Now, an image's source can be a data URI, and a data URI can contain a complete SVG document, with its own style sheet. The block below <strong>is not a formula to paste</strong>: it is the document your formula will produce, shown unfolded so the style sheet is visible. It is the halo from section 2; the formula that builds it is given at the end of this section.</p>
<div class="code-block"><div class="code-wrapper" data-lang="SVG · result"><pre><code class="lang-svg"><span class="hl-tag">&lt;svg</span> <span class="hl-attr">xmlns</span>=<span class="hl-str">"http://www.w3.org/2000/svg"</span> <span class="hl-attr">viewBox</span>=<span class="hl-str">"0 0 40 40"</span><span class="hl-tag">&gt;</span>
    <span class="hl-tag">&lt;style</span><span class="hl-tag">&gt;</span>
        .halo { fill: none; stroke: #0078d4; stroke-width: 2;
                transform-origin: 20px 20px;
                animation: pulse 1.8s ease-out infinite }
        @keyframes pulse { from { transform: scale(.78);  opacity: .9 }
                           to   { transform: scale(1.12); opacity: 0  } }
        @media (prefers-reduced-motion: reduce) {
            .halo { animation: none; opacity: 1; stroke-width: 2.5 }
        }
    <span class="hl-tag">&lt;/style</span><span class="hl-tag">&gt;</span>
    <span class="hl-tag">&lt;circle</span> <span class="hl-attr">class</span>=<span class="hl-str">"halo"</span> <span class="hl-attr">cx</span>=<span class="hl-str">"20"</span> <span class="hl-attr">cy</span>=<span class="hl-str">"20"</span> <span class="hl-attr">r</span>=<span class="hl-str">"17"</span><span class="hl-tag">/&gt;</span>
<span class="hl-tag">&lt;/svg</span><span class="hl-tag">&gt;</span></code></pre></div></div>
<p>Nested into the control, that document becomes an image source. It then fits on a single line, encoded. Here is the resulting tag, abbreviated for readability:</p>
<div class="code-block"><div class="code-wrapper" data-lang="HTML · result"><pre><code class="lang-html"><span class="hl-tag">&lt;img</span> <span class="hl-attr">src</span>=<span class="hl-str">"data:image/svg+xml;utf8,%3Csvg%20xmlns=%27...%27%3E%3Cstyle%3E...%3C/style%3E%3C/svg%3E"</span><span class="hl-tag">&gt;</span></code></pre></div></div>
<p>The SVG's angle brackets are encoded as <code>%3C</code> and <code>%3E</code>, the apostrophes as <code>%27</code>: the string no longer contains a single character able to close the attribute by accident. You will never have to write that string yourself: <code>EncodeUrl</code> produces it, in the formula given at the end of this section.</p>
<p><strong>This is where everything changes.</strong> The <code>&lt;style&gt;</code> is no longer a tag of the HTML submitted to the sanitiser. It sits inside the SVG document, which itself sits inside the image source.</p>
<p>The sanitiser only reads the outer markup. It sees an <code>&lt;img&gt;</code> tag whose source is a data URI, it lets it through, and it never opens what it contains. So the SVG reaches the rendering engine with its document, its style sheet and its media query intact.</p>
<p>The general rule, which will serve you well beyond this topic: <strong>the markup lives, its style sheet does not.</strong></p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>In the HTML Text control</th>
<th>Media query?</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&lt;style&gt; ... &lt;/style&gt;</code></td>
<td>No</td>
<td>the style sheet is stripped by the sanitiser</td>
</tr>
<tr>
<td><code>style=&quot;...&quot;</code></td>
<td>No</td>
<td>CSS grammar forbids an at-rule inside an attribute</td>
</tr>
<tr>
<td><code>&lt;img src=&quot;data:image/svg+xml,...&quot;&gt;</code></td>
<td><strong>Yes</strong></td>
<td>the SVG carries its own document, hence its own style sheet</td>
</tr>
</tbody>
</table></div>
<p>Which completely changes the way to look at the problem. <strong>The decisive element is not the Image control, it is the data URI.</strong> The Image control is merely the shortest path to using it.</p>
<p>Nothing stops you, then, from having both live in the same control. Here is the complete assembly, to paste into the <code>HtmlText</code> property:</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx"><pre><code class="lang-powerfx"><span class="hl-com">// HtmlText property of an HTML Text control</span>
<span class="hl-str">"&lt;div style='font-family:Segoe UI,sans-serif'&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;p&gt;Your text, your links, your formatting.&lt;/p&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;img src='data:image/svg+xml;utf8,"</span> <span class="hl-op">&amp;</span>
    <span class="hl-fn">EncodeUrl</span><span class="hl-punct">(</span>
        <span class="hl-str">"&lt;svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40' data-build='"</span> <span class="hl-op">&amp;</span> gblBust <span class="hl-op">&amp;</span> <span class="hl-str">"'&gt;"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"&lt;style&gt;"</span> <span class="hl-op">&amp;</span>
            <span class="hl-str">".halo{fill:none;stroke:#0078d4;stroke-width:2;transform-origin:20px 20px;"</span> <span class="hl-op">&amp;</span>
                   <span class="hl-str">"animation:pulse 1.8s ease-out infinite}"</span> <span class="hl-op">&amp;</span>
            <span class="hl-str">"@keyframes pulse{from{transform:scale(.78);opacity:.9}to{transform:scale(1.12);opacity:0}}"</span> <span class="hl-op">&amp;</span>
            <span class="hl-str">"@media (prefers-reduced-motion:reduce){"</span> <span class="hl-op">&amp;</span>
                <span class="hl-str">".halo{animation:none;opacity:1;stroke-width:2.5}"</span> <span class="hl-op">&amp;</span>
            <span class="hl-str">"}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"&lt;/style&gt;"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"&lt;circle class='halo' cx='20' cy='20' r='17'/&gt;"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"&lt;/svg&gt;"</span>
    <span class="hl-punct">)</span> <span class="hl-op">&amp;</span> <span class="hl-str">"'&gt;"</span> <span class="hl-op">&amp;</span>
<span class="hl-str">"&lt;/div&gt;"</span></code></pre></div></div>
<p>The outer HTML then carries what genuinely belongs to HTML: text, formatting, links. The SVG carries the graphics and the media query. One clarification is needed about interactivity, though, because it comes at a price. The displayed content really is alive: measured in a published app, a link opens a window, a checkbox ticks, a <code>&lt;details&gt;</code> block unfolds. But it is mute. None of that reaches Power Fx, which will never know the box is ticked. <strong>The content lives, but it does not speak.</strong></p>
<p>The only bridge to the application is the control's <code>OnSelect</code>. It exists and it fires, but it is the event of the <strong>control</strong>, not of its content: it is single and global, and nothing in Power Fx says <em>where</em> the click happened. As many reactive zones as you want, as many controls to place. In a chart component, the useful assembly therefore stays the same: <strong>the drawing and its visual behaviour live inside the image, and the interactivity the image cannot provide comes from Power Apps, placed around it or on top of it.</strong></p>
<p>And this is not guesswork. Section 7 builds a test bench that puts every link in the chain to the test: the canary shows that the SVG's CSS really is evaluated, then the <code>prefers-color-scheme</code> and <code>prefers-reduced-motion</code> probes show that user preference media queries answer as well. That bench is written for the <code>Image</code> property, the shortest path; it is the same SVG I placed in the <code>src</code> of an <code>&lt;img&gt;</code> to test the HTML Text control, and <a href="https://nelsonboruchowitch.com/blog/en/prefers-reduced-motion-power-apps/#the-decoding-lock">section 9</a> reports on it.</p>
<div class="partie" id="partie-3" data-toc="Doing it properly">
<p class="partie__num">Part 3 of 5 · 5 min read</p>

<h2 class="partie__titre">Doing it properly in production</h2>
<p class="partie__lead">The mechanism is settled. What remains is using it without degrading the experience: what to put in place of the motion, and how to leave the user the final word anyway.</p>
</div>
<h2 id="reducing-is-not-removing">5. Reducing is not removing</h2>
<p>The classic mistake is to treat <code>prefers-reduced-motion: reduce</code> as a master switch: <code>animation: none</code> everywhere, and let us say no more about it. That misses the point. An animation often carries information: &quot;it is loading&quot;, &quot;this is what just changed&quot;, &quot;this element is active&quot;.</p>
<p>If you remove it and put nothing in its place, you are not making the app more accessible: you are stripping a layer of information from an audience that already needs it.</p>
<p>The right rule: <strong>replace the motion with a static equivalent that carries the same information.</strong> A pulsing halo becomes a solid ring. A slide becomes a fade (changes of opacity and colour are far better tolerated than large displacements). A long duration becomes a short one.</p>
<p>That is what the halo in section 2 does, with its <code>stroke-width</code> going from 2 to 2.5.</p>
<h3 id="the-edge-case-the-loader">5.1. The edge case: the loader</h3>
<p>Same principle, different use:</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx"><pre><code class="lang-powerfx"><span class="hl-str">"data:image/svg+xml;utf8,"</span> <span class="hl-op">&amp;</span> <span class="hl-fn">EncodeUrl</span><span class="hl-punct">(</span>
    <span class="hl-str">"&lt;svg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 48 48'&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;style&gt;"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".ring{fill:none;stroke:#0078d4;stroke-width:4;stroke-linecap:round;"</span> <span class="hl-op">&amp;</span>
               <span class="hl-str">"stroke-dasharray:82 44;transform-origin:24px 24px;"</span> <span class="hl-op">&amp;</span>
               <span class="hl-str">"animation:spin 900ms linear infinite}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@keyframes spin{to{transform:rotate(360deg)}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (prefers-reduced-motion:reduce){"</span> <span class="hl-op">&amp;</span>
            <span class="hl-str">".ring{animation:none;stroke-dasharray:none;opacity:.35}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"}"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/style&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;circle class='ring' cx='24' cy='24' r='19'/&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/svg&gt;"</span>
<span class="hl-punct">)</span></code></pre></div></div>
<p>Careful here: a motionless spinner no longer communicates &quot;in progress&quot;. Alongside this SVG, what is needed is a genuine status text next to it, in a PowerApps Label, with <code>Live: Assertive</code>, announcing &quot;Loading...&quot; then &quot;Done&quot;. That is what should have been done for screen readers anyway. Reduced mode merely makes the omission visible.</p>
<h2 id="the-three-mode-pattern">6. The three-mode pattern</h2>
<p>The two examples above are good illustrations, but they leave a legitimate objection unanswered: <em>what if the user wants to decide for this particular application?</em> Perhaps they have turned animations off everywhere in Windows but tolerate yours. Perhaps the opposite: Windows says nothing, but your app is a bit much for their taste.</p>
<p>Here is the shape I recommend in production. A three-valued Power Fx variable arbitrates, and the CSS decides in only one of the three cases.</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx" data-pfx="1"><pre><code class="lang-powerfx"><span class="hl-str">"data:image/svg+xml;utf8,"</span> <span class="hl-op">&amp;</span> <span class="hl-fn">EncodeUrl</span><span class="hl-punct">(</span>
    <span class="hl-str">"&lt;svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40' "</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"class='"</span> <span class="hl-op">&amp;</span> <span class="hl-fn">Switch</span><span class="hl-punct">(</span>gblMotionMode<span class="hl-punct" data-sep="a">,</span> <span class="hl-str">"reduce"</span><span class="hl-punct" data-sep="a">,</span> <span class="hl-str">"red"</span><span class="hl-punct" data-sep="a">,</span> <span class="hl-str">"full"</span><span class="hl-punct" data-sep="a">,</span> <span class="hl-str">"ful"</span><span class="hl-punct" data-sep="a">,</span> <span class="hl-str">"sys"</span><span class="hl-punct">)</span> <span class="hl-op">&amp;</span> <span class="hl-str">"' "</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"data-build='"</span> <span class="hl-op">&amp;</span> gblBust <span class="hl-op">&amp;</span> <span class="hl-str">"'&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;style&gt;"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".halo{fill:none;stroke:#0078d4;stroke-width:2.5;transform-origin:20px 20px}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@keyframes pulse{from{transform:scale(.78);opacity:.9}to{transform:scale(1.12);opacity:0}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".ful .halo{stroke-width:2;animation:pulse 1.8s ease-out infinite}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (prefers-reduced-motion:no-preference){"</span> <span class="hl-op">&amp;</span>
            <span class="hl-str">".sys .halo{stroke-width:2;animation:pulse 1.8s ease-out infinite}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"}"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/style&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;circle class='halo' cx='20' cy='20' r='17'/&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/svg&gt;"</span>
<span class="hl-punct">)</span></code></pre></div></div>
<p>Three modes, three behaviours:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th><code>gblMotionMode</code></th>
<th>Class</th>
<th>Behaviour</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&quot;system&quot;</code> or empty</td>
<td><code>sys</code></td>
<td>the CSS decides, through the media query</td>
</tr>
<tr>
<td><code>&quot;reduce&quot;</code></td>
<td><code>red</code></td>
<td>motionless ring, whatever Windows says</td>
</tr>
<tr>
<td><code>&quot;full&quot;</code></td>
<td><code>ful</code></td>
<td>animation, whatever Windows says</td>
</tr>
</tbody>
</table></div>
<p>Four design points deserve some explanation, because they are counter-intuitive:</p>
<p><strong>The base state is static.</strong> <code>.halo</code> on its own carries no animation. The animation is added by <code>.ful</code>, or by <code>.sys</code> under the media query. This is the opposite of what one writes spontaneously (declare the animation, then cancel it with <code>animation: none</code> in a <code>reduce</code> block), and it is the opposite of what the halo in section 2 does. The inverted version is safer: if the CSS is not evaluated, if the class does not arrive, if <code>gblMotionMode</code> is empty, the result is the motionless ring. <strong>The safe behaviour becomes the default behaviour</strong>, while the animation is only enabled when explicitly asked for. To illustrate a principle, you want to see the cancellation; in production, you would rather a failure stay silent and harmless.</p>
<p><strong>The <code>Switch</code> has a default case.</strong> Its last value, <code>&quot;sys&quot;</code>, applies if <code>gblMotionMode</code> is empty, which is the state of a user who has never set anything. So the application's default behaviour is &quot;follow the system&quot;. That is the right default, and it has to be chosen deliberately: a boolean <code>If()</code> written in haste would return <code>Blank()</code> on the <code>else</code> branch and give you exactly the opposite of what you wanted.</p>
<p><strong>The descendant selector works</strong> because the root <code>&lt;svg&gt;</code> really is an ancestor of the <code>&lt;circle&gt;</code>. This point often causes confusion, but the behaviour here is entirely ordinary.</p>
<p><strong><code>reduce</code> does not mean &quot;nothing&quot;.</strong> Note the <code>stroke-width</code>: 2 in animated mode, 2.5 in motionless mode. That is the rule from section 5, applied.</p>
<p>And on the Power Fx side:</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx · App.OnStart" data-pfx="1"><pre><code class="lang-powerfx"><span class="hl-fn">Set</span><span class="hl-punct">(</span>gblBust<span class="hl-punct" data-sep="a">,</span> <span class="hl-fn">GUID</span><span class="hl-punct">(</span><span class="hl-punct">)</span><span class="hl-punct">)</span><span class="hl-punct" data-sep="c">;</span>
<span class="hl-fn">Set</span><span class="hl-punct">(</span>
    gblMotionMode<span class="hl-punct" data-sep="a">,</span>
    <span class="hl-fn">Coalesce</span><span class="hl-punct">(</span>
        <span class="hl-fn">LookUp</span><span class="hl-punct">(</span>Preferences<span class="hl-punct" data-sep="a">,</span> User <span class="hl-op">=</span> <span class="hl-fn">User</span><span class="hl-punct">(</span><span class="hl-punct">)</span><span class="hl-prop">.Email</span><span class="hl-punct" data-sep="a">,</span> MotionMode<span class="hl-punct">)</span><span class="hl-punct" data-sep="a">,</span>
        <span class="hl-str">"system"</span>
    <span class="hl-punct">)</span>
<span class="hl-punct">)</span></code></pre></div></div>
<p>A simple Radio or Dropdown with three options in your preferences screen, a <code>Patch</code> to your list, and you hand control to the user without ever forcing them to configure anything.</p>
<div class="rm-modes" data-rm-modes role="group" aria-label="The three modes, try them">
<p class="rm-modes__lead">Pick a mode: the preview rebuilds itself exactly as <code>gblMotionMode</code> would in your own application.</p>
<div class="rm-modes__grid">
<div class="rm-modes__choix">
<label class="rm-modes__opt"><input type="radio" name="rm-mode" value="sys" checked>
<span class="rm-modes__nom"><code>"system"</code> or empty</span>
<span class="rm-modes__aide">Class <code>sys</code>: the CSS decides, through the media query. This is the default.</span></label>
<label class="rm-modes__opt"><input type="radio" name="rm-mode" value="red">
<span class="rm-modes__nom"><code>"reduce"</code></span>
<span class="rm-modes__aide">Class <code>red</code>: motionless ring, whatever Windows says.</span></label>
<label class="rm-modes__opt"><input type="radio" name="rm-mode" value="ful">
<span class="rm-modes__nom"><code>"full"</code></span>
<span class="rm-modes__aide">Class <code>ful</code>: animation, whatever Windows says.</span></label>
</div>
<div class="rm-modes__apercu">
<span class="rm-modes__stage"><span class="rm-modes__num">18</span><img class="rm-modes__svg" id="rm-mode-img" alt="Preview of the halo in the selected mode."></span>
<p class="rm-modes__cap" id="rm-mode-cap"></p>
</div>
</div>
</div>
<style>
.rm-modes{margin:0 0 26px;padding:18px;border:1px solid var(--border);border-radius:12px;background:var(--bg-panel)}
.rm-modes__lead{margin:0 0 16px;font-size:13.5px;line-height:1.6;color:var(--text-dim)}
.rm-modes__lead code,.rm-modes__nom code,.rm-modes__aide code,.rm-modes__cap code{font-family:var(--mono);font-size:12px}
.rm-modes__grid{display:grid;grid-template-columns:minmax(0,1fr) 210px;gap:20px;align-items:center}
/* La colonne d'apercu est FIXE : en « auto », la legende la plus longue
   (mode « full ») elargissait la colonne et ecrasait les boutons radio. */
.rm-modes__apercu{min-width:0}
@media (max-width:560px){.rm-modes__grid{grid-template-columns:minmax(0,1fr)}}
.rm-modes__choix{display:flex;flex-direction:column;gap:10px}
.rm-modes__opt{display:grid;grid-template-columns:auto minmax(0,1fr);gap:2px 10px;align-items:start;padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-dark);cursor:pointer}
.rm-modes__opt:hover{border-color:var(--primary)}
.rm-modes__opt input{grid-row:1/3;align-self:center;accent-color:var(--primary);width:16px;height:16px;cursor:pointer}
.rm-modes__opt:has(input:checked){border-color:var(--primary)}
.rm-modes__opt input:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
.rm-modes__nom{font-size:13.5px;color:var(--text-light)}
.rm-modes__aide{font-size:12.5px;line-height:1.5;color:var(--text-dim)}
.rm-modes__apercu{text-align:center}
.rm-modes__stage{position:relative;display:block;width:104px;height:104px;margin:0 auto}
.rm-modes__num{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--text-light)}
.blog-post .rm-modes__svg{position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:0}
.rm-modes__cap{margin:12px 0 0;min-height:2.6em;font-size:12.5px;line-height:1.5;color:var(--text-dim)}
</style>
<script>
(function(){
var d=document.querySelector('[data-rm-modes]');if(!d)return;
var K="@keyframes pulse{from{transform:scale(.78);opacity:.9}to{transform:scale(1.12);opacity:0}}";
var BASE=".halo{fill:none;stroke:#0078d4;stroke-width:2.5;transform-origin:20px 20px}";
var ANIM=".halo{stroke-width:2;animation:pulse 1.8s ease-out infinite}";
var img=d.querySelector('#rm-mode-img'),cap=d.querySelector('#rm-mode-cap');
var mq=window.matchMedia('(prefers-reduced-motion: reduce)');
function uri(css){
return "data:image/svg+xml;utf8,"+encodeURIComponent(
"<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'><style>"+BASE+K+css+"</style><circle class='halo' cx='20' cy='20' r='17'/></svg>");
}
function paint(){
var m=(d.querySelector('input[name=rm-mode]:checked')||{}).value||'sys';
var anime=(m==='ful')||(m==='sys'&&!mq.matches);
img.src=uri(anime?ANIM:"");
cap.innerHTML=m==='sys'
?(mq.matches?"Your system answers <code>reduce</code>: the ring stays still, without you setting anything."
:"Your system answers <code>no-preference</code>: the halo breathes.")
:(m==='red'?"Motionless, even though your system asked for nothing."
:"Animated, even though your system asks for <code>reduce</code>. That is an explicit user choice.");
}
d.addEventListener('change',paint);
if(mq.addEventListener)mq.addEventListener('change',paint);
paint();
})();
</script>

<div class="partie" id="partie-4">
<p class="partie__num">Part 4 of 5 · 11 min read</p>

<h2 class="partie__titre">The proof</h2>
<p class="partie__lead">Everything above rests on a claim that had to be verified rather than believed. This is the longest part, and the easiest to skip: if you take my word for it, go straight to part 5. If, on the contrary, you want the test bench to paste into your own app, it is here.</p>
</div>
<h2 data-toc="7. The test bench" id="but-does-it-really-work-the-test-bench">7. &quot;But does it really work?&quot;: the test bench</h2>
<p>Everything above rests on one claim: the media query is evaluated inside an SVG image displayed by a PowerApps control. Here is how I verified it, and how you can verify it on your own hosts in thirty seconds.</p>
<p>The complete formula, to paste into the <code>Image</code> property of an Image control:</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx"><pre><code class="lang-powerfx"><span class="hl-str">"data:image/svg+xml;utf8,"</span> <span class="hl-op">&amp;</span> <span class="hl-fn">EncodeUrl</span><span class="hl-punct">(</span>
    <span class="hl-str">"&lt;svg xmlns='http://www.w3.org/2000/svg' width='600' height='340' viewBox='0 0 600 340' data-build='"</span> <span class="hl-op">&amp;</span> gblBust <span class="hl-op">&amp;</span> <span class="hl-str">"'&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;style&gt;"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"text{font-family:system-ui,-apple-system,sans-serif;font-size:19px}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".sm{font-size:15px;fill:#8a8886}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".a0{opacity:1;fill:#d13438}.a1{opacity:0;fill:#107c10}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".b0{opacity:1;fill:#d13438}.b1{opacity:0;fill:#107c10}.b2{opacity:0;fill:#107c10}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".c0{opacity:1;fill:#d13438}.c1{opacity:0;fill:#8a8886}.c2{opacity:0;fill:#0078d4}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">".mv{fill:#d13438;animation:mv 1.2s linear infinite alternate}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@keyframes mv{from{transform:translateX(0)}to{transform:translateX(480px)}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (min-width:1px){.a0{opacity:0}.a1{opacity:1}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (prefers-color-scheme:light){.b0{opacity:0}.b1{opacity:1}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (prefers-color-scheme:dark){.b0{opacity:0}.b2{opacity:1}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (prefers-reduced-motion:no-preference){.c0{opacity:0}.c1{opacity:1}}"</span> <span class="hl-op">&amp;</span>
        <span class="hl-str">"@media (prefers-reduced-motion:reduce){.c0{opacity:0}.c2{opacity:1}.mv{fill:#0078d4;animation:none}}"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/style&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;rect class='mv' x='20' y='20' width='45' height='45' rx='8'/&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='a0' x='20' y='130'&gt;1. CSS engine: inactive&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='a1' x='20' y='130'&gt;1. CSS engine: active&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='b0' x='20' y='175'&gt;2. prefers-color-scheme: not evaluated&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='b1' x='20' y='175'&gt;2. prefers-color-scheme: light&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='b2' x='20' y='175'&gt;2. prefers-color-scheme: dark&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='c0' x='20' y='220'&gt;3. prefers-reduced-motion: not evaluated&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='c1' x='20' y='220'&gt;3. prefers-reduced-motion: no-preference&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='c2' x='20' y='220'&gt;3. prefers-reduced-motion: reduce&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='sm' x='20' y='300'&gt;build: "</span> <span class="hl-op">&amp;</span> gblBust <span class="hl-op">&amp;</span> <span class="hl-str">"&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;/svg&gt;"</span>
<span class="hl-punct">)</span></code></pre></div></div>
<p>And in the app's <code>OnStart</code> (or the screen's <code>OnVisible</code>, or the <code>OnSelect</code> of a &quot;Refresh&quot; button):</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx" data-pfx="1"><pre><code class="lang-powerfx"><span class="hl-fn">Set</span><span class="hl-punct">(</span>gblBust<span class="hl-punct" data-sep="a">,</span> <span class="hl-fn">Text</span><span class="hl-punct">(</span><span class="hl-fn">Now</span><span class="hl-punct">(</span><span class="hl-punct">)</span><span class="hl-punct" data-sep="a">,</span> <span class="hl-str">"[$-en-US]yyyymmddhhmmssfff"</span><span class="hl-punct">)</span><span class="hl-punct">)</span></code></pre></div></div>
<p>A timestamp rather than a <code>GUID()</code> here is deliberate: the bench <em>displays</em> that value at the bottom of the image, and reading the render time keeps you from drawing conclusions on a stale screen. In production, where it is invisible, <code>GUID()</code> does better, and section 8 explains why. As for the <code>[$-en-US]</code> prefix, it is not decorative either: section 10 is devoted to it.</p>
<p>The code looks like a slightly odd pile of CSS classes, but in reality each block answers a precise question of method.</p>
<h3 id="the-principle-three-states-one-of-them-i-do-not-know">7.1. The principle: three states, one of them &quot;I do not know&quot;</h3>
<p>The whole trick rests on a design choice: <strong>each probe has a visible default state, meaning &quot;not evaluated&quot;.</strong></p>
<p>Take probe 3:</p>
<ul>
<li>
<p><code>.c0</code>, &quot;not evaluated&quot;: <code>opacity: 1</code> by default, so <strong>visible</strong>.</p>
</li>
<li>
<p><code>.c1</code>, &quot;no-preference&quot;: <code>opacity: 0</code> by default, revealed only by the matching media query.</p>
</li>
<li>
<p><code>.c2</code>, &quot;reduce&quot;: likewise.</p>
</li>
</ul>
<p>The three texts are stacked at exactly the same place (<code>x='20' y='220'</code>). Only one is visible at a time. <strong>The displayed text <em>is</em> the result.</strong></p>
<p>Why this third state? Because without it, two radically different situations cannot be told apart:</p>
<ol>
<li>
<p>the media query is supported and returns <code>false</code>;</p>
</li>
<li>
<p>the media query is not supported at all, or the CSS does not run.</p>
</li>
</ol>
<p>In both cases, the screen would show nothing special. This is a <strong>negative control</strong>: without it, the experiment proves nothing. With it, a &quot;not evaluated&quot; left showing in red is usable information, not an ambiguous silence.</p>
<p>It is the same reflex as a unit test that must fail first, so you know it is really testing something.</p>
<h3 id="probe-1-the-canary">7.2. Probe 1: the canary</h3>
<div class="code-block"><div class="code-wrapper"><pre><code class="lang-css"><span class="hl-meta">@media</span> <span class="hl-punct">(</span><span class="hl-prop">min-width</span><span class="hl-punct">:</span><span class="hl-num">1px</span><span class="hl-punct">)</span><span class="hl-punct">{</span><span class="hl-sel">.a0</span><span class="hl-punct">{</span><span class="hl-prop">opacity</span><span class="hl-punct">:</span><span class="hl-num">0</span><span class="hl-punct">}</span><span class="hl-sel">.a1</span><span class="hl-punct">{</span><span class="hl-prop">opacity</span><span class="hl-punct">:</span><span class="hl-num">1</span><span class="hl-punct">}</span><span class="hl-punct">}</span></code></pre></div></div>
<p>This one tests no preference at all. <code>min-width: 1px</code> is true in every case where CSS is evaluated (the SVG is 600 px wide: for this condition, the browser goes by the width of the SVG itself, not that of the screen).</p>
<p>It is a <strong>canary</strong>. If &quot;CSS engine: active&quot; shows in green, we know that:</p>
<ul>
<li>
<p>the data URI was decoded correctly;</p>
</li>
<li>
<p>the <code>&lt;style&gt;</code> tag was parsed;</p>
</li>
<li>
<p>class selectors work;</p>
</li>
<li>
<p>media queries are evaluated.</p>
</li>
</ul>
<p>If that first light stays red, there is no point interpreting the next two: the problem is upstream, in the encoding or in the rendering. That saves hours of false leads along the lines of &quot;prefers-reduced-motion does not work in PowerApps&quot; when in fact an <code>EncodeUrl</code> was simply missing.</p>
<p>An important point for reading the results: <strong>this canary proves nothing about user preferences.</strong> It stays green even if the engine refuses to resolve <code>prefers-reduced-motion</code> from the system preferences. That is precisely why all three probes are needed, not only the one we care about.</p>
<h3 id="probes-2-and-3-the-real-questions">7.3. Probes 2 and 3: the real questions</h3>
<p>They follow exactly the same mould, with two positive values each. Note that for <code>prefers-reduced-motion</code>, we explicitly query <strong>both</strong> values, <code>no-preference</code> <strong>and</strong> <code>reduce</code>, rather than writing only <code>@media (prefers-reduced-motion: reduce)</code>. Once again: we want to tell &quot;the system answers no-preference&quot; apart from &quot;nobody answers&quot;.</p>
<p><strong>A point of rigour about probe 2:</strong> <code>no-preference</code> is no longer a value of <code>prefers-color-scheme</code>; no browser ever implemented it, and it was removed from the specification. The <code>light</code> value covers both the case where the user explicitly prefers the light theme and the case where no active preference is expressed. The <code>.b1</code> probe therefore cannot tell those two situations apart: it shows in both.</p>
<h3 id="the-red-square-demonstration-on-top-of-measurement">7.4. The red square: demonstration on top of measurement</h3>
<div class="code-block"><div class="code-wrapper"><pre><code class="lang-css"><span class="hl-sel">.mv</span><span class="hl-punct">{</span><span class="hl-prop">fill</span><span class="hl-punct">:</span><span class="hl-num">#d13438</span><span class="hl-punct">;</span><span class="hl-prop">animation</span><span class="hl-punct">:</span>mv <span class="hl-num">1.2s</span> linear infinite alternate<span class="hl-punct">}</span>
<span class="hl-meta">@keyframes</span> mv<span class="hl-punct">{</span>from<span class="hl-punct">{</span><span class="hl-prop">transform</span><span class="hl-punct">:</span><span class="hl-fn">translateX</span><span class="hl-punct">(</span><span class="hl-num">0</span><span class="hl-punct">)</span><span class="hl-punct">}</span>to<span class="hl-punct">{</span><span class="hl-prop">transform</span><span class="hl-punct">:</span><span class="hl-fn">translateX</span><span class="hl-punct">(</span><span class="hl-num">480px</span><span class="hl-punct">)</span><span class="hl-punct">}</span><span class="hl-punct">}</span></code></pre></div></div>
<p>This little square travels back and forth across 480 px. Under <code>prefers-reduced-motion: reduce</code>, it turns blue and stops.</p>
<p>This is not decoration. The text proves that <strong>the media query is evaluated</strong>; the square proves that <strong>the CSS animation really runs in this context</strong> and that it obeys the rule. That is what takes the result from &quot;interesting&quot; to &quot;usable&quot;: it is not enough for the browser to know how to answer the question, the animation engine must also run inside an SVG rendered as an image.</p>
<p>On a test bench, this way of writing it is the right one: it makes the cancellation visible. In production code, you invert it: that is the pattern in section 6.</p>
<h3 id="the-result">7.5. The result</h3>
<p>On my machine (Windows 11, PowerApps in Chrome), all three probes answer, and the square stops when I turn animation effects off in Windows <strong>and trigger a fresh read</strong>. That last point is no detail: without a fresh read, nothing moves, and that is exactly what sections 8 and 9 are about.</p>
<p><strong><code>prefers-reduced-motion</code> works in PowerApps.</strong></p>
<figure class="rm-video">
<video class="rm-video__lecteur" controls preload="metadata" playsinline
       width="1280" height="720"
       aria-describedby="rm-video-desc">
<source src="https://assets.nelsonboruchowitch.com/media/powerappsreduce.mp4" type="video/mp4">
<p class="rm-video__repli">Your browser cannot play this video.
<a href="https://assets.nelsonboruchowitch.com/media/powerappsreduce.mp4">Download it (MP4, 690 KB)</a>.</p>
</video>
<figcaption class="rm-video__legende" id="rm-video-desc">
The test bench in a published application. All three probes answer, then animation effects
are turned off in Windows: once the value is read again, the third probe switches to
<code>reduce</code> and the square stops, in blue.
</figcaption>
</figure>
<style>
.rm-video{margin:0 0 26px;padding:14px;border:1px solid var(--border);border-radius:12px;background:var(--bg-panel)}
.rm-video__lecteur{display:block;width:100%;height:auto;max-width:100%;border-radius:8px;background:#000}
.rm-video__repli{margin:0;padding:16px;font-size:13.5px;line-height:1.6;color:var(--text-dim)}
.rm-video__legende{margin:12px 2px 0;font-size:12.5px;line-height:1.6;color:var(--text-dim)}
.rm-video__legende code{font-family:var(--mono);font-size:12px}
</style>

<h2 data-toc="8. gblBust" id="gblbust-is-not-a-test-artefact">8. <code>gblBust</code> is not a test artefact</h2>
<p>This is by far the point that cost me the most time. It is in fact <strong>the reading mechanism itself</strong>.</p>
<p>The data URI <strong>is</strong> the image's URL. If the produced string is identical byte for byte, the browser returns the already decoded image from its memory cache. Yet it is at decoding time, and only then, that the SVG's media queries are evaluated. Unchanged string, no decoding. No decoding, no evaluation.</p>
<p>You change the setting in Windows, you come back to the app, you click everywhere... and nothing moves. Not because it does not work, but because <strong>nothing has been reloaded</strong>.</p>
<p>The <code>data-build</code> attribute is entirely inert: it has no effect on the rendering. Its only job is to make the string vary, hence the URL, hence to force a fresh decode and a fresh evaluation of the media queries. And the <code>build: ...</code> line shown at the bottom of our test bench is not cosmetic: it tells you <strong>which version you are actually looking at</strong>.</p>
<p>A direct consequence, and the one that makes the pattern usable in production:</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx · App.OnStart" data-pfx="1"><pre><code class="lang-powerfx"><span class="hl-fn">Set</span><span class="hl-punct">(</span>gblBust<span class="hl-punct" data-sep="a">,</span> <span class="hl-fn">GUID</span><span class="hl-punct">(</span><span class="hl-punct">)</span><span class="hl-punct">)</span></code></pre></div></div>
<p>That line guarantees that at every session opening the data URI string is recreated, then decoded and evaluated against the current state of the system. So a user who has disabled animation effects in Windows automatically gets that setting honoured in your app, with no extra configuration.</p>
<p>It is the line announced in section 2, and the halo must therefore carry a <code>data-build='&quot; &amp; gblBust &amp; &quot;'</code> on its <code>&lt;svg&gt;</code> tag too, exactly as the three-mode pattern in section 6 does.</p>
<p>In production the value is invisible and serves only to break the cache: <code>GUID()</code> guarantees a unique value without depending on the locale or on a date format. During testing, the timestamp was the relevant choice, since it was displayed.</p>
<p>A word on the real scope of that line. On a full page reload, a new document is created and images are decoded afresh anyway. <code>gblBust</code> is there to cover the other cases, which are numerous in an application: internal navigation, redisplaying a screen, coming back from another tab. It is one line of insurance.</p>
<h2 id="the-decoding-lock">9. The decoding lock</h2>
<p>Here is the behaviour that surprises everyone, myself first, and that has a precise explanation.</p>
<p>Open the app so that the test bench shows <code>no-preference</code>, the square moves. Without closing the app, go into the Windows settings, disable animation effects, come back. The square is still moving.</p>
<p>It is not that the media query failed, it is that <strong>the image buffer was not repainted</strong>. Chromium has a filed bug for this, issue 1093736: SVG images are not repainted when media queries change. Rendering works most of the time, but it does not react if the preference flips mid-session. The documented workaround is exactly the one in section 8: force the repaint.</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx" data-pfx="1"><pre><code class="lang-powerfx"><span class="hl-com">// "Re-read system preference" button, OnSelect</span>
<span class="hl-fn">Set</span><span class="hl-punct">(</span>gblBust<span class="hl-punct" data-sep="a">,</span> <span class="hl-fn">GUID</span><span class="hl-punct">(</span><span class="hl-punct">)</span><span class="hl-punct">)</span></code></pre></div></div>
<p>So we have to be precise about what this pattern is and is not. <strong>It is not a stream, it is a snapshot.</strong> The SVG reads the preference at the moment it is decoded, and keeps that result until the next decode. Nothing notifies it of a change.</p>
<p>For the more curious among you! We have to go further still, because all of this leads to one question: if the SVG knows the preference, can we not make it tell us? So I tried, again and again. The Image control has a property the engine fills in itself, <code>OriginalWidth</code>, available with <code>CalculateOriginalDimensions</code>. The idea was to build an SVG whose width would depend on the media query: the value would have become a boolean in disguise, readable from Power Fx.</p>
<p>The canary gave me a clear no. With a media query true everywhere, <code>@media (min-width:1px){svg{width:120px}}</code>, the reported width never budged: 240 when the document declares <code>width='240'</code>, and 300 when it declares nothing, that is, the browser's fallback size. Never 120, and the readings are rigorously identical whether Windows animation effects are on or off. The CSS is nonetheless very much evaluated, as section 7 proves; it simply plays no part in computing the size reported to the host. <strong>The engine returns what the document declares, never what it drew.</strong></p>
<p>One detail of that attempt is worth keeping, because it can catch you out beyond this topic: between two decodes, <code>OriginalWidth</code> passes through <code>0</code>. A naive reading such as <code>width &lt; 200</code> therefore returns <code>true</code> during that interval, while the image has not yet recovered its dimensions. That transient state can then be mistaken for a preference. To avoid it, first check that the width really is greater than 0: <code>width &gt; 0 And width &lt; 200</code>.</p>
<p>The conclusion is therefore sharper than the mere observation of a snapshot: <strong>the SVG obeys the preference, and the application will never know.</strong> No value crosses the border in that direction, neither through the Image control nor through the HTML Text control, whose <code>HtmlText</code> property merely returns the string your own formula produced. To get a genuine boolean into Power Fx, you need a code component (PCF) and its <code>window.matchMedia</code>.</p>
<p>This point can also be verified with the method presented in section 4. The demonstration fits on a single screen, in the published app: two <code>&lt;img&gt;</code> tags display the <strong>same</strong> SVG side by side, one with <code>gblBust</code> in its string and one without. You disable animations in Windows without closing the app, you trigger the bust, then you look at the result.</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Route</th>
<th>Path taken by the CSS</th>
<th>Canary</th>
<th>Verdict read</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td><code>&lt;style&gt;</code> tag written in the HTML Text control</td>
<td><strong>inactive</strong></td>
<td>nothing, the style sheet was stripped</td>
</tr>
<tr>
<td>B</td>
<td><code>&lt;img&gt;</code> data URI in the HTML Text control, with <code>gblBust</code></td>
<td>active</td>
<td><code>reduce</code></td>
</tr>
<tr>
<td>C</td>
<td>the same <code>&lt;img&gt;</code>, without <code>gblBust</code></td>
<td>active</td>
<td><code>no-preference</code>, frozen</td>
</tr>
<tr>
<td>D</td>
<td>Image control, with <code>gblBust</code></td>
<td>active</td>
<td><code>reduce</code></td>
</tr>
</tbody>
</table></div>
<p>The decisive detail is the canary: it is active in both cases. So the frozen image is not broken. It reacted correctly at the moment it was evaluated, and nobody has re-evaluated it since. <strong>&quot;Not evaluated&quot; and &quot;evaluated ten minutes ago&quot; can look identical on screen, yet they are two very different situations and they do not call for the same fix.</strong> That is precisely what the third state from section 7.1 lets you tell apart, and why it must be planned for from the very first draft.</p>
<p>Which leaves the essential question: <strong>is it serious?</strong></p>
<p>For this particular media feature, no. And the reason is sociological rather than technical.</p>
<p>Dark mode is something people may switch several times a day, often automatically at sunset. <code>prefers-reduced-motion</code> is not. Someone sensitive to motion ticks the box once, when setting up their machine, and never comes back to it. <strong>The value read when the session opens <em>is</em> the value, durably.</strong> The hot-update capability this pattern lacks is a capability nobody needs here.</p>
<p>It is, incidentally, exactly the reading model of everything else in PowerApps. <code>Language()</code>, <code>Host.OSType</code>, <code>User()</code>, everything you initialise in <code>OnStart</code>: those are values read at startup, and nobody expects them to propagate live. Nobody blames PowerApps for not reacting instantly to a change of Windows display language: you restart the app. The pattern is not a shaky exception within the platform, it is consistent with it.</p>
<p>The nuance to keep in mind, and it matters if you extend the technique: this argument is solid for <code>prefers-reduced-motion</code> and for <code>forced-colors</code>, which are settings you set once. I consider it markedly weaker for <code>prefers-color-scheme</code>, which many users have switch automatically according to the time of day. For that one, the startup snapshot will regularly hand you a stale value during the day.</p>
<div class="partie" id="partie-5">
<p class="partie__num">Part 5 of 5 · 8 min read</p>

<h2 class="partie__titre">The details that matter</h2>
<p class="partie__lead">What is left, and fitted into none of the first four parts: the Power Fx pitfalls of the data URI, the complete chain from the Windows setting to the pixel, the three-level architecture that makes the whole thing compliant, and the other system preferences the same technique can reach.</p>
</div>
<h2 data-toc="10. Power Fx pitfalls" id="the-power-fx-pitfalls-of-the-data-uri">10. The Power Fx pitfalls of the data URI</h2>
<p>A few details that explain the shape of the code:</p>
<p><strong><code>EncodeUrl</code> is not optional.</strong> A data URI is a URL. The <code>#</code> character has a special meaning there: it opens a fragment. Without encoding, <code>#d13438</code> simply truncates your SVG at the first colour code. <code>EncodeUrl</code> turns it into <code>%23d13438</code>, and does the same for <code>&lt;</code>, <code>&gt;</code>, spaces, and so on.</p>
<p><strong>Single quotes inside the SVG.</strong> In Power Fx a string is delimited by <code>&quot;</code>, and a literal quote is written <code>&quot;&quot;</code>. Writing XML with attributes in <code>&quot;&quot;</code> doubles the length of the code and makes it unreadable. XML happily accepts <code>'</code> as an attribute delimiter: <code>width='600'</code>. That keeps the formula readable. <strong>Careful, though:</strong> if you edit the code and double an apostrophe out of habit, the SVG becomes invalid and the control shows a broken image without the slightest error message.</p>
<p><strong><code>;utf8</code> rather than <code>;base64</code>.</strong> Power Fx has no convenient base64 encoder for this case, and above all: in plain text the formula stays readable, easy to compare and to debug. You can copy the output into a <code>.svg</code> file and open it directly.</p>
<p><strong>Accented characters.</strong> <code>EncodeUrl</code> handles UTF-8 correctly, but I have got into the habit of writing my test labels without accents. When the aim is to isolate <em>one</em> variable, it is better to avoid a possible encoding problem muddying the diagnosis. In a production component, put your accents back and test them.</p>
<p><strong>Length.</strong> Power Fx can handle very long formulas, but a needlessly bulky formula stays hard to maintain. The data URI itself can quickly become heavy, especially when repeated for every row of a gallery. So minify the CSS (no superfluous spaces, short selectors) and avoid generating a needlessly bulky data URI per gallery row.</p>
<p><strong>Format strings are in the user's language.</strong> <code>Text()</code> interprets its tokens at run time, in the locale of whoever opens the app, not in yours as you write it. Hence the <code>[$-en-US]</code> prefix in the test bench: without it, a Dutch or English speaking user does not read the same tokens as you. In a multilingual environment, that is far from theoretical.</p>
<h2 data-toc="11. The complete chain" id="the-complete-chain-from-the-windows-setting-to-the-pixel">11. The complete chain: from the Windows setting to the pixel</h2>
<p>This is what makes the result genuinely useful. It is not a CSS gadget: it is a signal that crosses five layers.</p>
<div class="code-block"><div class="code-wrapper"><pre><code>OS accessibility setting
        ↓
System parameter (Windows: SPI_GETCLIENTAREAANIMATION)
        ↓
Browser / WebView engine (Edge, Chrome, WebView2, WKWebView)
        ↓
CSS media feature: prefers-reduced-motion
        ↓
Decoding of the data URI          ← the reading point, and the only one
        ↓
Your SVG in an Image or HTML Text control</code></pre></div></div>
<p>Where the user pulls the lever, depending on the platform:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Platform</th>
<th>Path</th>
</tr>
</thead>
<tbody>
<tr>
<td>Windows 11</td>
<td>Settings → Accessibility → Visual effects → <strong>Animation effects</strong> (turn off)</td>
</tr>
<tr>
<td>Windows 10</td>
<td>Settings → Ease of Access → Display → <strong>Show animations in Windows</strong></td>
</tr>
<tr>
<td>macOS</td>
<td>System Settings → Accessibility → Display → <strong>Reduce motion</strong></td>
</tr>
<tr>
<td>iOS / iPadOS</td>
<td>Settings → Accessibility → Motion → <strong>Reduce Motion</strong></td>
</tr>
<tr>
<td>Android</td>
<td>Settings → Accessibility → <strong>Remove animations</strong></td>
</tr>
</tbody>
</table></div>
<p>Beware the neighbouring-setting trap: on Windows it is neither the transparency effects nor the performance options in the advanced system panel. It really is <strong>Animation effects</strong>, under Accessibility.</p>
<p>And to test during development without touching your own machine: in DevTools (F12) → <code>⋮</code> menu → More tools → <strong>Rendering</strong> → <em>Emulate CSS media feature prefers-reduced-motion</em>. You flip the value, you trigger your <code>gblBust</code>, you observe. It is also the quickest way to check that a problem really comes from the SVG and not from Windows: under emulation, the OS is bypassed.</p>
<p>The point worth carrying into a meeting with a business team or an accessibility department: <strong>the user has nothing to configure in your application.</strong> It silently honours a preference they have already expressed elsewhere, once and for all.</p>
<h2 id="the-cascade-of-preferences">12. The cascade of preferences</h2>
<p>One thing remains that this pattern cannot do, and it is structural: <strong>the channel is one-way.</strong> The SVG can <em>see</em> the preference, but it has no way of sending it back to Power Fx. No JavaScript, no output, no event. You will never write <code>If(gblReducedMotion, ...)</code> on the back of this mechanism. That is why the pattern consists of <strong>pushing the animation down into the SVG</strong> rather than trying to lift a boolean back up.</p>
<p>Only a PCF component opens that return channel, with a <code>matchMedia</code> listener notifying Power Fx in real time. That is the complete solution, and if code components are allowed in your environment, that is what to aim for in the long run.</p>
<p>An immediate consequence: everything you animate with a Timer control and <code>Set()</code> calls on <code>X</code> / <code>Y</code> / <code>Height</code> properties is pure Power Fx. CSS has no hold over it. Those animations will keep running.</p>
<p>And that is precisely where the two approaches answer each other. Look at what each one sees:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th></th>
<th>What it drives</th>
<th>What it cannot see</th>
</tr>
</thead>
<tbody>
<tr>
<td>System inheritance, through CSS</td>
<td>animations declared in the SVG</td>
<td>Power Fx Timers</td>
</tr>
<tr>
<td>Application override, through Power Fx</td>
<td>Power Fx Timers</td>
<td>animations inside the SVG</td>
</tr>
</tbody>
</table></div>
<p>The two cover <strong>exactly</strong> each other's blind spot. This is not a workaround stacked on top of a trick, it is a clean split of responsibilities. Hence the architecture I recommend, in three levels that override one another in this order:</p>
<p><strong>Level 0: the platform default.</strong> Animation on. That is what you have today, doing nothing.</p>
<p><strong>Level 1: system inheritance.</strong> Read when the session opens, applied silently, with no configuration asked of the user. They have already expressed their preference once, at system level. That is exactly the model WCAG encourages, and it is what separates an application that <em>allows</em> accessibility from one that is accessible by default.</p>
<p><strong>Level 2: the application override.</strong> An explicit setting in your preferences screen, stored and taking priority over level 1. It is the one that covers Power Fx animations, and the one that formally satisfies WCAG 2.2.2, whose criterion asks for a pause mechanism and not for a media feature to be honoured. <code>prefers-reduced-motion</code> only appears among the sufficient techniques, not in the criterion itself.</p>
<p>The formulation that sums it all up, and which is the real result of this article: <strong>level 1 without level 2 is incomplete, level 2 without level 1 is mediocre.</strong> One gives the right default without asking anything, the other gives control and compliance. Together they do markedly better than either alone.</p>
<p>Two reservations to close, which do not undermine the architecture but must be stated:</p>
<p><strong>Rendering depends on the host.</strong> See section 7.5. Test on your real users' platforms before relying on it for an important feature.</p>
<p><strong>This is not a mechanism documented by Microsoft.</strong> It is an emergent property of the stack (Canvas App → browser → SVG image → CSS). It is not part of the platform's contract. That is one more reason to keep level 2: that one is entirely under your control. Level 1 <em>improves</em> the experience, level 2 <em>guarantees</em> it.</p>
<h2 data-toc="13. The other media features" id="what-comes-next-the-other-media-features">13. What comes next: the other media features</h2>
<p>The test bench is extensible as it stands. You only have to add probes on the same &quot;state 0 = not evaluated&quot; model:</p>
<ul>
<li>
<p><strong><code>forced-colors: active</code></strong>: the Windows High Contrast mode. Probably the most useful of the four in a corporate context: when it is on, your hard-coded colours in an SVG are ignored or replaced, and your icons can become illegible. A dedicated probe is worth its weight in gold. And like <code>prefers-reduced-motion</code>, it is a setting you set once: the startup snapshot is enough.</p>
</li>
<li>
<p><strong><code>prefers-contrast: more | less</code></strong>: a request for increased contrast, independently of the colour mode.</p>
</li>
<li>
<p><strong><code>prefers-color-scheme: dark</code></strong>: to align your SVG illustrations with the system theme rather than a frozen application theme. This one is the poor pupil of the bunch: it flips mid-session, and you will run into the problem described in section 9. Plan a re-read trigger, or stick to the application theme.</p>
</li>
</ul>
<p>Each of them can, once validated, be used exactly like <code>prefers-reduced-motion</code>: straight inside the SVG, without a single <code>If()</code> in Power Fx.</p>
<hr />
<h2 id="what-to-remember">What to remember</h2>
<ol>
<li>
<p>Your animated SVGs run without ever asking the user's opinion. Power Fx cannot read the system's accessibility preferences. CSS embedded in an SVG can.</p>
</li>
<li>
<p>A media query only gets into a Canvas App through an SVG data URI. Neither an inline <code>style=</code> attribute nor the <code>&lt;style&gt;</code> tag of the HTML Text control can carry one: the sanitiser strips the latter, and an at-rule does not fit into the former. The data URI, on the other hand, goes through the Image control just as well as through an <code>&lt;img&gt;</code> placed in the HTML Text control.</p>
</li>
<li>
<p>The reading happens <strong>at decoding time</strong>, so when the session opens. It is a snapshot, not a stream. For a preference set once in a lifetime, that is enough.</p>
</li>
<li>
<p><code>gblBust</code> is not a debugging trick: it is the line that triggers the reading.</p>
</li>
<li>
<p>Reducing is not removing. Replace the motion with a static equivalent that carries the same information.</p>
</li>
<li>
<p>In production, invert the logic: static base state, animation opt-in. The safe behaviour must be the default behaviour.</p>
</li>
<li>
<p>System inheritance for the right default, application override for control and compliance. Both, not one or the other.</p>
</li>
<li>
<p>The three-state pattern, one of them a &quot;not evaluated&quot; state visible by default, turns an image into a measuring instrument with its own negative control, useful well beyond this topic.</p>
</li>
</ol>
]]></content:encoded>
    </item>
  </channel>
</rss>
