130 Widgets

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

1. Observation — Extracting the Color System

Don’t guess what Teams uses — measure it. Extracting every design token, computed color, and DOM landmark from a running instance.

CDP Design

The Observation-First Philosophy

It’s tempting to open the Fluent UI documentation, look up token names, and start guessing values. Don’t. Documentation describes intent; the running app reveals reality. Token names get renamed between Fluent UI versions. Some documented tokens aren’t used. Some undocumented ones are critical. The only reliable source of truth is the live DOM of a running Teams instance.

The observation pipeline connects to Teams via CDP and programmatically extracts everything we need: every CSS custom property, every computed color, and the actual DOM selectors used by the UI. This data becomes the foundation for analysis (Section 2) and theme generation (Section 3).

Extracting Fluent UI Design Tokens

All of Teams’ design tokens live as CSS custom properties on the .fui-FluentProvider element. The observation script reads every one of them by iterating the element’s computed style:

// From observe.js — extracting all CSS custom properties
const tokenResult = await Runtime.evaluate({
  expression: `
    (() => {
      const provider = document.querySelector('.fui-FluentProvider');
      if (!provider) return JSON.stringify({
        error: 'No .fui-FluentProvider element found'
      });

      const cs = getComputedStyle(provider);
      const tokens = {};

      for (let i = 0; i < cs.length; i++) {
        const prop = cs[i];
        if (prop.startsWith('--')) {
          tokens[prop] = cs.getPropertyValue(prop).trim();
        }
      }

      return JSON.stringify(tokens);
    })()
  `,
  returnByValue: true,
});

const tokens = JSON.parse(tokenResult.result.value);
CDP Concept

Runtime.evaluate() executes a JavaScript expression in the page’s main context and returns the result. The returnByValue: true option serializes the return value so we get the actual data rather than a remote object reference. Since we’re returning a JSON string, we parse it on the Node.js side.

The result: 498 CSS custom properties. These aren’t just colors — the token set includes spacing, typography, borders, shadows, corner radii, and motion timing. Here’s a sampling of what comes back:

{
  "--colorNeutralBackground1": "#ffffff",
  "--colorNeutralBackground2": "#fafafa",
  "--colorNeutralBackground3": "#f5f5f5",
  "--colorNeutralForeground1": "#242424",
  "--colorNeutralForeground2": "#424242",
  "--colorBrandBackground": "#5b5fc7",
  "--colorBrandForeground1": "#5b5fc7",
  "--colorPaletteRedBackground3": "#d13438",
  "--colorStatusDangerForeground1": "#b10e1c",
  "--borderRadiusMedium": "4px",
  "--fontSizeBase300": "14px",
  "--fontWeightSemibold": "600",
  "--durationFast": "150ms",
  "--shadow4": "0 0 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.14)"
}

Tokens Beyond FluentProvider

The FluentProvider isn’t the only source of custom properties. Some older Teams components use :root or document.body properties. The observation script captures these separately to ensure complete coverage:

// From observe.js — reading :root and body properties
const rootResult = await Runtime.evaluate({
  expression: `
    (() => {
      const result = { root: {}, body: {} };
      const rootStyles = getComputedStyle(document.documentElement);
      const bodyStyles = getComputedStyle(document.body);

      for (let i = 0; i < rootStyles.length; i++) {
        const prop = rootStyles[i];
        if (prop.startsWith('--')) {
          result.root[prop] = rootStyles.getPropertyValue(prop).trim();
        }
      }

      for (let i = 0; i < bodyStyles.length; i++) {
        const prop = bodyStyles[i];
        if (prop.startsWith('--')) {
          result.body[prop] = bodyStyles.getPropertyValue(prop).trim();
        }
      }

      return JSON.stringify(result);
    })()
  `,
  returnByValue: true,
});

In practice, the FluentProvider tokens dominate. The :root and body properties provide a handful of additional values, mostly legacy or Teams-specific tokens that predate the Fluent v9 migration.

Exploring the DOM: Finding the Real Selectors

To sample computed colors from specific UI regions, we need CSS selectors that actually match elements in Teams. This turned out to be harder than expected.

Tip

Don’t guess selector names. The original version of the observation script used selectors like [data-tid="left-rail"] and [data-tid="chat-list"] — names that seemed logical but didn’t exist in the actual DOM. The explore-dom.js tool was built specifically to discover what selectors Teams really uses.

The explore-dom.js tool walks the DOM tree from <body> down four levels, recording every element’s tag name, classes, data-tid, data-testid, role, and aria-label attributes:

// From explore-dom.js — recursive DOM tree walker
function describeEl(el, depth, maxDepth) {
  if (depth > maxDepth) return null;
  const tag = el.tagName?.toLowerCase() || '?';
  const id = el.id ? '#' + el.id : '';
  const classes = el.className && typeof el.className === 'string'
    ? '.' + el.className.trim().split(/\s+/).slice(0, 5).join('.')
    : '';
  const role = el.getAttribute?.('role') || '';
  const dataTid = el.getAttribute?.('data-tid') || '';
  const dataTestid = el.getAttribute?.('data-testid') || '';
  const ariaLabel = el.getAttribute?.('aria-label') || '';

  // Collect all data-* attributes
  const dataAttrs = {};
  if (el.attributes) {
    for (const attr of el.attributes) {
      if (attr.name.startsWith('data-')) {
        dataAttrs[attr.name] = attr.value?.substring(0, 80) || '';
      }
    }
  }

  const info = {
    tag, selector: tag + id + classes,
    role, dataTid, dataTestid, ariaLabel,
    dataAttrs, childCount: el.children?.length || 0,
  };

  // Recurse into children
  if (depth < maxDepth && el.children) {
    const children = [];
    for (const child of el.children) {
      const desc = describeEl(child, depth + 1, maxDepth);
      if (desc) children.push(desc);
    }
    if (children.length > 0) info.children = children;
  }

  return info;
}

It also searches specifically for all elements with data-tid attributes and records their bounding rectangles. This reveals the actual naming scheme Teams uses:

Guessed Name Actual data-tid UI Element
left-rail app-layout-area--mid-nav The conversation list panel
chat-list simple-collab-dnd-rail The draggable chat tree
nav-bar app-bar-wrapper The icon rail on the far left
header entity-header The channel/chat header bar
compose ckeditor The message compose box
settings-btn more-options-header The “…” settings menu

Some elements use data-testid instead of data-tid (notably the collab rail components), and some use ARIA roles. The explorer captures all of these patterns, letting us build a comprehensive selector list.

Sampling Computed Colors from UI Regions

With correct selectors in hand, the observation script samples computed style properties from ~35 UI regions. For each region, it reads color-related CSS properties and records the actual rendered values:

// From observe.js — sampling computed colors from UI regions
const UI_REGIONS = [
  { selector: ".fui-FluentProvider", name: "FluentProvider (root)" },
  { selector: '[data-tid="app-layout-area--title-bar"]', name: "Title Bar Area" },
  { selector: '[data-tid="app-layout-area--nav"]', name: "Nav Area (App Bar)" },
  { selector: '[data-tid="app-layout-area--mid-nav"]', name: "Mid-Nav (Left Rail)" },
  { selector: '[data-testid="simple-collab-rail"]', name: "Collab Rail (Chat List)" },
  { selector: '[data-tid="app-layout-area--main"]', name: "Main Content Area" },
  { selector: '[data-tid="message-pane-body"]', name: "Message Pane Body" },
  { selector: '[data-tid="ckeditor"]', name: "Message Editor" },
  { selector: ".fui-Button", name: "Button (first)" },
  { selector: ".fui-TreeItem", name: "Tree Item (first)" },
  // ... 25 more regions ...
];

const COLOR_PROPERTIES = [
  "color", "background-color", "border-color",
  "border-top-color", "border-bottom-color",
  "box-shadow", "text-shadow", "caret-color",
  "fill", "stroke",
  // ... and more ...
];

For each region, the script queries the element, reads its computed style for every color-related property, and also captures any inline style declarations that might override the computed values. This gives us a ground truth map of what color appears where in the UI.

Stylesheet Scanning: Finding var() Usage

Beyond the FluentProvider tokens and computed colors, the observation script scans all accessible stylesheets to build a map of which CSS properties reference which var() tokens:

// From observe.js — scanning stylesheets for var() references
for (const sheet of document.styleSheets) {
  try {
    const rules = sheet.cssRules || sheet.rules;
    for (const rule of rules) {
      if (rule.style) {
        for (let i = 0; i < rule.style.length; i++) {
          const prop = rule.style[i];
          if (/color|background|border|shadow/i.test(prop)) {
            const val = rule.style.getPropertyValue(prop).trim();
            // Track var() references
            const varMatch = val.match(/var\(--([^)]+)\)/g);
            if (varMatch) {
              for (const v of varMatch) {
                const name = v.match(/var\(--(.*?)(?:,|\))/)?.[1];
                if (name) {
                  varReferences.get(name)?.add(prop)
                    || varReferences.set(name, new Set([prop]));
                }
              }
            }
          }
        }
      }
    }
  } catch {
    // Cross-origin stylesheet — skip silently
  }
}

This reveals which tokens are actually used by the stylesheets versus which tokens merely exist as properties. Some tokens are defined but never referenced in any CSS rule — useful to know when deciding what to override.

Detecting Light vs. Dark Mode

Before generating a theme, we need to know whether Teams is currently in Light or Dark mode. The observation script determines this by analyzing the luminance of the primary background token:

// From observe.js — mode detection via token luminance
const bg1 = cs.getPropertyValue('--colorNeutralBackground1').trim();
const fg1 = cs.getPropertyValue('--colorNeutralForeground1').trim();

function parseLuminance(color) {
  const temp = document.createElement('div');
  temp.style.color = color;
  document.body.appendChild(temp);
  const computed = getComputedStyle(temp).color;
  document.body.removeChild(temp);
  const match = computed.match(/\d+/g);
  if (!match) return 0.5;
  const [r, g, b] = match.map(Number);
  return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}

const bgLum = parseLuminance(bg1);
const mode = bgLum > 0.5 ? 'light' : 'dark';
Architecture Decision

Why not just read localStorage.getItem('tmp.desktopTheme')? Teams stores its native theme preference there, and it isn’t affected by injected CSS. The problem: this value can become stale. If the user changes themes via Settings and Teams doesn’t flush, the localStorage value may not reflect reality. The luminance approach is more reliable — but it has its own issue: if a theme is already injected, the luminance reflects the injected theme, not the native one. The injector solves this by temporarily removing the injected style before sampling, then re-injecting it.

Visual Verification with Screenshots

The observation pipeline includes a screenshot step. Using CDP’s Page.captureScreenshot, it captures the full viewport and measures the bounding boxes of each UI region:

// From screenshot.js — capturing the viewport
const { data: fullData } = await Page.captureScreenshot({ format: "png" });
fs.writeFileSync(
  path.join(OUTPUT_DIR, "screenshot-full.png"),
  Buffer.from(fullData, "base64")
);

The screenshot also records each region’s bounding rectangle by querying getBoundingClientRect() on the discovered selectors. This data feeds into the analysis step, where we can verify that our token-to-region mapping is correct by comparing the token values against what’s actually rendered.

Observation Outputs

The observation step produces four output files:

File Format Contents
observed-tokens.json JSON All 498 CSS custom properties, detected mode, token metadata
observed-tokens.css CSS A reference stylesheet with all tokens as a .fui-FluentProvider rule
computed-colors.json JSON Computed colors from ~35 UI regions with element metadata
observation-report.txt Text Human-readable summary: token counts, mode detection, region status

The Observation Pipeline

┌─────────────┐ │ Teams (Live) │ └──────┬──────┘ │ CDP: Runtime.evaluate() ▼ ┌──────────────────────────────────────────────────┐ │ observe.js │ │ │ │ 1. getComputedStyle(.fui-FluentProvider) │ │ → 498 CSS custom properties │ │ │ │ 2. getComputedStyle(:root) + getComputedStyle │ │ (document.body) → additional properties │ │ │ │ 3. For each UI_REGION selector: │ │ → querySelector → getComputedStyle │ │ → 15 color properties per element │ │ │ │ 4. Scan all document.styleSheets │ │ → var(--token) reference map │ │ │ │ 5. Luminance analysis of --colorNeutralBg1 │ │ → Light / Dark mode detection │ └──────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────┐ │ output/ directory │ │ │ │ observed-tokens.json│ │ observed-tokens.css │ │ computed-colors.json│ │ observation-report │ └─────────────────────┘

What We Learned

We now have a complete snapshot of Teams’ color system. Next, we’ll analyze this data to understand the structural relationships between tokens — the surface hierarchy, interaction states, brand system, and contrast requirements that define the design system.