130 Widgets

Building tools. Learning to build tools. Learning to build learning tools.

7. Making It Better — Fragility, Automation & What’s Next

Understanding what can break, building self-healing mechanisms, and exploring where this reverse-engineering approach leads.

Arch Design

Current Fragility Points

Theme injection into a closed-source application is inherently fragile. We’re overriding a system designed to not be overridden. Every Teams update is a potential breaking change. Let’s be honest about what can break and how likely each failure mode is.

Fragility Point Likelihood Impact Detection
data-tid selectors renamed or removed Low–Medium Specific UI regions lose theming (e.g., title bar reverts to default) Visual — affected elements are obvious
FluentProvider token names changed Low Entire theme stops working (wrong colors everywhere) Immediately visible
Calling screen structure rearchitected Medium Video feed bug returns or calling UI unthemed Requires joining a call to verify
Settings UI navigation paths changed Medium Auto mode-switching fails; themes still work, just can’t auto-switch Console warning from switchTeamsMode()
Griffel resolves tokens at build time Low Token overrides stop affecting Griffel-styled elements Subtle — individual components lose theming
CDP debug port disabled by Microsoft Low Total failure — no connection possible Immediate — injector can’t connect

The most important insight: token names are the most stable part of this system. Fluent UI v9’s design tokens are essentially a public API — documented, versioned, and used across multiple Microsoft products. Renaming --colorNeutralBackground1 would break every Fluent UI consumer, not just our theme injector. This makes the core token override approach relatively durable.

The fragile parts are the hardcoded overrides — the targeted selectors for specific elements (Section 5) and the calling screen workarounds (Section 6). These depend on implementation details that Teams can change without notice.

Self-Healing Mechanisms

The observation and analysis tools from Sections 1 and 2 aren’t just for initial development — they’re the maintenance strategy. When a Teams update breaks something, the tools can diagnose what changed:

explore-dom.js: Selector Health Check

After a Teams update, run explore-dom.js to re-inventory all data-tid and data-testid elements. Compare the output against the previous run. Did data-tid="title-bar" disappear? Was it renamed to data-tid="titlebar"? The JSON diff tells you exactly what changed and which CSS selectors need updating.

// Quick diff between two explore-dom.js outputs
const fs = require("fs");
const prev = JSON.parse(fs.readFileSync("output/dom-exploration-old.json"));
const curr = JSON.parse(fs.readFileSync("output/dom-exploration.json"));

const prevTids = new Set(prev.tids.map(t => t.tid));
const currTids = new Set(curr.tids.map(t => t.tid));

const removed = [...prevTids].filter(t => !currTids.has(t));
const added   = [...currTids].filter(t => !prevTids.has(t));

console.log("Removed:", removed);  // selectors that broke
console.log("Added:",   added);    // potential replacements

observe.js: Token Structure Monitoring

Similarly, observe.js can re-extract all tokens from a new Teams version. If token names changed, the diff between runs reveals it immediately. If new tokens appeared, they might need to be added to the buildTokenMap() function. If tokens disappeared, their mappings can be removed.

observe-calling.js: Calling Screen Validation

The calling observation script can be re-run after updates to verify that the FluentProvider structure hasn’t changed. If the data-tid="calling-screen-background" element moves, gains a new identifier, or uses a different positioning strategy, the diff will show it.

Build a Habit, Not a Tool

The most effective maintenance strategy is simply running the observation pipeline after each major Teams update. It takes ~30 seconds and produces a complete picture of what changed. The alternative — waiting until something visually breaks and then debugging — is far more time-consuming.

Token-Only vs. Hardcoded Themes

The dimmed themes from Section 3 are the most resilient themes in the system. They only override tokens on .fui-FluentProvider — no hardcoded selectors, no data-tid targeting, no wildcard propagation. If a token name changes, every dimmed theme breaks identically and the fix is a single rename in the token map.

Ported VS Code themes (Section 4) are moderately resilient. They use tokens for the vast majority of the UI but add hardcoded overrides for elements where Griffel bypasses tokens. If a data-tid changes, the specific UI region loses theming but the rest still works.

This creates a clear strategy for minimizing fragility:

  1. Maximize token coverage. Every element themed via tokens is one fewer hardcoded override to maintain.
  2. Minimize hardcoded selectors. Only add them when tokens provably don’t work for a specific element.
  3. Use the most stable selector available. Prefer data-tid over Fluent component classes, and never use Griffel hashes.
  4. Isolate hardcoded overrides. Keep them in a clearly marked section of the CSS so they’re easy to audit after updates.

Making Theme Porting Easier

The 25-role palette system means adding a new theme is a matter of defining ~30 colors. But even that could be simpler:

Auto-Extraction Beyond VS Code

port-all.js currently reads VS Code theme JSON files. The same approach could be extended to other theme formats:

From Palette to Theme in One Command

Currently, defining a manual palette requires writing a JavaScript object in port-theme.js. A JSON-based palette format would let users define themes without touching the codebase:

{
  "label": "My Custom Theme",
  "bg": {
    "deep": "#0d1117",
    "base": "#161b22",
    "elevated": "#1c2128",
    "subtle": "#21262d",
    "wash": "#30363d"
  },
  "fg": {
    "emphasis": "#f0f6fc",
    "primary": "#c9d1d9",
    "muted": "#8b949e",
    "subtle": "#6e7681",
    "faint": "#484f58"
  },
  "brand": {
    "primary": "#1f6feb",
    "fg": "#58a6ff"
  }
}

The porter engine would read this JSON, fill in missing roles with sensible defaults (e.g., generate brand.hover from brand.primary + lightening), and produce a complete theme CSS. Adding a theme becomes editing a JSON file and running a command.

Making Themes Less Fragile

CSS Cascade Layers

Modern CSS supports @layer declarations that control specificity ordering. If Griffel’s atomic classes were in a lower cascade layer and our overrides were in a higher one, we wouldn’t need !important everywhere:

/* Hypothetical — if Teams supported cascade layers */
@layer griffel, theme-override;

@layer theme-override {
  .fui-FluentProvider {
    --colorNeutralBackground1: #21252b;
    /* No !important needed — layer wins */
  }
}

Unfortunately, we can’t control the layer that Griffel’s classes are injected into. But if Fluent UI v9 ever adds cascade layer support, it would dramatically simplify third-party theming.

CSS Containment

CSS containment (contain: style) limits the scope of CSS custom properties, preventing them from leaking out of a subtree. This is the opposite of what we want — we need our token overrides to leak into every subtree. But understanding containment helps diagnose cases where tokens mysteriously don’t apply: if a component sets contain: style, our inherited custom properties won’t reach its children.

A Theme Health Check Tool

A dedicated health check tool could automate post-update validation:

// Concept: theme health check
async function checkThemeHealth(Runtime) {
  const checks = [
    // Verify key selectors still exist
    { selector: '[data-tid="title-bar"]', name: 'Title bar' },
    { selector: '[data-tid="app-bar-wrapper"]', name: 'App bar' },
    { selector: '[data-tid="message-pane-body"]', name: 'Message pane' },
    { selector: '[data-tid="ckeditor"]', name: 'Compose box' },
  ];

  for (const check of checks) {
    const exists = await Runtime.evaluate({
      expression: `!!document.querySelector('${check.selector}')`,
      returnByValue: true,
    });
    console.log(`  ${exists.result.value ? '✓' : '✗'} ${check.name}`);
  }

  // Verify token overrides are taking effect
  const bg = await Runtime.evaluate({
    expression: `getComputedStyle(
      document.querySelector('.fui-FluentProvider')
    ).getPropertyValue('--colorNeutralBackground1').trim()`,
    returnByValue: true,
  });
  console.log(`  Token check: --colorNeutralBackground1 = ${bg.result.value}`);
}

Future Directions

Theme Sharing Format

A standard JSON palette format (as described above) would enable a theme marketplace — a repository of community-created palettes that anyone can download, preview, and inject. The porter engine converts palette JSON to CSS automatically; sharing themes becomes sharing a 30-line JSON file.

Live Theme Preview

The injector already supports hot-reloading: edit the CSS file and it re-injects within 300ms. Extending this to a “preview mode” — inject temporarily, let the user evaluate, and revert if they decline — would make theme selection more interactive.

// Concept: temporary theme preview
async function previewTheme(themePath, durationMs = 10000) {
  const css = fs.readFileSync(themePath, "utf-8");
  const expr = buildInjectionExpression(css);
  await injectIntoAll(expr);

  console.log(`Previewing ${path.basename(themePath)}...`);
  console.log(`Press Enter to keep, or wait ${durationMs/1000}s to revert.`);

  const kept = await Promise.race([
    new Promise(r => process.stdin.once("data", () => r(true))),
    new Promise(r => setTimeout(() => r(false), durationMs)),
  ]);

  if (!kept) {
    // Revert to previous theme
    await injectIntoAll(previousExpression);
    console.log("Reverted.");
  }
}

Outlook Theming

The v1 system already supported Outlook (new Outlook for Windows uses the same WebView2 + Fluent UI architecture as Teams). Porting v2’s improved engine to Outlook would extend the same approach to a second Microsoft 365 app. The token names are largely shared between Teams and Outlook, so the palette system would work with minimal adaptation.

Browser Extension

Teams for web (teams.microsoft.com) runs in a regular browser tab. A browser extension could inject the same CSS without needing CDP. The extension would use the standard chrome.tabs.insertCSS API or a content script. The same theme CSS files work unchanged — they don’t depend on the injection mechanism.

Contributing Themes

With the palette format standardized and auto-extraction from VS Code working, the barrier to contributing a theme is low: install a VS Code theme, run port-all.js, review the output, commit. Community contributions could rapidly expand the theme library beyond what a single developer can manually curate.

Lessons Learned

Observe Before You Hack

The single most valuable principle in this project: understand the system before you override it. v1 guessed at token values and fought the design system. v2 observed every token, analyzed the patterns, and worked with the structure. The difference in results — and in long-term maintainability — is dramatic.

This applies beyond theming. Any time you’re extending, modifying, or working around a system you don’t control, instrument first. Read every computed style, log every event, map every element. The upfront investment in observation saves multiples in debugging and guessing.

CDP Is More Powerful Than Most People Realize

The Chrome DevTools Protocol gives you everything the Chrome DevTools UI does and more: DOM inspection, JavaScript evaluation, network interception, screenshots, emulation, runtime profiling. Most developers interact with CDP indirectly through DevTools or Puppeteer. Working with the protocol directly revealed capabilities that aren’t exposed in the standard DevTools UI — like evaluating expressions in specific execution contexts or intercepting specific CDP domains.

Fluent UI v9’s Token System Is Well-Designed

Despite fighting it throughout this project, the Fluent UI token system deserves praise. The layered background hierarchy, the consistent foreground ramp, the interaction state pattern — these are thoughtfully designed. The 25-role palette system only works because Fluent UI’s token structure is coherent and systematic. A chaotic token system would require chaotic overrides.

The WebView2/Native Boundary Is the Hardest Part

Every challenge in this project that couldn’t be solved elegantly was at the boundary between the web layer (where CDP works) and the native layer (where it doesn’t). Theme switching required UI automation because the setting lives in native storage. Video feed transparency required understanding how the WebView2 compositor layers web content and native windows. The CDP debug port itself is a configuration that the native shell controls.

If you’re working with WebView2 or Electron apps, expect the native boundary to be the source of your most difficult bugs.

Congratulations

You’ve worked through the entire Theme Injector tutorial — from connecting to Teams via CDP, through observing and analyzing the Fluent UI token system, to generating themes from a 25-color palette and handling edge cases like video feeds and native theme switching.

Here’s what you’ve learned:

The techniques here extend far beyond Teams theming. The observation → analysis → systematic override pipeline applies to any application you want to customize from the outside. The palette → token mapping pattern applies to any design system with structured tokens. And CDP’s capabilities go far beyond what was covered here — network interception, performance profiling, accessibility auditing, and more.

Go Build Something

The best way to internalize these concepts is to apply them. Pick an Electron or WebView2 application you use daily. Connect to it via CDP. Observe its design tokens. Analyze the patterns. Build something. The worst that can happen is you learn how the application works — and that knowledge is valuable regardless of whether your theme or extension ships.