Skip to content

feat(react-components)!: migrate styling from Griffel to Tailwind v4 + CSS Modules (Griffel-zero) - #36513

Draft
Ray Knight (ArrayKnight) wants to merge 640 commits into
microsoft:masterfrom
ArrayKnight:main
Draft

feat(react-components)!: migrate styling from Griffel to Tailwind v4 + CSS Modules (Griffel-zero)#36513
Ray Knight (ArrayKnight) wants to merge 640 commits into
microsoft:masterfrom
ArrayKnight:main

Conversation

@ArrayKnight

@ArrayKnight Ray Knight (ArrayKnight) commented Aug 4, 2026

Copy link
Copy Markdown

DRAFT — under author review only. Not yet ready for maintainer review; will be marked ready by Ray Knight (@ArrayKnight). Do not merge before its dependency (see Merge order below).

Migrate react-components styling from Griffel to Tailwind v4 + CSS Modules

This PR replaces the runtime CSS-in-JS styling system (Griffel makeStyles/mergeClasses) across the entire non-deprecated v9 surface — every react-components package, the charts package, the test/VR/SSR/perf harness, and the stories and docs pages — with build-time CSS Modules + Tailwind v4 tokens under a cascade-layer contract (@layer fui.theme, fui.base, fui.components(.l1–l5), fui.utilities). The repo ends Griffel-zero outside deprecated/ packages: the Griffel AOT pass runs in 3 packages / 6 files, all under packages/react-components/deprecated/, and no *.styles.raw.js is emitted anywhere else. Per-stage counts are in GRIFFEL_ZERO_CLOSURE.md.

Why

  • Runtime cost out, build-time CSS in: styles ship as static, per-package dist/styles.css compiled at build time; no style computation or injection at runtime, and no Griffel AOT compilation step.
  • Measured wins — re-measured on this branch's HEAD with the repo's own metrics/capture.sh, same machine, cold, clean tree (full data and provenance: migration/griffel-to-tailwind/reports/FINAL_REPORT.md §2.0):
    • Griffel AOT: 62 packages / 277 source files transformed → 3 packages / 6 files, all under packages/react-components/deprecated/ (react-alert 1, react-infobutton 2, react-virtualizer 3)
    • Dead *.styles.raw.js shipped in every tarball: 700,829 B → 0 across non-deprecated react-components (12 files / 44,304 B remain on disk, all under deprecated/)
    • Shipped lib/ JS across react-components: −15.57% (like-for-like, 4,556,347 → 3,846,978 B; baseline restricted to HEAD's 84-package set, since react-conformance-griffel left it for deprecated/)
    • @fluentui/react-button npm tarball: −34.14% packed (124,354 → 81,896 B), −41.18% unpacked (1,050,635 → 617,951 B), 185 → 176 files
    • Honest trades, stated in the report: CSS aggregates per-package rather than deduplicating atomically, so a single-component consumer pays its package's whole stylesheet; and the VR storybook's static output is +12.95% (34,569,262 → 39,045,618 B). That last one is not a clean like-for-like and is not offered as one: the storybook also gained 24 story files, and it is a bundled artifact whose HEAD leg links the icons fork below where the baseline leg linked published icons. Neither confounder is netted out — see FINAL_REPORT.md §2.7 — because doing so honestly requires the post-revert re-capture.
    • Bundle-size and build-time figures are deliberately absent. Bundle numbers are blocked on the icons dependency below and will be published from a post-revert capture. Build time is not publishable at all: five cold identical-scope runs on one unchanged tree measured 167/163/161/158/156 s in run order — a monotone warming trend spanning 6.8% of the median — against a single-observation baseline, so no percentage in that range is defensible. Client-runtime figures are cited from their own dated reports below rather than restated here.
  • Simpler consumer contract: standard CSS with cascade layers replaces a proprietary runtime styling API.

Developer experience

Beyond the measurements, the authoring model changes in ways that matter day-to-day (expanded rationale: Less variant nesting in Tailwind with compound and stacked syntax):

  • You write CSS. No object notation, no camelCased property names, no JS-based abstraction standing between the author and the stylesheet — CSS-in-JS is an indirection layer that has to be learned, tooled, and mentally translated back to the CSS it produces. Tailwind introduces some indirection of its own (it isn't perfect), but the escape hatch is always the same: when its vocabulary doesn't cover a case, you drop to plain CSS in the same file — not to another abstraction.
  • Styles are grouped, not scattered. A component's entire appearance lives in one *.module.css, organized by slot and state, instead of being distributed across atomic utility classes. That also bounds the blast radius of change: editing a component's rule affects that component — whereas editing a shared atomic/utility class (in code or live in DevTools) changes every element on the page that happens to use it.
  • Separation of concerns. Behavior stays in .tsx, appearance in .module.css. Reviews get smaller and more focused; styling changes stop churning component logic files.
  • The DOM is parseable. Emitted class names are scoped but semantic (fuicm-<component>-<slot>-<hash>), so component structure is identifiable by reading the markup in DevTools — versus a hashed atomic class soup where nothing in the element tells you what it is.
  • States are single-sourced. Tailwind v4's compound/stacked variant syntax (@variant hover, focus-visible { … }, @variant dark:hover { … }) expresses "these states share this appearance" once, instead of duplicated blocks that silently drift apart — the source reads the way you'd describe the design out loud.
  • Overrides are predictable. Cascade layers give deterministic precedence (fui.themefui.basefui.components.l1–l5fui.utilities): a consumer override wins by layer, not by specificity games, source-order luck, or !important.
  • Theming is just a class. A theme is a CSS class containing only custom-property declarations, so scoping it is a DOM operation rather than an API call: put webDarkThemeClassName on <FluentProvider>, on a bare <aside>, or on <html> — no provider is required for a scoped theme, portals and nested providers inherit automatically, and the default (web light) resolves at :root with no wrapper at all. Custom themes are authored the same way, which means they are inspectable and overridable in DevTools like any other stylesheet, and switching themes is a className swap instead of rebuilding and re-injecting a rule set.
  • Work moves from per-render JavaScript into the per-frame cascade. Class resolution that a runtime styling engine performs inside every component render — including renders that get batched or never produce a visual change — is expressed declaratively and resolved by the browser's cascade instead: coalesced once per frame, scoped by the engine's invalidation machinery to the elements that actually changed, and executed as native code that improves with every browser release. Style recalculation still shares the main thread, so this is a claim about where and how often the work is paid, not about it becoming free — but the JavaScript savings are real and serialized with everything else the app does, which is main-thread time handed back to application code. Measurements are in reports/perf-eval.md and reports/perf-consumer-dynamic-class.md, each dated to the commit pair it traced.

Runtime-performance deep dive (state-toggle investigation)

An observed state-toggle regression was investigated to root cause across eight controlled experiments (migration/griffel-to-tailwind/reports/perf-*.md): selector policy, selector ordering, attribute-vs-class form, named groups, will-change, transition tightening, @layer structure, and value literalization each measured null. The mechanism the investigation landed on: Blink charges a fixed per-started-transition cost whenever the document's custom-property registry is non-empty — any @property rule, related to the transition or not, reproduced by injecting a single dummy registration into the unmigrated build, which slowed it equally. The migrated theme had registered a handful of focus-ring knob properties; those registrations are now removed, with the same nesting-isolation guarantee reproduced at the cascade level. Verified on this branch: across all 63 dist/styles.css artifacts under packages/, the theme package and every component package emit zero @property rules — the only two that remain are Tailwind-internal and confined to a dev-only storybook addon that no shipped package depends on. The remedy brings Button toggle cost back to Griffel parity and recovers most of Switch's; the remainder is custom-property resolution during transition-triggered recalc, the accepted cost of a fully var-driven theme.

The per-cell timings, the µs-per-transition figure and the recovery percentages live in reports/perf-mechanism-diagnostic.md, reports/perf-property-remedy.md and reports/perf-layers-vs-literals.md, each stamped with the commit pair it traced. They are cited rather than restated here because they have not been re-run against the current HEAD, which has since had theming Phase 2b remove provider rule-building entirely. The diagnostic recommends filing the underlying behavior as a Chromium optimization gap; it affects any page that registers custom properties, regardless of styling system.

Validation

Pixel identity was the non-negotiable gate throughout. Every conversion batch was captured against pre-change baselines via StoryWright and diffed at zero tolerance, with per-batch gates plus full-suite sweeps at phase boundaries. Where states lacked coverage, VR stories were added and baselined before converting (focus states, icon filled/regular swaps across hover/open/pressed/disabled, chart popovers, RTL/dark/high-contrast) — the VR story count under apps/vr-tests-react-components/src went from 137 files at the baseline commit to 161 today. Behavioral gates: full jest suites at parity, the SSR render+hydration suite, and CDP computed-style/matched-rules equivalence probes for cascade-sensitive changes.

Per-gate set and screenshot counts are deliberately not restated here: each was recorded against the tree that existed when it ran, and quoting one as a current status would misdate it. Every batch, decision, gate result and adjudication is in-repo with its own date under migration/griffel-to-tailwind/ — see DECISIONS.md, GRIFFEL_ZERO_CLOSURE.md, theming-css-native.md (the most recent full-VR sweep), the per-batch reports, and the post-campaign consistency audits.

Breaking changes (major-version)

Consumer-facing walkthrough of all of the below, with before/after for each: Concepts/Migration/Upgrading from v9 on the docsite, plus the full 467-row token map at .../FromV9/TokenRename.mdx.

  1. @fluentui/react-tailwind-theme must be installed and its stylesheet imported once per document. It carries the cascade @layer order, the token values (Web Light at :root, :host), the spacing/stroke scales and the shipped theme classes — everything the component CSS references. This is the break most likely to be missed, because skipping it does not throw: components render with collapsed spacing, no radii and no shadows. Two of the breaks in this list fail silently; this is one, the token rename (item 7) is the other.
  2. Static BEM class names removed (fui-Component/fui-Component__slot and the xClassNames slot maps). The supported styling contract is: per-slot className props; one public identity marker per component (group/fui-<component>, usable via fuiSelector() from react-utilities and as a Tailwind group target); data-* state attributes; and the documented layer system for cascade-safe overrides. This is a type change, not a deprecation: 215 *ClassNames constants typed SlotClassNames<…Slots> on master are 193 typed { root: string } here, so every non-root slot reference is a compile error. The one step the type system cannot force is `.${x.root}`fuiSelector(x.root), since string building type-checks fine.
  3. Umbrella Griffel re-exports removed from @fluentui/react-components (12 runtime symbols + 3 types) along with its @griffel/react dependency.
  4. @fluentui/react-conformance-griffel retired (moved to deprecated/, per repo precedent).
  5. v0→v9 migration style mixins retired (their static output is inlined into the migration guides).
  6. FluentProvider: Griffel renderer APIs removed. The provider no longer renders Griffel's TextDirectionProvider — direction flows via dir + CSS logical properties, so RTL now follows the DOM rather than React context. (That was an internal render detail, not a published export; it appears in no api.md at master.) The interim first-class nonce prop was itself removed by item 7 — the provider no longer creates any style element, so there is nothing to sign; cover the static .css assets with a CSP style-src source list.
  7. @fluentui/react-icons v3 (headless) required — see Merge order.
  8. Theming is CSS-native; the JS theming path is removed. All 467 token CSS variables were renamed to a single canonical kebab vocabulary aligned to Tailwind v4 namespaces (--colorNeutralBackground1--color-neutral-background-1, --spacingHorizontalM--spacing-horizontal-m, --strokeWidthThin--stroke-width-thin, …; complete old→new map committed at migration/griffel-to-tailwind/reports/token-rename-map.json and published as a docsite page with a find/replace script) — the old camelCase variables no longer exist in any shipped CSS. This one fails silently: a var() on an undefined custom property drops the declaration with no error, warning or log. The tokens object is unaffected — same JS key names, updated values — so only hand-written CSS needs rewriting. Themes ship as static CSS classes in @fluentui/react-tailwind-theme (.fui-theme-web-dark, …; web-light values are the :root defaults), applied via FluentProvider's new themeClassName prop (typed constants: webLightThemeClassName, …) or directly on any DOM node for scoped theming; portals and nested providers inherit automatically. REMOVED: FluentProvider's theme prop and runtime theme <style> tag, its nonce prop, createCSSRuleFromTheme, useFluentProviderThemeStyleTag, ThemeContext_unstable, and the umbrella/react-theme exports of the theme objects, create*Theme factories, themeToTokensObject and Theme/PartialTheme/BrandVariants types (theme objects remain in @fluentui/tokens as build-time tooling input). Custom themes are CSS classes containing only custom-property declarations; runtime value reads go through useCssVarValue (from @fluentui/react-utilities, which the umbrella does not re-export). Provider mount no longer builds or injects a rule set at all — the work is gone rather than reduced, though this PR quotes no re-measured mount timing for it.

Merge order / dependencies — IMPORTANT

This is PR 2 of 2 and must not merge first:

  1. feat(react-icons)!: promote the headless API to the package default fluentui-system-icons#1204 merges first and publishes @fluentui/react-icons 3.0.0 (headless default + required styles.css). This PR's icon integration (the icons stylesheet imported at layer(fui.components.l1), layered icon-state selectors, and retirement of the Griffel jest snapshot serializer) was validated against exactly that code.
  2. This branch currently carries two clearly-marked LOCAL-ONLY(revert-before-PR) commits (90d1096404, b0248a57f1) that point the workspace at a packed tarball of the icons fork for development. Before this PR is marked ready they are reverted — but the revert alone is not enough, and the remaining steps are load-bearing:
    • Bump the declared range. Across every tracked package.json there are 42 @fluentui/react-icons range declarations and zero declare ^3.0.0 (36 × ^2.0.245, plus ^2.0.306 ×2, ^2.0.311, ^2.0.239, ^2.0.237, and the one file: resolution). Reverting without bumping resolves the workspace to published 2.x, which is the Griffel-based build this PR's icon integration assumes is gone.
    • yarn install, then re-run the icon-integration VR gate.
    • Re-capture the size metrics (bash migration/griffel-to-tailwind/metrics/capture.sh <leg>) and publish the bundle numbers from that leg. Every monosize figure currently on the branch is measured against the fork tarball, so none is quoted in this PR body — see FINAL_REPORT.md §2.0 for what is and is not publishable and why.
  3. Then this PR undrafts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455

Ray Knight (ArrayKnight) and others added 30 commits July 29, 2026 22:39
…es hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…s hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…styles hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… from styles hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…yles hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…n from styles hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…s hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…les hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…les hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…tyles hooks (Batch F)

Non-mutating composition per the user-mandated D14; react-hooks/immutability
now enforces with zero suppressions in converted packages. Class output
verified identical (snapshots unchanged by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… the shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…hipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…the shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…ribe the shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…e shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…e the shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…e shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…the shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…describe the shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…nd docs describe the shipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…hipped contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Ray Knight (ArrayKnight) and others added 30 commits August 6, 2026 20:37
…al kebab variables (theming Phase 2a)

SCSS variable NAMES stay camelCase (a JS-like read API, as in Phase 1); only the
emitted var() references change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Mechanical rename of raw var(--camelCaseToken) references (inline styles,
.styles.ts, stories, module css) to the canonical kebab names from
token-rename-map.json. chart-web-components excluded (separate system).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…ing Phase 2a)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…theming Phase 2a)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: apps/public-docsite-v9

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: apps/vr-tests-react-components, migration, packages/react-components/react-accordion, packages/react-components/react-aria, packages/react-components/react-avatar, packages/react-components/react-badge, packages/react-components/react-breadcrumb, packages/react-components/react-button, packages/react-components/react-calendar-compat, packages/react-components/react-card

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: packages/react-components/react-carousel, packages/react-components/react-checkbox, packages/react-components/react-color-picker, packages/react-components/react-combobox, packages/react-components/react-datepicker-compat, packages/react-components/react-dialog, packages/react-components/react-divider

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: packages/react-components/react-drawer, packages/react-components/react-field, packages/react-components/react-image, packages/react-components/react-infolabel, packages/react-components/react-input, packages/react-components/react-label, packages/react-components/react-link, packages/react-components/react-list, packages/react-components/react-menu, packages/react-components/react-menu-grid-preview

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: packages/react-components/react-message-bar, packages/react-components/react-migration-v0-v9, packages/react-components/react-migration-v8-v9, packages/react-components/react-nav, packages/react-components/react-overflow, packages/react-components/react-persona, packages/react-components/react-popover, packages/react-components/react-portal

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: packages/react-components/react-positioning, packages/react-components/react-progress, packages/react-components/react-radio, packages/react-components/react-rating, packages/react-components/react-search, packages/react-components/react-select, packages/react-components/react-skeleton, packages/react-components/react-slider, packages/react-components/react-spinbutton, packages/react-components/react-spinner, packages/react-components/react-storybook-addon, packages/react-components/react-swatch-picker, packages/react-components/react-switch, packages/react-components/react-table, packages/react-components/react-tabs

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: packages/react-components/react-tag-picker, packages/react-components/react-tags, packages/react-components/react-teaching-popover, packages/react-components/react-text, packages/react-components/react-textarea, packages/react-components/react-theme, packages/react-components/react-toast, packages/react-components/react-toolbar, packages/react-components/react-tooltip, packages/react-components/react-tree, packages/react-components/recipes

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Areas: packages/react-components/theme-designer, packages/tokens, tools

Mechanical rename per reports/token-rename-map.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… map (md + json)

Full-token-set kebab rename recorded: namespace mapping decisions, zero
utility-name impact (467 @theme keys byte-identical), the dual-vocabulary
provider-tag correction to the task premise, inline-reference adoption, sweep
counts (5,331 repl / 438 files + tokens/sass/theme), and all gate results
(identity probe 2,237 checks; VR 76/76 at zero tolerance; jest/SSR/lint/tc
green; perf E-cells flat). token-rename-map.json is the machine-readable
find/replace source for consumer migration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…pshot (theming 2b)

Adds the 7 shipped theme CLASS-name constants (webLightThemeClassName, ...,
themeClassNames map, ThemeClassName type) — the JS read path for the static CSS
theme classes — plus the committed theme-values.json snapshot (7 themes x 459
keys) that feeds the react-tailwind-theme generator without a build-time
dependency edge. Jest gates: themeClassNames derivation lockstep +
themeValues.json drift (deep-equal vs computed themes); generate/verify scripts
wired as nx targets and verify-theme-values added to the CI target lists.
Theme objects and createTheme factories deliberately REMAIN exported here —
they are the build-time generator/tooling input under the Phase 2b contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… classes (theming 2b)

The generator now reads packages/tokens/theme-values.json + themeClassNames.ts
(text-level, still no build dependency) and emits: (a) the 433 theme-variant
web-light values into the existing @layer fui.theme { :root, :host } block —
the artifact is the SOLE default value source now that FluentProvider's runtime
tag is gone — and (b) NEW generated css/themes.css with one class per shipped
theme (.fui-theme-web-light ... .fui-theme-teams-dark-v21; 433 custom-property
declarations each, @layer fui.theme), imported by css/index.css. Cross-asserts
theme-set/classname/tokens.ts lockstep, spacing/stroke theme-invariance (the 26
stay :root-only in density-knob form) and zIndex theme-absence. dist/styles.css
2,810 -> 175,638 B raw (14,603 B gzip). Generated css is prettier-ignored:
byte-exact value fidelity vs the JS themes beats formatting (prettier would
rewrite rgba spacing/trailing zeros). @reference emission verified unchanged
(zero theme leakage into module output).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…ag removed (theming 2b)

BREAKING (the migration major): FluentProvider's theme prop (JS theme object)
is REPLACED by themeClassName?: string — pass a shipped constant
(webDarkThemeClassName, ...) or any class containing only custom-property
declarations. The provider injects NO styles: useFluentProviderThemeStyleTag,
createCSSRuleFromTheme, serverStyleProps + the SSR style element, the nonce
prop + StyleNonceContext (D20.1 retired) and the theme-undefined dev warning
are all deleted. Nested providers inherit the resolved class via the new
FluentProviderThemeClassName context (useFluentProviderThemeClassName_unstable,
@internal); portals keep the EXISTING ThemeClassNameContext propagation
(root.className carries the class; bare class under applyStylesToPortals=false).
styles hook leads with styles.root now that the theme class may be empty
(D15.1 marker-position invariant).

react-shared-contexts: ThemeContext_unstable/ThemeProvider_unstable/
ThemeContextValue_unstable removed (sole producer gone); react-theme dep
dropped. react-portal-compat: v8 portals apply the RESOLVED theme class from
the new context instead of regex-extracting the runtime fui-FluentProvider<id>
class (which no longer exists); works for consumer-authored classes too.

Tests rewritten to the new contract (37/37 provider, portal-compat green); SSR
markup is a plain div (hydration head snapshot now empty); api.md regenerated.
Nested-provider custom theming story now demos a module-css theme class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…seCssVarValue memo+deps (theming 2b)

BREAKING: @fluentui/react-components and @fluentui/react-theme stop exporting
the 7 theme objects, create*Theme factories, themeToTokensObject and the
Theme/PartialTheme/Brands/BrandVariants types (they remain in @fluentui/tokens
for build-time tooling), plus the removed provider APIs
(createCSSRuleFromTheme, useFluentProviderThemeStyleTag). New exports: the 7
*ThemeClassName constants, themeClassNames map, ThemeClassName type and
useFluentProviderThemeClassName_unstable. tokens/typographyStyles unchanged.
Bundle-size fixtures follow the new exports.

react-utilities: useCssVarValue gains a module-level memo keyed by
(element, variable) and an EXPLICIT re-read trigger — a deps option compared
like an effect dependency list; providing deps opts that instance out of the
cross-mount memo on first read (a cached hit after a theme change while
unmounted would be stale). Default stays read-once; no per-render reads, no
observers. 10/10 unit tests; api.md regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…seCssVarValue (theming 2b)

BREAKING: useIsDarkTheme now takes an element ref and compares the HSL
lightness of --color-neutral-background-1 vs --color-neutral-foreground-1
computed at that element (canonical names derived from the tokens.* strings —
lockstep, not hardcoded), replacing the removed JS ThemeContext read. The
duplicated DeclarativeChart copy is unified onto the shared hook;
DeclarativeChart uses its container ref, VegaDeclarativeChart merges a root ref
with the forwarded one. Web-light fallbacks preserve the pre-2b default-theme
behavior before mount/outside themed scopes. Tests: theme-object literals ->
themeClassName={webDarkThemeClassName}; snapshots regenerated (diffs are
provider markup + useId shifts only; 917 passed / 86 skipped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…ped-theming + deprecated-package probes (theming 2b)

Mechanical sweep of the component packages' cypress mounts, e2e helpers and the
react-portal unit test from theme={<shipped>Theme} to
themeClassName={<shipped>ThemeClassName} (38 cy files + helpers). NEW committed
browser probes: react-menu MenuThemeScoping.cy.tsx (2/2 — a theme class on any
subtree node themes it while siblings keep :root web light, and a portaled menu
opened inside a themed provider carries the class + resolves dark values on the
portal mount node) and deprecated react-infobutton InfoLabel.cy.tsx (+2 — the
still-Griffel package's tokens.* references resolve against the dark theme
class end to end; deprecated packages have no stories anywhere, so cypress is
the themed-rendering evidence). Motion preview demo + text migration doc ride
along.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…cts from @fluentui/tokens, apply them as CSS classes (theming 2b)

theme-designer keeps building Theme objects (createLightTheme/createDarkTheme
now imported from @fluentui/tokens) and previews/exports them as generated
custom-property theme classes via a local applyThemeAsClass util (canonical
names derived from the tokens.* var() strings); export snippets emit the new
consumer pattern. react-migration-v8-v9: createV9Theme/createV8Theme stay
object-level converters importing from @fluentui/tokens (documented as
build-time/tooling input); ThemeShim stories preview via a generated theme
class; button-shim stories swept to themeClassName constants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… 2b)

getStoryVariant maps DARK_MODE / HIGH_CONTRAST / RTL onto the shipped theme
class constants instead of theme objects, in both the vr-tests copy and the
published tools/visual-regression-utilities mirror. The mirror has zero
in-repo consumers and is therefore NOT covered by any of the 76 VR sets --
its gate is its unit spec (3/3); recorded in the report's VR-reach note so
the pixel evidence is not overclaimed.

Also widens the capture staleness guard, which walked only
packages/react-components and so could never have caught a stale
getStoryVariant. It now also walks packages/charts (19 stories/Charts/*
files import @fluentui/react-charts), apps/vr-tests-react-components/src and
tools/visual-regression-utilities/src.

ChartAnnotationLayer's fontSize 18 -> '18px' is a non-theming edit that
arrived inside this diff. Kept because it is part of the tree the 76/76 was
captured from and is pixel-neutral (React serializes numeric fontSize to
px); called out in the report rather than silently dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
… theme prop, nonce and runtime style tag (theming 2b)

Sweeps the docsite to the shipped contract: import styles.css once, web light
is the :root default, and a theme is applied with themeClassName={xThemeClassName}
or by putting a theme class on any DOM node. Theming.mdx is rewritten around
scoped theming, portal inheritance, the 7 constants + themeClassNames map +
ThemeClassName type, and custom themes as classes of custom-property
declarations in canonical kebab names. It also gains an explicit before/after
migration callout -- theme={webLightTheme} -> themeClassName={webLightThemeClassName}
-- and the import line the nested-provider example was missing.

The createCSSRuleFromTheme section is replaced by Utilities/Theme/ThemeClasses
(default, custom, and a live theme-switching story plus a best-practices doc).
ThemeShim reads theme objects from @fluentui/tokens, which the app now declares
as a dependency ("*", matching all 16 of its other @FluentUI deps in this
private app) -- that clears 4 import/no-extraneous-dependencies errors; the 9
that remain were proven pre-existing by linting the pre-2b file content.

Closeout fixes folded in:
- AdvancedConfiguration.mdx documented the REMOVED nonce prop and runtime
  <style> tag and actively told consumers to migrate TO the nonce prop. The CSP
  section is rewritten (component CSS is a static asset covered by style-src
  source lists; the provider creates no style element), as is "Configuring
  rendering", which claimed the provider writes custom properties into the
  child document.
- QuickStart.mdx and Migration/GettingStarted.mdx were half-applied: code
  fences swapped, prose left asserting that FluentProvider owns the token
  values / that a theme object sets the CSS variables. Prose now matches.

Verified against the SSR run: 0 <style> elements, 0 nonce attributes,
theme = fui-theme-teams-light. public-docsite-v9:build-storybook exits 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…ClassName (theming 2b)

Mechanical prop/import swap to the class constants across the 8 perf
scenarios, the react-components-vite starter template and the recipes
Example template.

Note on FluentProviderWithTheme: the scenario is now effectively vestigial.
Its forced-reflow harness existed to measure the cost of the provider's
theme injection, and there is no injection left -- 20 nested providers now
differ from the sibling FluentProvider scenario only by count and a static
class string. Its perf baseline will therefore move sharply for a reason
unrelated to any component; keeping the scenario for continuity, flagged so
the shift is not misread as a component regression. The dedicated
before/after benchmark measured provider mount at 9.40 ms -> 0.30 ms median
per 40-provider window (-96.8%, n=186/leg), with liveness asserted at 40 vs
0 injected style tags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…VarValue deps (theming 2b)

Phase 2b moved useIsDarkTheme from a ThemeContext_unstable read to
useCssVarValue, and dropped reactivity in the process: useCssVarValue is
read-once-per-(element, variable) by contract and module-memoized in a
WeakMap, so with no deps a stable chart container leaves DeclarativeChart
and VegaDeclarativeChart pinned to whatever theme was active at first mount.

Fix: pass the resolved theme class from useThemeClassName_unstable as deps
on both reads. That restores the pre-2b envelope exactly -- provider theme
changes re-read; a class swapped on a bare ancestor element was never
tracked pre-2b (it read provider context) and still is not. The hook's JSDoc
states that boundary rather than overclaiming it.

Not useFluentProviderThemeClassName_unstable: it lives in
@fluentui/react-provider, which react-charts does not depend on;
useThemeClassName_unstable is already a dependency and carries the same
string (state.themeClassName joins root.className and is published by
useFluentProviderContextValues_unstable).

New suite VegaDeclarativeChartHooks.test.tsx (5 tests) -- proven to bite:
with deps removed the 2 reactivity tests fail; restored, 5/5 pass. Package
suite 922 passed / 86 skipped, no snapshot churn.

No VR set renders a consumer of this hook: it is imported only by
DeclarativeChart and VegaDeclarativeChart, neither of which has a story
under apps/vr-tests-react-components/src, and only barrel files re-export
them -- so the unit suite is its gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…nd theme utils on the class-based API (theming 2b)

- react-storybook-addon: withFluentProvider maps theme ids to the shipped
  class constants; the docs container and example container follow.
- react-storybook-addon-export-to-sandbox: generated sandboxes emit
  themeClassName, so exported examples compile against the new surface
  (54/54).
- scripts/test-ssr: the generated entry points render
  themeClassName={teamsLightThemeClassName}. Verified end to end --
  ssr-tests-v9:test-ssr exits 0 with 0 <style> elements and 0 nonce
  attributes in the output.
- storybook-llms-extractor: the golden fixture and its inline snapshot were
  half-applied (code samples swapped, prose left saying "styled using CSS in
  JS ... a style renderer which inserts CSS into DOM ... pass theme as a
  prop"). Fixed -- this fixture is the contract for generated llms.txt, so
  the stale model would have shipped straight to LLM consumers (9/9, 6
  snapshots).
- theme-designer / react-migration-v8-v9 shims: applyThemeAsClass builds
  theme objects from @fluentui/tokens and applies them as generated
  custom-property classes. The theme-designer copy injected into the global
  document while its byte-identical twin correctly used
  useFluent().targetDocument -- fixed (child-window/iframe hosts are exactly
  why that plumbing exists). Both copies now carry a DUPLICATE
  cross-reference and note that a theme key with no matching tokens entry is
  silently dropped.
- react-theme-sass, eslint-plugin README: docs/tests to the new names.
- workspace-plugin generator spec: themeClassName in the scaffolded output.
  This suite fails on Windows for pre-existing reasons (EBUSY temp locks,
  POSIX-vs-Windows path assertions); the pre-2b content of this spec fails
  snapshots 3, 9, 11 and 12 while the 2b content fails only 3 and 9.

eslint-plugin no-restricted-imports fixtures deliberately keep the old
symbol names -- they are arbitrary RuleTester string literals, never
type-checked, and the rule is config-driven.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…ajor gates (theming 2b)

Change files for every package Phase 2b touches with non-ignored files, plus
the manifest gate lifts the entries need to validate.

Missing entries added:
- @fluentui/tokens (prerelease) -- the package that gained the ENTIRE new
  public surface: the 7 theme class-name constants, themeClassNames and
  ThemeClassName, plus the committed theme-values snapshot. Shipping without
  an entry meant no version bump and no changelog for the new API.
- @fluentui/react-theme-sass (prerelease).
Both manifests disallow major/minor/patch, so prerelease is the only
writable type -- matching the existing react-tailwind-theme entry.

Gate lifts (one deleted "major" line each, precedent commit 9b59224,
which did the same for 56 manifests): react-theme, react-shared-contexts,
react-portal-compat. Their new major entries would otherwise be rejected by
beachball's validator, masked today only because the missing-entry error
fires first.

@fluentui/react-storybook-addon flipped major -> minor: it is 0.7.1, and the
branch's stated 0.x convention (eae2756) keeps 0.x packages on minor.
That also removes its need for a gate lift.

Changelog-fidelity entries added for react-storybook-addon-export-to-sandbox
(the sandbox scaffold change) and react-tailwind-theme (the 2b artifact
delta -- byte figures deliberately omitted, see the report's correction
note: the reported tokens.css baseline was wrong and dist/styles.css is not
committed at the base ref, so neither figure is reproducible from git).

Simulating beachball's validator over all 223 entries now reports 0
disallowed types. @fluentui/theme-designer is private, so its entry is
warned-and-discarded before validation (223 -> 220 entry delta) -- kept for
the human record, matching the pre-existing precedent entry.

NOT fixed here: 18 other packages beachball flags have zero
Phase-2b-attributable non-ignored files. That residual is pre-existing
branch debt and needs its own decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…nsumer migration map (theming 2b)

Closes the record for the phase that removes the JS theming path.

reports/theming-css-native.md -- the Phase 2b section now carries the
shipped artifact (7 theme classes, their contents and sizes, the deliberate
spacing/stroke and zIndex exclusions), the themeClassName prop shape and its
resolution order, the provider surface delta (runtime tag, createCSSRuleFromTheme,
useFluentProviderThemeStyleTag, serverStyleProps, the nonce prop and
StyleNonceContext, ThemeContext_unstable -- all removed), the scoped-theming
and portal contract, the useCssVarValue memo/deps semantics, the runtime
value-reader inventory, and the full gate evidence. Added in this commit:
the re-gate table taken after the closeout fixes (type-check 0, lint 0, the
per-package jest totals, SSR exit 0 with 0 style tags and 0 nonce attributes,
docsite storybook exit 0, perf recomputed from the stored 186+186 samples at
9.40 -> 0.30 ms median, -96.8%), the two failures proven pre-existing by
running pre-2b file content, the beachball state and its 18-package
pre-existing residual, and a "deliberately deferred" list.

Honesty corrections recorded rather than quietly fixed: the artifact byte
figures were wrong (tokens.css pre-2b measures 41,511 bytes, not 20,842;
dist/styles.css is not committed at the base ref so its baseline is not
reproducible; local gzip measures 15,714, not 14,603), and the VR-reach note
states precisely what the 76/76 does NOT cover -- the published
visual-regression-utilities mirror (no in-repo consumers) and the react-charts
hook (no VR story renders a consumer).

NEW reports/theme-api-migration-map.{md,json} -- the committed consumer
find/replace source for this break: 7 theme -> class rows, the prop rename,
the relocated-to-@fluentui/tokens list and the removed-with-no-replacement
table. This is the Phase-2b counterpart to token-rename-map, which Phase 2a
shipped and 2b was missing.

DECISIONS.md D28 records the settled contract and the D20.1 retirement (the
nonce existed solely for the theme style element; with no style element
there is nothing to nonce -- component CSS is a static asset covered by CSP
style-src source lists).

Two maintained in-repo docs still taught the removed surface and are fixed:
docs/react-v9/contributing/patterns/extending-tokens.md (imported
themeToTokensObject, webLightTheme and Theme from the umbrella and used the
theme prop) and docs/architecture/design-tokens.md, the unswept twin of the
copilot.instructions.md block. Both of those also asserted pre-2a
vocabulary -- tokens.colorNeutralForeground1 === 'var(--colorNeutralForeground1)'
-- which is false: the value is 'var(--color-neutral-foreground-1)', and
themes.css declares 0 camelCase names against 7 kebab-case.

Historical RFCs under docs/react-v9/contributing/rfcs/** are point-in-time
design records and stay untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…aminated

The 2026-08-07 restatement re-ran metrics/capture.sh unmodified on
7cc30e3. Its outputs are the provenance for every figure republished in
FINAL_REPORT.md 2.0 and in the PR body, so the leg belongs in the tree
alongside baseline/ and phase4/ rather than living only on the machine that
measured it.

12 of the 15 captured files are committed. The other three are the
build-vnext.log, monosize.log and storybook-build.log captures, which
migration/griffel-to-tailwind/.gitignore excludes from every leg.

A README in the leg directory marks it superseded before anyone cites it.
At this commit the root package.json resolutions field redirects
@fluentui/react-icons to the LOCAL-ONLY fork tarball introduced by
90d1096 / b0248a5, and the installed package self-reports version
2.0.334 with a styles.css export and tslib as its only dependency, i.e. the
headless build rather than published Griffel-based 2.x. Anything measured
through a resolved dependency graph therefore does not reproduce on the tree
a maintainer clones, and the README says which captures that covers and
which measure only this repo's own output.

The json captures are prettier-formatted to satisfy the pre-commit hook;
parsed values were compared before and after and are identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
…-run today

The reports carried figures measured at Phase 4 of the original migration.
Everything since -- the Griffel-zero campaign, the icons fork, the @Property
remedy, Prettier 3, and theming phases 1 / 2a / 2b -- landed after those
numbers were taken, and a prior gate pass found several that no longer
reproduce at all. Every figure below was re-run against 7cc30e3 and its
capture is committed at metrics/head-7cc30e35/.

FINAL_REPORT.md gains a 2.0 "what is publishable today" section that sorts
each metric into publishable, blocked, needs-reframing, or not-re-measured,
with the reason attached to each. Four survive: the Griffel AOT recount
(62 packages / 277 files -> 3 / 6, all under deprecated/), dead
*.styles.raw.js (700,829 B -> 0, with 12 files / 44,304 B remaining on disk
and none outside deprecated/), shipped lib/ JS (-15.57% like-for-like, the
baseline restricted to HEAD's 84-package set), and the react-button tarball
(-34.14% packed, -41.18% unpacked, 185 -> 176 files).

Build time is withheld rather than restated. Five cold identical-scope runs
on one unchanged tree measured 167/163/161/158/156 s in run order, a monotone
warming trend spanning 6.8% of the median, against a single-observation
baseline. No percentage in that range is defensible, so none is given.
Bundle size is withheld because every monosize figure on this branch is
measured through the LOCAL-ONLY icons fork.

The VR storybook figure is disclosed as confounded twice over, not just
once. It was already noted that the storybook gained 24 story files
(137 -> 161). Newly recorded: it is a bundled artifact, 105 of its 505 files
reference the icons package, and the icons override landed 2026-07-31 --
after the baseline (2026-07-27) and Phase-4 (2026-07-30) legs and before
this one. The baseline bundled published Griffel-based icons and this leg
bundled the headless fork. The direction and size of that component are not
measured and deliberately not estimated; +12.95% is stated as the measured
difference between two artifacts that differ in three ways.

Unreproducible figures are removed rather than adjusted:

- theming-css-native.md: the unsourceable 20,842 B for css/tokens.css
  pre-2b is replaced with the 41,511 B that ref 4e90ba2 actually
  measures; both dist/styles.css baselines (1,410 and 2,810 B) are removed
  outright because dist is not committed at that ref, so no baseline for it
  can be reproduced; the 14,603 B gzip figure is reinstated with its
  compression level annotated, having been retracted for disagreeing with a
  measurement taken at a different level. The correction block is rewritten
  as a binding "measurement conventions" section so the same class of
  mismatch cannot recur.
- DECISIONS.md: D10's AOT target of "-> 0" is corrected to the 3 packages /
  6 files that were actually reached; D11's nonce and react-icons bullets
  get retirement markers pointing at D28 and D27; the two 2,810 B figures go.
- icons-integration-1.md: the 2,559 B figure is removed. It measured a
  gitignored artifact produced by a dependency that will not exist in the
  reviewed tree. A provenance caveat replaces it.
- GRIFFEL_ZERO_CLOSURE.md: the charts-jest 912/86/998 cells are marked
  stage-local so they cannot be lifted into a PR body as current status.

perf-property-remedy.md records a re-verification dated today: across the 63
shipped dist/styles.css artifacts, 2 @Property rules survive. Both are
Tailwind-internal and confined to @fluentui/react-storybook-addon, a
devDependency of one package and a runtime dependency of none, so the
consumer-facing claim holds. Because the mechanism is document-wide, any
perf measurement taken inside Storybook has a non-empty registry, and that
is now stated where the measurements are.

theme-api-migration-map.md splits the section 5 fall-through rule into
light-derived and dark-derived cases, which behave differently, and adds a
full-theme-class generator that was verified to run: it emits exactly 433
declarations, matching every shipped .fui-theme-* class.

FINAL_REPORT.md 6.3 also records the open items this pass could not close
without touching component source or release metadata, including the Theme
Designer createThemeClassRule defect, which emits 26 literal spacing and
stroke tokens that sever the --spacing and --base-scale knobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
The migration removed the JS theming path, renamed all 467 token CSS
variables and made the theme package a required install, but the docsite
still taught the removed surface. Two of the breaks fail silently -- a var()
on an undefined custom property drops the declaration with no error, warning
or log, and skipping the theme stylesheet renders components with collapsed
spacing, no radii and no shadows -- so a consumer who reads nothing gets no
signal at all. This gives them somewhere to land.

NEW Concepts/Migration/FromV9/UpgradeGuide.mdx is the single entry point:
seven breaking changes with before/after for each, ordered so the two silent
ones come first.

NEW Concepts/Migration/FromV9/TokenRename.mdx is generated from the
committed token-rename-map.json rather than written by hand: the failure
mode, a summary of the 11 Tailwind namespaces the names now align to, a
find/replace script, and the full 467-row old -> new table.

Corrections to pages that taught the old contract:

- Theming.mdx: the light-derived vs dark-derived fall-through hazard (the
  two behave differently and the difference is silent), an end-to-end custom
  brand-ramp recipe, runtime theme switching, and useCssVarValue as the
  replacement for the removed ThemeContext_unstable.
- StylingComponents.mdx: "still exported / supported escape hatch" was false
  -- replaced with the removal and the two migration paths out of it. The
  FluentProvider custom-properties claim is corrected.
- SSR.mdx and Remix.mdx: these told readers to use a deprecated makeStyles
  re-export that no longer exists; they now point at a direct @griffel/react
  dependency.
- Portals.mdx: new "theming portals" section, since portals inherit a theme
  class rather than a provider context.
- QuickStart.mdx: a "loading stylesheets" section covering the three
  delivery paths, including the umbrella CJS gap.
- UnprocessedStyles.mdx: the charts and icons rows still said those packages
  ship Griffel raw modules. Both now ship plain CSS.
- AdvancedStylingTechniques.mdx: the custom style hook example did not work.
  It discarded the value the hooks return, referenced an undefined symbol in
  the hook map, and had an unbalanced JSX tag. Rewritten to the returning
  shape.
- Migration/GettingStarted.mdx: cross-link to the new guide.
- react-components README: the install line omitted the theme package the
  component CSS compiles against, which is break 0 of the upgrade guide.

Documentation only; no component source is touched and no rendered output
changes, so the visual-regression baselines are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB8ax95bxM4N23bUkSm455
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants