130 Widgets

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

5. Hardcoded Overrides & Griffel Atomic Classes

Why design tokens alone can’t theme everything, and how stable DOM attributes let us target the elements that Griffel’s atomic CSS makes unreachable.

Design CDP

Why Token Overrides Aren’t Enough

After applying 400+ token overrides to the .fui-FluentProvider element, you’d expect every surface in Teams to reflect the new palette. Open Teams with an injected theme, though, and you’ll see white gaps bleeding through: the title bar, the compose box background, the chat list sidebar. These elements stubbornly ignore your tokens.

The culprit is Griffel — Microsoft’s CSS-in-JS library used by Fluent UI v9. Griffel generates atomic CSS classes: each class sets exactly one property. Instead of a semantic class like .sidebar { background-color: var(--colorNeutralBackground2); }, Griffel generates something like:

/* Griffel atomic class — sets background-color directly */
.___1qrapf3 {
  background-color: #ffffff;
}

The hardcoded #ffffff wins over any CSS custom property override on a parent element. Griffel extracts the resolved value of the design token at build time and bakes it into the atomic class. At runtime, changing the token doesn’t affect elements styled by these pre-resolved classes.

CSS Specificity: Why Griffel Wins

CSS custom properties (like --colorNeutralBackground1) on a parent element are inherited, not directly applied. When a Griffel atomic class sets background-color: #ffffff on an element, that declaration has higher specificity than an inherited custom property. The only way to override it is with a rule that targets the same element with equal or greater specificity, or uses !important.

The html/body Background Bleed

The most visible problem is the <html> and <body> elements themselves. Teams runs inside a WebView2 control, and the native window has its own background color. If the web content’s <body> has a white background, it shows through every gap between Fluent UI components — margins, padding, scroll gutters. Token overrides don’t help because <html> and <body> aren’t inside .fui-FluentProvider.

The fix is straightforward: set the html/body background directly. But as Section 6 will explain, this has to be conditional — during calls, the body must be transparent to allow video feeds through.

The Selector Fragility Problem

If Griffel generates atomic classes like .___1qrapf3 for the sidebar background, could we just target that class in our CSS override?

/* ❌ Don't do this — hash changes on every Teams update */
.___1qrapf3 {
  background-color: #21252b !important;
}

No. Griffel class names are content hashes of the style definition. Any change to the component — even a minor prop adjustment or Fluent UI version bump — produces a different hash. After the next Teams update, .___1qrapf3 becomes .___17x8g2a and your override silently breaks. The theme looks fine for two weeks, then spontaneously fails.

We need selectors that are stable across updates.

The Solution: data-tid and data-testid Attributes

The DOM exploration phase (explore-dom.js) revealed that Teams marks its major UI regions with data-tid and data-testid attributes. These exist for testing purposes — they let Teams’ own automated tests find elements regardless of class name changes. They’re exactly what we need: stable identifiers that survive updates.

// From explore-dom.js — discovering all data-tid elements
const tidResult = await Runtime.evaluate({
  expression: `
    (() => {
      const els = document.querySelectorAll('[data-tid]');
      const results = [];
      for (const el of els) {
        const rect = el.getBoundingClientRect();
        if (rect.width === 0 && rect.height === 0) continue;
        results.push({
          tid: el.getAttribute('data-tid'),
          tag: el.tagName.toLowerCase(),
          bounds: {
            x: Math.round(rect.x),
            y: Math.round(rect.y),
            w: Math.round(rect.width),
            h: Math.round(rect.height)
          },
        });
      }
      return JSON.stringify(results);
    })()
  `,
  returnByValue: true,
});

explore-dom.js: Live DOM Discovery

The explore-dom.js tool connects to Teams via CDP and does a comprehensive inventory of the live DOM. It maps the document tree to four levels deep, captures every data-tid and data-testid attribute, records bounding rectangles, ARIA labels, and roles. The output is a JSON file that serves as a blueprint for writing stable selectors.

Running explore-dom.js on a typical Teams chat view produces results like this:

data-tid elements found: title-bar 1920×40 (0,0) Title bar app-bar-wrapper 68×980 (0,40) App bar (left icons) channel-list 300×980 (68,40) Channel/chat list entity-header 1552×48 (368,40) Content header message-pane-body 1552×884 (368,88) Message area ckeditor 1552×48 (368,932) Compose box data-testid elements found: simple-collab-rail 300×980 (68,40) Chat list rail

Each of these provides a stable CSS selector. Instead of targeting the unpredictable Griffel hash classes, we use:

[data-tid="title-bar"]          { /* stable */ }
[data-tid="app-bar-wrapper"]    { /* stable */ }
[data-testid="simple-collab-rail"] { /* stable */ }
[data-tid="entity-header"]      { /* stable */ }
[data-tid="message-pane-body"]  { /* stable */ }
[data-tid="ckeditor"]           { /* stable */ }

Key Selectors Discovered

UI Region Selector Palette Role
Title bar [data-tid="title-bar"] titleBar (special)
App bar (left icon rail) [data-tid="app-bar-wrapper"] bg.base
Chat/channel list [data-testid="simple-collab-rail"] bg.base
Content header [data-tid="entity-header"] bg.elevated
Message area [data-tid="message-pane-body"] bg.elevated
Compose box [data-tid="ckeditor"] bg.elevated

buildCSS(): Generating Tokens + Targeted Overrides

The buildCSS() function in port-theme.js generates the complete theme CSS file. It produces two sections: the token map (applied to .fui-FluentProvider) and the hardcoded overrides (targeting specific elements by data-tid).

// From port-theme.js — the CSS generator
function buildCSS(palette, tokenMap, observedTokens) {
  const P = palette;
  const lines = [];

  // ── Section 1: Token overrides on FluentProvider ──
  lines.push(".fui-FluentProvider {");
  for (const [token, value] of Object.entries(tokenMap)) {
    lines.push(`  ${token}: ${value} !important;`);
  }
  lines.push("}");

  // ── Section 2: Hardcoded overrides ──

  // html/body background (conditional for calls)
  lines.push(`html:not([class*="V2"]) {`);
  lines.push(`  background-color: ${P.bg.base} !important;`);
  lines.push(`}`);

  // Title bar
  lines.push(`.fui-FluentProvider [data-tid="title-bar"] {`);
  lines.push(`  background-color: var(--titleBar) !important;`);
  lines.push(`}`);

  // App bar
  lines.push(`.fui-FluentProvider [data-tid="app-bar-wrapper"] {`);
  lines.push(`  background-color: ${P.bg.base} !important;`);
  lines.push(`}`);

  // Chat list
  lines.push(`.fui-FluentProvider [data-testid="simple-collab-rail"] {`);
  lines.push(`  background-color: ${P.bg.base} !important;`);
  lines.push(`}`);

  // ... more targeted overrides
}

The .fui-FluentProvider * Wildcard

Even with targeted selectors, some Griffel-styled elements sit deep in the component tree and resist overrides. The nuclear option is a descendant wildcard that re-propagates key token values to every child element:

/* Token propagation for Griffel atomic overrides */
.fui-FluentProvider * {
  --colorNeutralBackground1: #21252b;
  --colorNeutralBackground2: #1b1f23;
  --colorNeutralBackground3: #282c34;
}

This is a blunt instrument but effective. By setting the custom property directly on every element (not just the provider), any Griffel class that uses var(--colorNeutralBackground1) will pick up the override. The performance impact is negligible — CSS custom properties are inheritable by default, so the browser already tracks them through the tree. Setting them explicitly on * just ensures that inline-style-like Griffel declarations can’t escape.

The Specificity Arms Race

The wildcard approach works today because Griffel uses var(--tokenName) in most of its atomic classes. If a future Griffel version resolves tokens at build time (replacing var(--colorNeutralBackground1) with #ffffff directly), the wildcard trick would stop working. We’d need even more targeted overrides. This is the fundamental fragility of overriding a CSS-in-JS system from the outside.

Chat Message Styling

Chat messages are one of the most visually prominent elements. Fluent UI uses two distinct component classes: .fui-ChatMessage__body for messages from other people, and .fui-ChatMyMessage__body for your own messages. These aren’t data-tid elements — they’re Fluent UI component class names, which are more stable than Griffel hashes because they’re public API rather than generated internals.

/* Chat messages — other people */
.fui-FluentProvider .fui-ChatMessage__body {
  background-color: #2c313a !important;
}

/* Chat messages — my messages (tinted with brand color) */
.fui-FluentProvider .fui-ChatMyMessage__body {
  background-color: #2b3140 !important;
}

The distinction matters: “my message” backgrounds typically get a subtle brand tint to visually distinguish them. The palette’s myMessageBg role captures this — a dark blue-tinted variant of the base background in Atom One Dark, for example.

Counter Badges

Teams shows unread message counts as small colored badges on channel and chat entries. These use the .fui-CounterBadge component class:

/* Notification badges — custom brand color */
.fui-FluentProvider .fui-CounterBadge {
  background-color: #e5c07b !important;  /* warning.fg */
  color: #282c34 !important;             /* bg.elevated */
}

Using the warning color (gold/yellow) for badges makes them stand out against dark backgrounds without the aggressive red that Teams uses by default. This is an opinionated design choice — each theme can customize the badge palette role to whatever feels right.

Compose Box: Placeholder Text

The compose box ([data-tid="ckeditor"]) uses a CKEditor instance. Its placeholder text (“Type a new message”) is set via a ::before pseudo-element on a [data-placeholder] attribute or a .ck-placeholder class. Both need to be targeted:

/* Compose box placeholder text */
.fui-FluentProvider [data-tid="ckeditor"] [data-placeholder]::before,
.fui-FluentProvider [data-tid="ckeditor"] .ck-placeholder::before {
  color: #636d83 !important;  /* fg.subtle */
}

Without this override, the placeholder remains an unreadable near-white text on a light background — or worse, inherits a dark color from the token override that doesn’t contrast with the compose box’s background.

Selector Stability Hierarchy

Not all selectors are equally fragile. Here’s the observed stability ranking:

Selector Type Stability Example
CSS custom property names ★★★★★ --colorNeutralBackground1
Fluent component classes ★★★★ .fui-ChatMessage__body
data-tid attributes ★★★★ [data-tid="title-bar"]
data-testid attributes ★★★ [data-testid="simple-collab-rail"]
ARIA roles/labels ★★ [role="navigation"]
Griffel hash classes .___1qrapf3

The theme generator uses the highest-stability selectors available for each target element. Token overrides handle the vast majority of the UI; hardcoded selectors are the fallback for elements where Griffel bakes in resolved values.

Summary

Tokens handle the system; hardcoded overrides handle the exceptions. Together they produce a fully themed Teams UI — until you join a call. The next section covers the calling screen bug and the discovery that Teams has multiple FluentProviders with different token sets.