<?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>Blog de Nelson Boruchowitch</title>
    <link>https://nelsonboruchowitch.com/blog/fr/</link>
    <atom:link href="https://nelsonboruchowitch.com/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Power Platform, d&#233;veloppement web et petites obsessions techniques. Chaque parution, article complet.</description>
    <language>fr-fr</language>
    <copyright>&#169; 2026 Nelson Boruchowitch. Tous droits r&#233;serv&#233;s.</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>Blog de Nelson Boruchowitch</title>
      <link>https://nelsonboruchowitch.com/blog/fr/</link>
    </image>
    <item>
      <title>SVG dans PowerApps : Et l&#39;accessibilit&#233; dans tout &#231;a ?</title>
      <link>https://nelsonboruchowitch.com/blog/fr/prefers-reduced-motion-power-apps/</link>
      <guid isPermaLink="true">https://nelsonboruchowitch.com/blog/fr/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>Accessibilit&#233;</category>
      <category>WCAG</category>
      <description>Le SVG est devenu un r&#233;flexe pour dynamiser nos Canvas Apps. Sauf que certains utilisateurs ont demand&#233; &#224; leur syst&#232;me d&#39;arr&#234;ter le mouvement, et que Power Fx ne le sait pas. Le CSS embarqu&#233; dans le SVG, si.</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR</strong> : L'usage du SVG est devenu un réflexe pour dynamiser nos Canvas Apps. Sauf qu'une partie de vos utilisateurs a demandé à son système d'arrêter les animations, et que Power Fx n'a aucun moyen de le savoir. Le CSS embarqué dans le SVG, si : <code>@media (prefers-reduced-motion: reduce)</code> est bel et bien évalué à l'intérieur d'une Canvas App, <strong>à chaque ouverture de session</strong>. Voici le pattern prêt à coller, le banc d'essai qui le prouve, la raison précise pour laquelle cette lecture est un instantané et non un flux, et l'architecture en cascade qui en fait un pattern réellement utilisable en production.</p>
</blockquote>
<hr />
<div class="partie" id="partie-1" data-toc="Le problème, et la réponse">
<p class="partie__num">Partie 1 sur 5 · 4 min de lecture</p>

<h2 class="partie__titre">Le problème et la réponse</h2>
<p class="partie__lead">Le constat et le code qui le règle.</p>
</div>
<h2 id="une-tendance-et-un-angle-mort">1. Une tendance et un angle mort</h2>
<p>Les SVG sont pratiques, légers, nets à toutes les tailles, colorables avec Power Fx et au vu de la pléthore de ressources disponibles en ligne, nous avons de quoi faire.</p>
<p>Mais une tendance est progressivement apparue : le SVG <em>animé</em>. Un spinner qui tourne, un halo qui pulse sur la date du jour, une icône qui se dessine à l'ouverture de l'écran. Ça donne immédiatement un côté vivant à une app, pour un coût dérisoire de quelques lignes.</p>
<p>Sauf que pour une partie de vos utilisateurs, ce mouvement n'est pas une touche sympa mais une gêne physique. Troubles vestibulaires, sensibilité migraineuse, certains troubles de l'attention : ces personnes ont déjà dit à leur système d'exploitation qu'elles ne voulaient plus d'animations dans les paramètres de Windows et votre app ne les écoute pas.</p>
<p>Ce n'est pas une question de confort, c'est écrit dans les référentiels :</p>
<ul>
<li>
<p><strong>WCAG 2.2.2 : Mettre en pause, arrêter, masquer (niveau A)</strong> : tout contenu en mouvement qui démarre automatiquement et dure plus de 5 secondes doit pouvoir être mis en pause.</p>
</li>
<li>
<p><strong>WCAG 2.3.3 : Animation par interaction (niveau AAA)</strong> : les animations non essentielles déclenchées par une interaction doivent pouvoir être désactivées.</p>
</li>
</ul>
<p>Quand on parle d'accessibilité dans une Canvas App, on parle en général d'<code>AccessibleLabel</code>, de <code>TabIndex</code> (ordre de tabulation), de contraste et de <code>Live</code> regions. Tout ça est documenté, tout ça passe dans l'Accessibility Checker. Le mouvement, lui, n'est nulle part. Il n'existe pas davantage dans Power Fx : il y a <code>Host.OSType</code>, <code>Host.BrowserUserAgent</code>, <code>App.Width</code>... et rien du tout sur les préférences d'accessibilité du système. Aucun <code>Host.PrefersReducedMotion</code>.</p>
<p>Concrètement : votre Timer qui fait glisser un panneau, votre <code>Transition</code> sur un bouton, votre badge qui clignote, ils tournent, quoi que l'utilisateur ait demandé à son OS. Et vous n'avez, en apparence, aucun moyen de le savoir.</p>
<p>Bonne nouvelle : vous n'avez pas besoin de <code>Host.PrefersReducedMotion</code>. La préférence est déjà là, à l'intérieur même de vos SVG. Il suffit de trois lignes de CSS.</p>
<h2 data-toc="2. La réponse en trois lignes de CSS" id="la-reponse-tient-en-trois-lignes-de-css">2. La réponse tient en trois lignes de CSS</h2>
<p>Prenons un cas réel. Dans mon composant <code>cmpCalendar</code>, la date du jour porte un halo qui pulse doucement. Voici la version qui écoute le système :</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>Par défaut : un halo qui respire. Sous <code>reduce</code> : un anneau net, immobile, légèrement plus épais pour rester tout aussi visible. <strong>L'information &quot;c'est aujourd'hui&quot; est intacte dans les deux cas.</strong> Aucun <code>If()</code> dans la formule, aucune variable, aucune logique Power Fx : c'est le CSS qui arbitre.</p>
<div class="rm-demo" data-rm-demo role="group" aria-label="Le halo dans ses deux états">
<p class="rm-demo__lead" id="rm-verdict">Lecture de votre préférence système...</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">votre rendu</span></p>
<span class="rm-demo__stage"><span class="rm-demo__num">18</span><img class="rm-demo__svg" id="rm-img-a" alt="Halo bleu autour de la date : il grandit, s'estompe, puis repart."></span>
<p class="rm-demo__cap">Le halo respire. <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">votre rendu</span></p>
<span class="rm-demo__stage"><span class="rm-demo__num">18</span><img class="rm-demo__svg" id="rm-img-b" alt="Anneau bleu net et immobile autour de la date."></span>
<p class="rm-demo__cap">Anneau net, immobile. <code>stroke-width:2.5</code></p>
</div>
</div>
<p class="rm-demo__foot"><button type="button" id="rm-play" hidden>Lancer l'animation tout de même</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?"Votre système répond <code>reduce</code> : c'est la vignette marquée « votre rendu » que vous obtiendriez dans l'app. L'autre est figée sur sa première image, parce que cet article s'applique à lui-même sa propre règle."
:"Votre système répond <code>no-preference</code> : c'est la vignette marquée « votre rendu » que vous obtiendriez dans l'app.";
}
btn.addEventListener('click',function(){ia.src=uri(ANIM);btn.hidden=true;});
if(mq.addEventListener)mq.addEventListener('change',paint);
paint();
})();
</script>

<p>Il manque une ligne pour que ce soit fiable, et une seule. Dans <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>Cette variable n'apparaît pas dans le code ci-dessus, mais elle doit y être. J'explique en section 8 pourquoi elle n'est pas un artefact de debug mais <strong>le mécanisme de lecture lui-même</strong>.</p>
<p>C'est tout. Pas de connecteur, pas de PCF, pas de composant. Une chaîne de caractères.</p>
<p>Maintenant, pourquoi ça marche.</p>
<div class="partie" id="partie-2">
<p class="partie__num">Partie 2 sur 5 · 7 min de lecture</p>

<h2 class="partie__titre">Pourquoi ça marche</h2>
<p class="partie__lead">Cette partie explique par quelle porte le CSS entre dans une Canvas App, et pourquoi la voie qui semble évidente, la balise <code>&lt;style&gt;</code> du contrôle Texte HTML, n'en est pas une.</p>
</div>
<h2 data-toc="3. L'angle mort technique" id="langle-mort-technique-il-y-a-deja-un-moteur-css-dans-votre-canvas-app">3. L'angle mort technique : il y a déjà un moteur CSS dans votre Canvas App</h2>
<p>Une Canvas App, c'est du web. Ça tourne dans Edge, dans Chrome, dans Teams (WebView2), dans iOS (WKWebView). Il y a donc, quelque part sous vos contrôles, un moteur de rendu complet avec un moteur CSS complet.</p>
<p>Le problème, c'est qu'on n'y a pas accès... sauf par une porte : <strong>une image peut être un data URI, et un data URI peut contenir du SVG, et un SVG peut contenir une balise <code>&lt;style&gt;</code>.</strong></p>
<p>C'est le point clé. Un SVG affiché <em>en tant qu'image</em> est rendu dans ce que les navigateurs appellent le mode &quot;statique sécurisé&quot; :</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Ce qui est <strong>désactivé</strong> dans l'image</th>
<th>Ce qui reste <strong>actif</strong> dans l'image</th>
</tr>
</thead>
<tbody>
<tr>
<td>JavaScript, <code>&lt;script&gt;</code></td>
<td>Le CSS complet : sélecteurs, classes, cascade</td>
</tr>
<tr>
<td>Ressources externes (fetch, polices distantes, <code>&lt;image href&gt;</code> distant)</td>
<td><code>@keyframes</code> et les animations CSS</td>
</tr>
<tr>
<td>Interactivité, événements, liens : un <code>&lt;a&gt;</code> placé dans le SVG n'est pas cliquable</td>
<td><code>@media</code>, y compris les media features de préférence utilisateur</td>
</tr>
<tr>
<td>Toute remontée d'information vers la page hôte</td>
<td>Les transformations, opacités, filtres</td>
</tr>
</tbody>
</table></div>
<p>Autrement dit : c'est une sandbox parfaitement étanche... qui a quand même le droit de poser des questions au système d'exploitation.</p>
<p>Et c'est exactement la &quot;faille&quot; dont on a besoin. Parce que les media features dites <em>de préférence utilisateur</em> (<code>prefers-reduced-motion</code>, <code>prefers-color-scheme</code>, <code>forced-colors</code>, <code>prefers-contrast</code>) ne dépendent pas du document hôte : elles sont fournies par l'agent utilisateur lui-même, qui les tient du système. Une image SVG isolée y a donc accès au même titre qu'une page complète.</p>
<p>Restait à le vérifier. Parce que &quot;ça devrait marcher d'après la spec&quot; et &quot;ça marche dans PowerApps sur le poste d'un utilisateur&quot; sont deux affirmations très différentes. C'est l'objet de la section 7.</p>
<h2 data-toc="4. Pourquoi pas une balise style" id="pourquoi-pas-une-balise-style-dans-le-controle-texte-html">4. Pourquoi pas une balise <code>&lt;style&gt;</code> dans le contrôle Texte HTML ?</h2>
<p>Cette question est logique puisque PowerApps possède un contrôle qui accepte du HTML brut et celui-ci accepte la balise <code>&lt;style&gt;</code>. Dès lors, pourquoi passer par un data URI dans un contrôle Image ?</p>
<p>La réponse tient en deux temps : <strong>la balise <code>&lt;style&gt;</code> ne survit pas, et pourtant ce contrôle peut porter une media query.</strong></p>
<h3 id="ce-qui-ne-marche-pas-la-feuille-de-style-du-controle">4.1. Ce qui ne marche pas : la feuille de style du contrôle</h3>
<p>Le contrôle Texte HTML passe son contenu dans un assainisseur, qui retire la balise <code>&lt;style&gt;</code>. Ce qui reste exploitable est le style en ligne, <code>style=&quot;...&quot;</code>.</p>
<p>Encore faut-il être précis sur ce que &quot;retire&quot; veut dire, parce que la formulation courante est en dessous de la réalité. Je l'ai mesuré en app publiée, avec le canari de la section 7.2 : j'ai collé dans le contrôle une feuille contenant un sélecteur de classe ordinaire et un <code>@media (min-width:1px)</code>, vrai partout où du CSS est évalué. <strong>Aucun des deux ne s'allume.</strong> Il ne s'agit donc ni de fonctions avancées désactivées, ni d'un tri entre les règles : <strong>il ne reste aucune feuille de style.</strong></p>
<p>Autrement dit, ceci n'est pas un moyen d'ajouter une media query au contrôle :</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>Cette balise ne parvient jamais au moteur de rendu.</p>
<h3 id="et-le-style-en-ligne">4.2. Et le style en ligne ?</h3>
<p>On peut vouloir contourner l'obstacle en posant le CSS directement sur les éléments :</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>Ça règle la question du style, pas la nôtre. Un attribut <code>style</code> ne contient qu'une liste de déclarations <code>propriété: valeur</code> séparées par des point-virgules. Une media query, elle, est une <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>Cette syntaxe n'a aucune place dans un attribut. Même avec un assainisseur totalement permissif, même si Microsoft laissait passer les balises <code>&lt;style&gt;</code> demain matin, un style en ligne resterait incapable de porter un <code>@media</code>. <strong>Ce n'est pas une restriction de PowerApps, c'est une propriété de CSS.</strong></p>
<p>Nous constatons donc deux obstacles de natures très différentes : le premier vient de la plateforme et pourrait évoluer un jour, le second vient du langage et n'évoluera pas.</p>
<p>À ce stade, on conclurait volontiers que le contrôle Texte HTML est incapable d'héberger une media query. <strong>Ce serait aller trop vite.</strong></p>
<h3 id="ce-qui-marche-la-feuille-de-style-voyage-dans-limage">4.3. Ce qui marche : la feuille de style voyage dans l'image</h3>
<p>Le déclic vient d'un détail qu'on oublie : ce contrôle ne reçoit pas que du texte. Il reçoit du HTML, et le HTML comprend la balise <code>&lt;img&gt;</code>. Or la source d'une image peut être un data URI, et un data URI peut contenir un document SVG complet, avec sa propre feuille de style. Le bloc ci-dessous <strong>n'est pas une formule à coller</strong> : c'est le document que votre formule produira, montré déplié pour qu'on y voie la feuille de style. C'est le halo de la section 2 ; la formule qui le fabrique est donnée en fin de section.</p>
<div class="code-block"><div class="code-wrapper" data-lang="SVG · résultat"><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>Emboîté dans le contrôle, ce document devient la source d'une image. Il y tient alors sur une seule ligne, encodé. Voici la balise obtenue, abrégée ici pour la lisibilité :</p>
<div class="code-block"><div class="code-wrapper" data-lang="HTML · résultat"><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>Les chevrons du SVG y sont encodés en <code>%3C</code> et <code>%3E</code>, les apostrophes en <code>%27</code> : la chaîne ne contient plus aucun caractère capable de refermer l'attribut par accident. Vous n'aurez jamais à écrire cette chaîne vous-même : c'est <code>EncodeUrl</code> qui la produit, dans la formule donnée en fin de section.</p>
<p><strong>C'est là que tout bascule.</strong> Le <code>&lt;style&gt;</code> n'est plus une balise du HTML soumis à l'assainisseur. Il se trouve à l'intérieur du document SVG, lui-même à l'intérieur de la source de l'image.</p>
<p>L'assainisseur ne lit que le balisage extérieur. Il voit une balise <code>&lt;img&gt;</code> dont la source est un data URI, il la laisse passer, et il n'ouvre jamais ce qu'elle contient. Le SVG arrive donc au moteur de rendu avec son document, sa feuille de style et sa media query intacts.</p>
<p>La règle générale, qui vous servira au-delà de ce sujet : <strong>le balisage vit, sa feuille de style non.</strong></p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Dans le contrôle Texte HTML</th>
<th>Media query ?</th>
<th>Pourquoi</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&lt;style&gt; ... &lt;/style&gt;</code></td>
<td>Non</td>
<td>la feuille de style est retirée par l'assainisseur</td>
</tr>
<tr>
<td><code>style=&quot;...&quot;</code></td>
<td>Non</td>
<td>la grammaire CSS interdit une at-rule dans un attribut</td>
</tr>
<tr>
<td><code>&lt;img src=&quot;data:image/svg+xml,...&quot;&gt;</code></td>
<td><strong>Oui</strong></td>
<td>le SVG embarque son propre document, donc sa propre feuille de style</td>
</tr>
</tbody>
</table></div>
<p>Ce qui change complètement la façon de voir le problème. <strong>Le point décisif n'est pas le contrôle Image, c'est le data URI.</strong> Le contrôle Image n'est que le chemin le plus court pour s'en servir.</p>
<p>Rien n'empêche donc de faire cohabiter les deux dans le même contrôle. Voici le montage complet, à coller dans la propriété <code>HtmlText</code> :</p>
<div class="code-block"><div class="code-wrapper" data-lang="Power Fx"><pre><code class="lang-powerfx"><span class="hl-com">// Propriété HtmlText d'un contrôle Texte HTML</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;Votre texte, vos liens, votre mise en forme.&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>Le HTML extérieur porte alors ce qui appartient vraiment au HTML : du texte, de la mise en forme, des liens. Le SVG porte le rendu graphique et la media query. Une précision s'impose cependant sur l'interactivité, parce qu'elle se paie. Le contenu affiché est bel et bien vivant : mesuré en app publiée, un lien ouvre une fenêtre, une case à cocher se coche, un bloc <code>&lt;details&gt;</code> se déplie. Mais il est muet. Rien de tout cela n'atteint Power Fx, qui ne saura jamais que la case est cochée. <strong>Le contenu vit, mais il ne parle pas.</strong></p>
<p>Le seul pont vers l'application est l'<code>OnSelect</code> du contrôle. Il existe et il se déclenche, mais c'est l'événement du <strong>contrôle</strong>, pas celui de son contenu : il est unique et global, et rien dans Power Fx ne dit <em>où</em> le clic a eu lieu. Autant de zones réactives que l'on veut, autant de contrôles à poser. Dans un composant de graphe, le montage utile reste donc le même : <strong>le tracé et son comportement visuel vivent dans l'image, et l'interactivité que l'image ne sait pas fournir vient de Power Apps, posée autour d'elle ou par-dessus.</strong></p>
<p>Et ce n'est pas une hypothèse. La section 7 construit un banc d'essai qui éprouve chaque maillon : le canari montre que le CSS du SVG est bien évalué, puis les sondes <code>prefers-color-scheme</code> et <code>prefers-reduced-motion</code> montrent que les media queries de préférence utilisateur répondent elles aussi. Ce banc est écrit pour la propriété <code>Image</code>, le chemin le plus court ; c'est le même SVG que j'ai posé dans le <code>src</code> d'une <code>&lt;img&gt;</code> pour éprouver le contrôle Texte HTML, et la <a href="https://nelsonboruchowitch.com/blog/fr/prefers-reduced-motion-power-apps/#le-verrou-au-decodage">section 9</a> en rend compte.</p>
<div class="partie" id="partie-3" data-toc="Le faire proprement">
<p class="partie__num">Partie 3 sur 5 · 5 min de lecture</p>

<h2 class="partie__titre">Le faire proprement en production</h2>
<p class="partie__lead">Le mécanisme est acquis. Reste à s'en servir sans dégrader l'expérience : ce qu'il faut mettre à la place du mouvement, et comment laisser malgré tout le dernier mot à l'utilisateur.</p>
</div>
<h2 id="reduire-nest-pas-supprimer">5. Réduire n'est pas supprimer</h2>
<p>L'erreur classique est de traiter <code>prefers-reduced-motion: reduce</code> comme un interrupteur général : <code>animation: none</code> partout, et on n'en parle plus. C'est un contresens. Une animation porte souvent de l'information, &quot;ça charge&quot;, &quot;c'est ici que ça vient de changer&quot;, &quot;cet élément est actif&quot;.</p>
<p>Si vous la supprimez sans rien mettre à la place, vous ne rendez pas l'app plus accessible : vous en retirez une couche d'information à un public qui en a déjà besoin.</p>
<p>La bonne règle : <strong>remplacer le mouvement par un équivalent statique qui porte la même information.</strong> Un halo qui pulse devient un anneau plein. Un slide devient un fondu (les changements d'opacité et de couleur sont bien mieux tolérés que les grands déplacements). Une durée longue devient une durée courte.</p>
<p>C'est ce que fait le halo de la section 2, avec son <code>stroke-width</code> qui passe de 2 à 2,5.</p>
<h3 id="le-cas-limite-le-chargeur">5.1. Le cas limite : le chargeur</h3>
<p>Même principe, autre usage :</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>Attention ici : un spinner immobile ne communique plus &quot;en cours&quot;. En complément de ce SVG, c'est un vrai texte de statut à côté, dans un Label PowerApps, avec <code>Live: Assertive</code>, qui annonce &quot;Chargement...&quot; puis &quot;Terminé&quot;. C'est de toute façon ce qu'il fallait faire pour les lecteurs d'écran. Le mode réduit ne fait que rendre l'omission visible.</p>
<h2 id="le-pattern-a-trois-modes">6. Le pattern à trois modes</h2>
<p>Les deux exemples ci-dessus sont bons pour illustrer, mais ils laissent une objection légitime sans réponse : <em>et si l'utilisateur veut décider pour cette application précise ?</em> Peut-être qu'il a coupé les animations partout dans Windows mais qu'il tolère les vôtres. Peut-être l'inverse : Windows ne dit rien, mais votre app en fait trop à son goût.</p>
<p>Voici la forme que je recommande en production. Une variable Power Fx à trois valeurs arbitre, et le CSS ne décide que dans un seul des trois cas.</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>Trois modes, trois comportements :</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th><code>gblMotionMode</code></th>
<th>Classe</th>
<th>Comportement</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&quot;system&quot;</code> ou vide</td>
<td><code>sys</code></td>
<td>le CSS décide, via la media query</td>
</tr>
<tr>
<td><code>&quot;reduce&quot;</code></td>
<td><code>red</code></td>
<td>anneau immobile, quoi qu'en dise Windows</td>
</tr>
<tr>
<td><code>&quot;full&quot;</code></td>
<td><code>ful</code></td>
<td>animation, quoi qu'en dise Windows</td>
</tr>
</tbody>
</table></div>
<p>Quatre points de conception méritent quelques explications, parce qu'ils sont contre-intuitifs :</p>
<p><strong>L'état de base est statique.</strong> <code>.halo</code> seul ne porte aucune animation. Celle-ci est ajoutée par <code>.ful</code>, ou par <code>.sys</code> sous media query. C'est l'inverse de ce qu'on écrit spontanément (déclarer l'animation, puis l'annuler avec <code>animation: none</code> dans un bloc <code>reduce</code>), et c'est l'inverse de ce que fait le halo de la section 2. La version inversée est plus sûre : si le CSS ne s'évalue pas, si la classe n'arrive pas, si <code>gblMotionMode</code> est vide, le résultat est l'anneau immobile. <strong>Le comportement sûr devient le comportement par défaut</strong>, tandis que l'animation n'est activée que si elle est explicitement demandée. Pour illustrer un principe, on veut voir l'annulation ; en production, on préfère qu'un échec reste silencieux et sans conséquence.</p>
<p><strong>Le <code>Switch</code> a un cas par défaut.</strong> Sa dernière valeur, <code>&quot;sys&quot;</code>, s'applique si <code>gblMotionMode</code> est vide, ce qui est l'état d'un utilisateur qui n'a jamais rien réglé. Le comportement par défaut de l'application est donc &quot;suivre le système&quot;. C'est le bon défaut, et il faut le choisir consciemment : un <code>If()</code> booléen écrit à la va-vite renverrait <code>Blank()</code> sur la branche <code>else</code> et vous donnerait exactement l'inverse de ce que vous vouliez.</p>
<p><strong>Le sélecteur descendant fonctionne</strong> parce que la racine <code>&lt;svg&gt;</code> est bien un ancêtre du <code>&lt;circle&gt;</code>. C'est un point qui prête souvent à confusion, mais le fonctionnement est ici tout à fait classique.</p>
<p><strong><code>reduce</code> ne veut pas dire &quot;rien&quot;.</strong> Notez le <code>stroke-width</code> : 2 en mode animé, 2,5 en mode immobile. C'est la règle de la section 5, appliquée.</p>
<p>Et côté Power Fx :</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> Utilisateur <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> ModeAnimation<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>Un simple Radio ou Dropdown à trois options dans votre écran de préférences, un <code>Patch</code> vers votre liste, et vous donnez le contrôle à l'utilisateur sans jamais lui imposer de configurer quoi que ce soit.</p>
<div class="rm-modes" data-rm-modes role="group" aria-label="Les trois modes, à essayer">
<p class="rm-modes__lead">Choisissez un mode : l'aperçu se recompose exactement comme le ferait <code>gblMotionMode</code> dans votre 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> ou vide</span>
<span class="rm-modes__aide">Classe <code>sys</code> : le CSS décide, via la media query. C'est le défaut.</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">Classe <code>red</code> : anneau immobile, quoi qu'en dise Windows.</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">Classe <code>ful</code> : animation, quoi qu'en dise Windows.</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="Aperçu du halo dans le mode choisi."></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?"Votre système répond <code>reduce</code> : l'anneau reste immobile, sans que vous ayez rien réglé."
:"Votre système répond <code>no-preference</code> : le halo respire.")
:(m==='red'?"Immobile, même si votre système n'a rien demandé."
:"Animé, même si votre système demande <code>reduce</code>. C'est un choix explicite de l'utilisateur.");
}
d.addEventListener('change',paint);
if(mq.addEventListener)mq.addEventListener('change',paint);
paint();
})();
</script>

<div class="partie" id="partie-4">
<p class="partie__num">Partie 4 sur 5 · 12 min de lecture</p>

<h2 class="partie__titre">La preuve</h2>
<p class="partie__lead">Tout ce qui précède repose sur une affirmation qu'il fallait vérifier plutôt que croire. C'est la partie la plus longue, et la plus facile à sauter : si vous me croyez sur parole, passez directement à la partie 5. Si vous voulez au contraire le banc d'essai à coller chez vous, c'est ici.</p>
</div>
<h2 data-toc="7. Le banc d'essai" id="mais-est-ce-que-ca-marche-vraiment-le-banc-dessai">7. &quot;Mais est-ce que ça marche vraiment ?&quot; : le banc d'essai</h2>
<p>Tout ce qui précède repose sur une affirmation : la media query est évaluée à l'intérieur d'une image SVG affichée par un contrôle PowerApps. Voici comment je l'ai vérifié, et comment vous pouvez le vérifier sur vos propres hôtes en trente secondes.</p>
<p>La formule complète, à coller dans la propriété <code>Image</code> d'un contrôle Image :</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. moteur CSS : inactif&lt;/text&gt;"</span> <span class="hl-op">&amp;</span>
    <span class="hl-str">"&lt;text class='a1' x='20' y='130'&gt;1. moteur CSS : actif&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 : non evalue&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 : non evalue&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>Et dans le <code>OnStart</code> de l'app (ou le <code>OnVisible</code> de l'écran, ou le <code>OnSelect</code> d'un bouton &quot;Rafraîchir&quot;) :</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>Un horodatage plutôt qu'un <code>GUID()</code> ici, est délibéré : le banc <em>affiche</em> cette valeur en bas de l'image, et lire l'heure du rendu évite de conclure sur un écran périmé. En production, où elle est invisible, <code>GUID()</code> fera mieux, la section 8 explique pourquoi. Quant au préfixe <code>[$-en-US]</code>, il n'est pas décoratif non plus : la section 10 lui est consacrée.</p>
<p>Le code a l'air d'un empilement de classes CSS un peu étrange mais en réalité chaque bloc répond à une question de méthode précise.</p>
<h3 id="le-principe-trois-etats-dont-un-etat-je-ne-sais-pas">7.1. Le principe : trois états, dont un état &quot;je ne sais pas&quot;</h3>
<p>Toute l'astuce tient dans un choix de conception : <strong>chaque sonde possède un état par défaut visible, qui signifie &quot;non évalué&quot;.</strong></p>
<p>Prenons la sonde 3 :</p>
<ul>
<li>
<p><code>.c0</code>, &quot;non évalué&quot; : <code>opacity: 1</code> par défaut, donc <strong>visible</strong>.</p>
</li>
<li>
<p><code>.c1</code>, &quot;no-preference&quot; : <code>opacity: 0</code> par défaut, révélée uniquement par la media query correspondante.</p>
</li>
<li>
<p><code>.c2</code>, &quot;reduce&quot; : idem.</p>
</li>
</ul>
<p>Les trois textes sont superposés exactement au même endroit (<code>x='20' y='220'</code>). Un seul est visible à la fois. <strong>Le texte affiché <em>est</em> le résultat.</strong></p>
<p>Pourquoi ce troisième état ? Parce que sans lui, on ne peut pas distinguer deux situations radicalement différentes :</p>
<ol>
<li>
<p>la media query est supportée et retourne <code>false</code> ;</p>
</li>
<li>
<p>la media query n'est pas supportée du tout, ou le CSS ne s'exécute pas.</p>
</li>
</ol>
<p>Dans les deux cas, l'écran n'afficherait rien de spécial. C'est un <strong>contrôle négatif</strong> : sans lui, l'expérience ne prouve rien. Avec lui, un &quot;non evalue&quot; qui reste affiché en rouge est une information exploitable, pas un silence ambigu.</p>
<p>C'est le même réflexe qu'un test unitaire qui doit d'abord échouer pour qu'on sache qu'il teste vraiment quelque chose.</p>
<h3 id="la-sonde-1-le-canari">7.2. La sonde 1 : le canari</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>Celle-ci ne teste aucune préférence. <code>min-width: 1px</code> est vraie dans tous les cas où le CSS est évalué (le SVG fait 600 px de large : pour cette condition, le navigateur se base sur la largeur du SVG lui-même, pas sur celle de l'écran.).</p>
<p>C'est un <strong>canari</strong>. Si &quot;moteur CSS : actif&quot; s'affiche en vert, on sait que :</p>
<ul>
<li>
<p>le data URI a été correctement décodé ;</p>
</li>
<li>
<p>la balise <code>&lt;style&gt;</code> a été parsée ;</p>
</li>
<li>
<p>les sélecteurs de classe fonctionnent ;</p>
</li>
<li>
<p>les media queries sont évaluées.</p>
</li>
</ul>
<p>Si ce premier voyant reste rouge, inutile d'interpréter les deux suivants : le problème est en amont, dans l'encodage ou dans le rendu. Cela évite des heures de fausses pistes du type &quot;prefers-reduced-motion ne marche pas dans PowerApps&quot; alors qu'en réalité il manquait juste un <code>EncodeUrl</code>.</p>
<p>Point important pour interpréter les résultats : <strong>ce canari ne prouve rien sur les préférences utilisateur.</strong> Il reste vert même si le moteur refuse de résoudre <code>prefers-reduced-motion</code> à partir des préférences système. C'est bien pour ça qu'il faut les trois sondes, et pas seulement celle qui nous intéresse.</p>
<h3 id="les-sondes-2-et-3-les-vraies-questions">7.3. Les sondes 2 et 3 : les vraies questions</h3>
<p>Elles suivent exactement le même moule, avec deux valeurs positives chacune. Notez que pour <code>prefers-reduced-motion</code>, on interroge explicitement <strong>les deux</strong> valeurs, <code>no-preference</code> <strong>et</strong> <code>reduce</code>, plutôt que d'écrire seulement <code>@media (prefers-reduced-motion: reduce)</code>. Encore une fois : on veut pouvoir distinguer &quot;le système répond no-preference&quot; de &quot;personne ne répond&quot;.</p>
<p><strong>Une précision de rigueur sur la sonde 2 :</strong> <code>no-preference</code> n'est plus une valeur de <code>prefers-color-scheme</code>, aucun navigateur ne l'ayant implémentée, celle-ci a été retirée de la spécification. La valeur <code>light</code> couvre à la fois le cas où l'utilisateur préfère explicitement le thème clair et celui où aucune préférence active n'est exprimée. La sonde <code>.b1</code> ne permet donc pas de distinguer ces deux situations : elle s'affiche dans les deux cas.</p>
<h3 id="le-carre-rouge-la-demonstration-en-plus-de-la-mesure">7.4. Le carré rouge : la démonstration en plus de la mesure</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>Ce petit carré fait des allers-retours sur 480 px. Sous <code>prefers-reduced-motion: reduce</code>, il devient bleu et s'immobilise.</p>
<p>Ce n'est pas de la décoration. Le texte prouve que <strong>la media query est évaluée</strong> ; le carré prouve que <strong>l'animation CSS s'exécute réellement dans ce contexte</strong> et qu'elle obéit à la règle. C'est ce qui fait passer le résultat de &quot;intéressant&quot; à &quot;exploitable&quot; : il ne suffit pas que le navigateur sache répondre à la question, il faut aussi que le moteur d'animation tourne dans un SVG rendu en tant qu'image.</p>
<p>Sur un banc d'essai, cette écriture est la bonne : elle rend l'annulation visible. Dans du code de production, on l'inverse : c'est le pattern de la section 6.</p>
<h3 id="le-resultat">7.5. Le résultat</h3>
<p>Sur mon poste (Windows 11, PowerApps dans Chrome), les trois sondes répondent, et le carré s'immobilise quand je coupe les effets d'animation dans Windows <strong>et que je relance la lecture</strong>. Ce dernier point n'est pas un détail : sans relancer, rien ne bouge, et c'est tout l'objet des sections 8 et 9.</p>
<p><strong><code>prefers-reduced-motion</code> fonctionne dans 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">Votre navigateur ne sait pas lire cette vidéo.
<a href="https://assets.nelsonboruchowitch.com/media/powerappsreduce.mp4">La télécharger (MP4, 690 Ko)</a>.</p>
</video>
<figcaption class="rm-video__legende" id="rm-video-desc">
Le banc d'essai dans une application publiée. Les trois sondes répondent, puis les effets
d'animation sont coupés dans Windows : au rechargement de la valeur, la troisième sonde
bascule sur <code>reduce</code> et le carré s'immobilise en bleu.
</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-nest-pas-un-artefact-de-test">8. <code>gblBust</code> n'est pas un artefact de test</h2>
<p>C'est, de très loin, le point qui m'a coûté le plus de temps. Il est en réalité <strong>le mécanisme de lecture lui-même</strong>.</p>
<p>Le data URI <strong>est</strong> l'URL de l'image. Si la chaîne produite est identique octet pour octet, le navigateur renvoit l'image déjà décodée depuis son cache mémoire. Alors que c'est au moment du décodage et à ce moment seulement que les media queries du SVG sont évaluées. Chaîne inchangée, pas de décodage. Pas de décodage, pas d'évaluation.</p>
<p>Vous changez le réglage dans Windows, vous revenez dans l'app, vous cliquez partout... et rien ne bouge. Non pas parce que ça ne marche pas, mais parce que <strong>rien n'a été rechargé</strong>.</p>
<p>L'attribut <code>data-build</code> est totalement inerte : il n'a aucun effet sur le rendu. Sa seule fonction est de faire varier la chaîne, donc l'URL, donc de forcer un décodage neuf et une nouvelle évaluation des media queries. Et la ligne <code>build : ...</code> affichée en bas de notre banc d'essai n'est pas cosmétique : elle vous dit <strong>quelle version vous êtes réellement en train de regarder</strong>.</p>
<p>Conséquence directe, et c'est elle qui rend le pattern utilisable en 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>Cette ligne garantit qu'à chaque ouverture de session, la chaîne du data URI est recréée, puis décodée et évaluée en fonction de l'état actuel du système. Ainsi, un utilisateur qui a désactivé les effets d'animation dans Windows retrouve automatiquement ce réglage dans votre app, sans aucune configuration supplémentaire.</p>
<p>C'est la ligne annoncée en section 2 et le halo doit donc porter lui aussi un <code>data-build='&quot; &amp; gblBust &amp; &quot;'</code> sur sa balise <code>&lt;svg&gt;</code>, exactement comme le fait le pattern à trois modes de la section 6.</p>
<p>En production, la valeur est invisible et et sert uniquement à casser le cache : <code>GUID()</code> garantit une valeur unique, sans dépendre de la locale ni du format de date. Lors des tests, l'horodatage était pertinent puisqu'il était affiché.</p>
<p>Un mot sur la portée réelle de cette ligne. Sur un rechargement complet de page, un nouveau document est créé et les images sont décodées à neuf de toute façon. <code>gblBust</code> est là pour couvrir les autres cas, qui sont nombreux dans une application : navigation interne, réaffichage d'un écran, retour depuis un autre onglet. C'est une assurance à une ligne.</p>
<h2 id="le-verrou-au-decodage">9. Le verrou au décodage</h2>
<p>Voici le comportement qui surprend tout le monde, moi le premier, et qui a une explication précise.</p>
<p>Ouvrez l'app pour que le banc d'essai affiche <code>no-preference</code>, le carré bouge. Sans fermer l'app, allez dans les réglages Windows, désactivez les effets d'animation, revenez. Le carré bouge toujours.</p>
<p>Ce n'est pas que la media query a échoué mais c'est que <strong>le tampon d'image n'a pas été redessiné</strong>. Chromium a un bug référencé pour ça, l'issue 1093736 : les images SVG ne sont pas redessinées lorsque les media queries changent. Le rendu fonctionne la plupart du temps, mais ne réagit pas si la préférence bascule en cours de route. Le contournement documenté est exactement celui de la section 8 : forcer le redessin.</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">// Bouton "Relire la préférence système", 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>Il faut donc être précis sur ce que ce pattern est et n'est pas. <strong>Ce n'est pas un flux, c'est un instantané.</strong> Le SVG lit la préférence au moment où il est décodé et conserve ce résultat jusqu'au prochain décodage. Rien ne le notifie d'un changement.</p>
<p>Pour les plus curieux ! Il faut même aller plus loin, parce que de tout ceci, peut découler cette question : si le SVG, lui, connaît la préférence, ne peut-on pas la lui faire dire ? Du coup, j'ai essayé encore et encore. Le contrôle Image possède une propriété que le moteur alimente lui-même, <code>OriginalWidth</code>, disponible avec <code>CalculateOriginalDimensions</code>. L'idée était de fabriquer un SVG dont la largeur dépendrait de la media query : la valeur serait devenue un booléen déguisé, lisible depuis Power Fx.</p>
<p>Le canari m'a répondu clairement non. Avec une media query vraie partout, <code>@media (min-width:1px){svg{width:120px}}</code>, la largeur rapportée n'a jamais bougé : 240 quand le document déclare <code>width='240'</code>, et 300 quand il ne déclare rien, c'est-à-dire la taille de repli du navigateur. Jamais 120, et le relevé est rigoureusement identique que les effets d'animation de Windows soient activés ou coupés. Le CSS est pourtant bel et bien évalué, la section 7 le prouve ; il ne participe simplement pas au calcul de la taille rapportée à l'hôte. <strong>Le moteur renvoie ce que le document déclare, jamais ce qu'il a dessiné.</strong></p>
<p>Un détail de cette tentative mérite d'être gardé, parce qu'il peut piéger au-delà de ce sujet : entre deux décodages, <code>OriginalWidth</code> passe par <code>0</code>. Une lecture naïve du type <code>largeur &lt; 200</code> renvoie donc <code>true</code> pendant cet intervalle, alors que l'image n'a pas encore retrouvé ses dimensions. On peut alors prendre cet état transitoire pour une préférence. Pour éviter cela, il faut d'abord vérifier que la largeur est bien supérieure à 0 : <code>largeur &gt; 0 And largeur &lt; 200</code>.</p>
<p>La conclusion est donc plus nette que le simple constat d'instantané : <strong>le SVG obéit à la préférence, et l'application ne le saura jamais.</strong> Aucune valeur ne franchit la frontière dans ce sens, ni par le contrôle Image, ni par le contrôle Texte HTML, dont la propriété <code>HtmlText</code> se contente de rendre la chaîne que votre propre formule a produite. Pour obtenir un vrai booléen dans Power Fx, il faut un composant de code (PCF) et son <code>window.matchMedia</code>.</p>
<p>Ce point se vérifie également avec la méthode présentée en section 4. La démonstration tient sur un seul écran, dans l'app publiée : deux <code>&lt;img&gt;</code> affichent le <strong>même</strong> SVG côte à côte, l'un avec <code>gblBust</code> dans sa chaîne et l'autre sans. On désactive les animations dans Windows sans fermer l'app, on déclenche le bust, puis on observe le résultat.</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Voie</th>
<th>Chemin emprunté par le CSS</th>
<th>Canari</th>
<th>Verdict lu</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>balise <code>&lt;style&gt;</code> écrite dans le contrôle Texte HTML</td>
<td><strong>inactif</strong></td>
<td>rien, la feuille de style a été retirée</td>
</tr>
<tr>
<td>B</td>
<td><code>&lt;img&gt;</code> data URI dans le contrôle Texte HTML, avec <code>gblBust</code></td>
<td>actif</td>
<td><code>reduce</code></td>
</tr>
<tr>
<td>C</td>
<td>le même <code>&lt;img&gt;</code>, sans <code>gblBust</code></td>
<td>actif</td>
<td><code>no-preference</code>, figé</td>
</tr>
<tr>
<td>D</td>
<td>contrôle Image, avec <code>gblBust</code></td>
<td>actif</td>
<td><code>reduce</code></td>
</tr>
</tbody>
</table></div>
<p>Le détail déterminant est le canari : il est actif dans les deux cas. L'image figée n'est donc pas en panne. Elle a correctement réagi au moment où elle a été évaluée, et personne ne l'a réévaluée depuis. <strong>&quot;Non évalué&quot; et &quot;évalué il y a dix minutes&quot; peuvent avoir le même aspect à l'écran, mais correspondent à deux situations très différentes et n'appellent pas le même correctif.</strong> C'est précisément ce que permet de distinguer le troisième état de la section 7.1, et pourquoi il faut le prévoir dès le premier jet.</p>
<p>Reste alors la question essentielle : <strong>est-ce grave ?</strong></p>
<p>Pour cette media feature précisément, non. Et la raison est davantage sociologique que technique.</p>
<p>Le mode sombre, les gens peuvent le basculer plusieurs fois par jour, souvent automatiquement au coucher du soleil. <code>prefers-reduced-motion</code>, non. Une personne sensible au mouvement coche la case une fois, à la configuration de son poste, et n'y revient pas. <strong>La valeur lue à l'ouverture de la session <em>est</em> la valeur, durablement.</strong> La capacité de mise à jour à chaud qui manque au pattern est une capacité dont personne n'a besoin ici.</p>
<p>C'est d'ailleurs exactement le modèle de lecture de tout le reste dans PowerApps. <code>Language()</code>, <code>Host.OSType</code>, <code>User()</code>, tout ce que vous initialisez dans <code>OnStart</code> : ce sont des valeurs lues au démarrage, et personne n'attend qu'elles se propagent à chaud. Personne ne reproche à PowerApps de ne pas réagir instantanément à un changement de langue d'affichage de Windows : on relance l'app. Le pattern n'est pas une exception bancale dans la plateforme, il est cohérent avec elle.</p>
<p>La nuance à garder en tête, et elle est importante si vous étendez la technique : cet argument est solide pour <code>prefers-reduced-motion</code> et pour <code>forced-colors</code>, qui sont des réglages qu'on pose une fois. Je considère qu'il est nettement plus faible pour <code>prefers-color-scheme</code>, que beaucoup d'utilisateurs font basculer automatiquement selon l'heure. Sur celui-là, l'instantané au démarrage vous donnera régulièrement une valeur périmée en cours de journée.</p>
<div class="partie" id="partie-5">
<p class="partie__num">Partie 5 sur 5 · 8 min de lecture</p>

<h2 class="partie__titre">Les détails qui comptent</h2>
<p class="partie__lead">Ce qui reste, et qui n'entrait dans aucune des quatre premières : les pièges Power Fx du data URI, la chaîne complète du réglage Windows au pixel, l'architecture à trois niveaux qui rend l'ensemble conforme, et les autres préférences système que la même technique va chercher.</p>
</div>
<h2 data-toc="10. Les pièges Power Fx" id="les-pieges-power-fx-du-data-uri">10. Les pièges Power Fx du data URI</h2>
<p>Quelques détails qui expliquent la forme du code :</p>
<p><strong><code>EncodeUrl</code> n'est pas optionnel.</strong> Un data URI est une URL. Le caractère <code>#</code> y a une signification particulière : il ouvre un fragment. Sans encodage, <code>#d13438</code> tronque purement et simplement votre SVG au premier code couleur. <code>EncodeUrl</code> le transforme en <code>%23d13438</code>, et fait de même pour <code>&lt;</code>, <code>&gt;</code>, les espaces, etc.</p>
<p><strong>Guillemets simples à l'intérieur du SVG.</strong> En Power Fx, une chaîne est délimitée par <code>&quot;</code> et un guillemet littéral s'écrit <code>&quot;&quot;</code>. Écrire du XML avec des attributs en <code>&quot;&quot;</code> double la longueur du code et le rend illisible. Le XML accepte parfaitement <code>'</code> comme délimiteur d'attribut : <code>width='600'</code>. On garde ainsi une formule lisible. <strong>Attention toutefois :</strong> si vous retouchez le code et que vous doublez une apostrophe par réflexe, le SVG devient invalide et le contrôle affiche une image cassée sans le moindre message d'erreur.</p>
<p><strong><code>;utf8</code> plutôt que <code>;base64</code>.</strong> Power Fx n'a pas d'encodeur base64 pratique pour ce cas, et surtout : en clair, la formule reste lisible, facile à comparer et à déboguer. Vous pouvez copier la sortie dans un fichier <code>.svg</code> et l'ouvrir directement.</p>
<p><strong>Les accents.</strong> <code>EncodeUrl</code> gère correctement l'UTF-8, mais j'ai pris l'habitude d'écrire mes libellés de test sans accents. Lorsque l'objectif est d'isoler <em>une</em> variable, mieux vaut éviter qu'un éventuel problème d'encodage vienne compliquer le diagnostic. Dans un composant de production, remettez vos accents et testez-les.</p>
<p><strong>La longueur.</strong> Power Fx peut gérer des formules très longues, mais une formule inutilement volumineuse reste difficile à maintenir. Le data URI, lui, peut rapidement devenir lourd, surtout s'il est répété pour chaque ligne d'une galerie. Minifiez donc le CSS (pas d'espaces superflus, sélecteurs courts) et évitez de générer un data URI inutilement volumineux par ligne de galerie.</p>
<p><strong>Les chaînes de format sont dans la langue de l'utilisateur.</strong> <code>Text()</code> interprète ses jetons à l'exécution, dans la locale de celui qui ouvre l'app, pas dans la vôtre à l'écriture. D'où le préfixe <code>[$-en-US]</code> du banc d'essai : sans lui, un utilisateur néerlandophone ou anglophone ne lit pas les mêmes jetons que vous. En environnement multilingue, c'est loin d'être théorique.</p>
<h2 data-toc="11. La chaîne complète" id="la-chaine-complete-du-reglage-windows-au-pixel">11. La chaîne complète : du réglage Windows au pixel</h2>
<p>C'est ce qui rend le résultat réellement utile. Il ne s'agit pas d'un gadget CSS : c'est un signal qui traverse cinq couches.</p>
<div class="code-block"><div class="code-wrapper"><pre><code>Réglage d'accessibilité de l'OS
        ↓
Paramètre système (Windows : SPI_GETCLIENTAREAANIMATION)
        ↓
Moteur du navigateur / WebView (Edge, Chrome, WebView2, WKWebView)
        ↓
Media feature CSS : prefers-reduced-motion
        ↓
Décodage du data URI          ← le point de lecture, et le seul
        ↓
Votre SVG dans un contrôle Image ou Texte HTML</code></pre></div></div>
<p>Là où l'utilisateur actionne le levier, selon la plateforme :</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Plateforme</th>
<th>Chemin</th>
</tr>
</thead>
<tbody>
<tr>
<td>Windows 11</td>
<td>Paramètres → Accessibilité → Effets visuels → <strong>Effets d'animation</strong> (désactiver)</td>
</tr>
<tr>
<td>Windows 10</td>
<td>Paramètres → Options d'ergonomie → Affichage → <strong>Afficher les animations dans Windows</strong></td>
</tr>
<tr>
<td>macOS</td>
<td>Réglages Système → Accessibilité → Affichage → <strong>Réduire les animations</strong></td>
</tr>
<tr>
<td>iOS / iPadOS</td>
<td>Réglages → Accessibilité → Mouvement → <strong>Réduire les animations</strong></td>
</tr>
<tr>
<td>Android</td>
<td>Paramètres → Accessibilité → <strong>Supprimer les animations</strong></td>
</tr>
</tbody>
</table></div>
<p>Attention au piège du réglage voisin : sous Windows, ce n'est ni les effets de transparence, ni les options de performance du panneau système avancé. C'est bien <strong>Effets d'animation</strong>, dans Accessibilité.</p>
<p>Et pour tester en développement sans toucher à votre poste : dans les DevTools (F12) → menu <code>⋮</code> → More tools → <strong>Rendering</strong> → <em>Emulate CSS media feature prefers-reduced-motion</em>. Vous basculez la valeur, vous relancez votre <code>gblBust</code>, vous observez. C'est aussi le moyen le plus rapide de vérifier qu'un problème vient bien du SVG et pas de Windows : sous émulation, l'OS est court-circuité.</p>
<p>Le point important à faire passer dans une réunion avec un métier ou un service accessibilité : <strong>l'utilisateur n'a rien à configurer dans votre application.</strong> Elle respecte, silencieusement, une préférence qu'il a déjà exprimée ailleurs, une fois pour toutes.</p>
<h2 id="la-cascade-de-preferences">12. La cascade de préférences</h2>
<p>Il reste une chose que ce pattern ne peut pas faire, et elle est structurelle : <strong>le canal est à sens unique.</strong> Le SVG peut <em>voir</em> la préférence, mais il n'a aucun moyen de la renvoyer à Power Fx. Pas de JavaScript, pas de sortie, pas d'événement. Vous n'écrirez jamais <code>If(gblReducedMotion, ...)</code> à partir de ce mécanisme. C'est pour ça que le pattern consiste à <strong>faire descendre l'animation dans le SVG</strong> plutôt qu'à essayer de faire remonter un booléen.</p>
<p>Seul un composant PCF ouvre ce canal de retour, avec un écouteur <code>matchMedia</code> qui notifie Power Fx en temps réel. C'est la solution complète, et si les code components sont autorisés dans votre environnement, c'est celle qu'il faut viser à terme.</p>
<p>Conséquence immédiate : tout ce que vous animez avec un contrôle Timer et des <code>Set()</code> sur des propriétés <code>X</code> / <code>Y</code> / <code>Height</code> est du Power Fx pur. Le CSS n'a aucune prise dessus. Ces animations continueront à tourner.</p>
<p>Et c'est précisément là que les deux approches se répondent. Regardez ce que chacune voit :</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th></th>
<th>Ce que ça pilote</th>
<th>Ce que ça ne voit pas</th>
</tr>
</thead>
<tbody>
<tr>
<td>Héritage système, via CSS</td>
<td>les animations déclarées dans le SVG</td>
<td>les Timer Power Fx</td>
</tr>
<tr>
<td>Surcharge applicative, via Power Fx</td>
<td>les Timer Power Fx</td>
<td>les animations dans le SVG</td>
</tr>
</tbody>
</table></div>
<p>Les deux couvrent <strong>exactement</strong> l'angle mort l'une de l'autre. Ce n'est pas une solution de contournement empilée sur une astuce, c'est une répartition propre des responsabilités. D'où l'architecture que je recommande, en trois niveaux qui se surchargent dans cet ordre :</p>
<p><strong>Niveau 0 : le défaut de la plateforme.</strong> Animation active. C'est ce que vous avez aujourd'hui, sans rien faire.</p>
<p><strong>Niveau 1 : l'héritage système.</strong> Lu à l'ouverture de la session, appliqué silencieusement, sans aucune configuration demandée à l'utilisateur. Il a déjà exprimé sa préférence une fois, au niveau de son système. C'est exactement le modèle que WCAG encourage, et c'est ce qui fait la différence entre une application qui <em>permet</em> d'être accessible et une application qui l'est par défaut.</p>
<p><strong>Niveau 2 : la surcharge applicative.</strong> Un réglage explicite dans votre écran de préférences, enregistré et prioritaire sur le niveau 1. C'est lui qui couvre les animations Power Fx, et c'est lui qui satisfait formellement WCAG 2.2.2, dont le critère demande un mécanisme de mise en pause et non le respect d'une media feature. <code>prefers-reduced-motion</code> n'apparaît que dans les techniques suffisantes, pas dans le critère lui-même.</p>
<p>La formulation qui résume l'ensemble, et qui est le vrai résultat de cet article : <strong>le niveau 1 sans le niveau 2 est incomplet, le niveau 2 sans le niveau 1 est médiocre.</strong> L'un donne le bon défaut sans rien demander, l'autre donne le contrôle et la conformité. Ensemble, ils font nettement mieux que chacun séparément.</p>
<p>Deux réserves pour finir, qui n'entament pas l'architecture mais qu'il faut poser :</p>
<p><strong>Le rendu dépend de l'hôte.</strong> Voir la section 7.5. Testez sur les plateformes de vos utilisateurs réels avant de vous appuyer dessus pour une fonctionnalité importante.</p>
<p><strong>Ce n'est pas un mécanisme documenté par Microsoft.</strong> C'est une propriété émergente de l'empilement (Canvas App → navigateur → image SVG → CSS). Ça ne fait pas partie du contrat de la plateforme. C'est d'ailleurs une raison de plus de maintenir le niveau 2 : lui est entièrement sous votre contrôle. Le niveau 1 <em>améliore</em> l'expérience, le niveau 2 la <em>garantit</em>.</p>
<h2 data-toc="13. Les autres media features" id="la-suite-les-autres-media-features">13. La suite : les autres media features</h2>
<p>Le banc d'essai est extensible tel quel. Il suffit d'ajouter des sondes sur le même modèle &quot;état 0 = non évalué&quot; :</p>
<ul>
<li>
<p><strong><code>forced-colors: active</code></strong> : le mode Contraste élevé de Windows. Probablement la plus utile des quatre en contexte entreprise : quand il est actif, vos couleurs codées en dur dans un SVG sont ignorées ou remplacées, et vos icônes peuvent devenir illisibles. Une sonde dédiée vaut son pesant d'or. Et comme <code>prefers-reduced-motion</code>, c'est un réglage qu'on pose une fois : l'instantané au démarrage suffit.</p>
</li>
<li>
<p><strong><code>prefers-contrast: more | less</code></strong> : la demande d'un contraste renforcé, indépendamment du mode couleur.</p>
</li>
<li>
<p><strong><code>prefers-color-scheme: dark</code></strong> : pour aligner vos illustrations SVG sur le thème du système plutôt que sur un thème applicatif figé. Celle-ci est le mauvais élève du lot : elle bascule en cours de session, et vous retrouverez ici le problème décrit en section 9. Prévoyez un déclencheur de relecture, ou tenez-vous-en au thème applicatif.</p>
</li>
</ul>
<p>Chacune peut, une fois validée, être utilisée exactement comme <code>prefers-reduced-motion</code> : directement dans le SVG, sans un seul <code>If()</code> en Power Fx.</p>
<hr />
<h2 id="ce-quil-faut-retenir">Ce qu'il faut retenir</h2>
<ol>
<li>
<p>Vos SVG animés tournent sans jamais demander l'avis de l'utilisateur. Power Fx ne peut pas lire les préférences d'accessibilité du système. Le CSS embarqué dans un SVG, si.</p>
</li>
<li>
<p>Une media query n'entre dans une Canvas App que par un data URI SVG. Ni un attribut <code>style=</code> en ligne ni la balise <code>&lt;style&gt;</code> du contrôle Texte HTML ne peuvent en porter une : l'assainisseur retire la seconde, et une at-rule ne rentre pas dans le premier. Le data URI, lui, passe aussi bien par le contrôle Image que par un <code>&lt;img&gt;</code> posé dans le contrôle Texte HTML.</p>
</li>
<li>
<p>La lecture se fait <strong>au décodage</strong>, donc à l'ouverture de la session. C'est un instantané, pas un flux. Pour une préférence qu'on règle une fois dans sa vie, c'est suffisant.</p>
</li>
<li>
<p><code>gblBust</code> n'est pas un truc de debug : c'est la ligne qui déclenche la lecture.</p>
</li>
<li>
<p>Réduire n'est pas supprimer. Remplacez le mouvement par un équivalent statique qui porte la même information.</p>
</li>
<li>
<p>En production, inversez la logique : état de base statique, animation en opt-in. Le comportement sûr doit être le comportement par défaut.</p>
</li>
<li>
<p>Héritage système pour le bon défaut, surcharge applicative pour le contrôle et la conformité. Les deux, pas l'un ou l'autre.</p>
</li>
<li>
<p>Le pattern à trois états, dont un état &quot;non évalué&quot; visible par défaut, transforme une image en instrument de mesure avec son propre contrôle négatif, utile bien au-delà de ce sujet.</p>
</li>
</ol>
]]></content:encoded>
    </item>
  </channel>
</rss>
