Skip to content

feat(react-windmod-preview): a Tailwind v4 + CSS Modules styling layer for Fluent's headless components - #36656

Open
Ray Knight (ArrayKnight) wants to merge 327 commits into
microsoft:masterfrom
ArrayKnight:styling/react-windmod
Open

feat(react-windmod-preview): a Tailwind v4 + CSS Modules styling layer for Fluent's headless components#36656
Ray Knight (ArrayKnight) wants to merge 327 commits into
microsoft:masterfrom
ArrayKnight:styling/react-windmod

Conversation

@ArrayKnight

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

Copy link
Copy Markdown
Contributor

What this PR proposes

A Tailwind v4 + CSS Modules styling layer for Fluent's headless components, offered for official support and discussion of a sustainable landing plan. It adds preview packages without changing the styling system shipped by existing @fluentui/react-components consumers.

Following the maintainer feedback about review size, the independent fixes have been split into their own PRs. The remaining decision is whether the styling layer should be submitted in smaller in-tree pieces or maintained as a community package. The follow-up comments describe those options.

What ships

Package Role
@fluentui/react-tailwind-theme-preview Fluent tokens, seven theme stylesheets, Tailwind variants, cascade layers, and density scaling
@fluentui/react-windmod-preview Styled headless components across 52 family subpaths; 133 authored CSS Modules; per-component CSS delivery
@fluentui/postcss-tailwind-css-modules Public PostCSS plugin that preserves Tailwind group/peer markers under CSS Modules; used by this build pipeline and Storybook

The styling packages include a 63-entry migration guide and a shipped authoring skill. Checkbox, Radio, and Switch use a grid to align the indicator with the first label line, including wrapping labels. The CSS layer and scaling contracts are documented with the packages.

Independent fixes and current status

PR Fix Status
#36663 Breadcrumb distributive Omit Merged
#36664 ColorPicker zero-channel fallback Merged
#36665 Headless popover attribute spread order Merged
#36666 Dialog scrollbar gutter Open; preserves host stable both-edges gutters after review
#36667 TagPicker aside-width animation frame Open; ref-detach cleanup, valid zero handles, cancellation-aware React 18 verification
#36668 Tooltip arrow class Merged
#36669 MenuSplitGroup context values Merged
#36670 Headless context exports Merged
#36671 Storybook sandbox registration on Windows Open
#36672 API subpath generation on Windows Open
#36673 Preservation of declared static export subpaths Open
#36690 NavCategoryItem decorative icon accessibility Merged

This branch incorporates current upstream and the reviewed Dialog, TagPicker, and context-export fixes. The four StackShim compiler tests were leftovers from the earlier whole-library conversion: they referenced a CSS module that is absent from this PR. That obsolete test group is removed. The existing v8-to-v9 StackShim implementation remains intact. The PostCSS declarations now match the callable CommonJS export, with type fixtures for CommonJS and ESM consumers.

Verification and evidence

The September 9 upstream-sync sweep passes all 93 scenes: 67 strict-zero / 26 within individually ratified allowances / 0 failures. The registry retains its existing 27 allowance rows; one of those rows rendered at strict zero in this run. Pixelmatch uses threshold zero with its antialiasing classifier enabled; this is not a claim of byte-identical screenshots. All 163 Windmod test suites / 3,642 tests pass. All 83 headless suites / 1,167 tests also pass, including the new axe conformance checks. Build, lint, type-check, browser mutation checks, and verification limits are recorded in the latest update.

The three TagPicker frame regressions pass on React/ReactDOM 18.3.1 and React 19.2.0. On React 18, reverting to passive-effect cleanup fails the StrictMode test; reverting the explicit null check fails the zero-handle unmount test. Dialog preservation tests cover inline and stylesheet-provided gutters.

The bundle and runtime tables in the deep-dive comment are historical measurements, not fresh measurements of this head. The runtime benchmark predates per-component CSS delivery and used the monolithic stylesheet; its heap metric covers JavaScript, excluding CSSOM. Griffel was faster on the measured re-render case. The deep dive retains the methodology and rationale alongside those limits.

Remaining dependencies and decisions

Related issues

#36645, #36646, #36647, #36648, #36649, #36650, #36651, #36652, #36653, #36654, #36655, #36685.

The individual fix PRs own issue closure. The larger argument, historical measurements, and detailed findings remain in the linked deep dive.

Ray Knight (ArrayKnight) and others added 30 commits August 24, 2026 04:05
…Item and NavSubItemGroup (headless + Tailwind, pixel-identical to Griffel)

The category components complete the Nav family, reusing the shared row
presentation and identity marker the first four components established. The
expand chevron rotates through plain CSS declarations on the icon-slot class,
so consumer-supplied icons rotate identically, and the open group ships
Griffel's compiled overflow verbatim. The collapse enter/exit motion is not
ported; its end state leaves a Griffel group able to scroll overflowing
content where this implementation clips — recorded with the motion delta in
the migration notes, invisible while content fits.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x2015 scene, Griffel-vs-Griffel control zero), 23 planned + 12 CSS
mutations accounted for, and byte-idempotent API reports across all 60 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ch, ImageSwatch and SwatchPickerRow (headless + Tailwind, pixel-identical to Griffel)

The five-component family ships with the root unpinned — the provider already
authors a superset of the typography and colour the plan expected to pin. The
theme catalog gains a layout-grid variant, and the library catalog drops five
entries no stylesheet ever referenced, proven inert in both compiled sheets.
The disabled swatch's glyph honours consumer children and slot-null removal.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x684), with the full-catalog sweep as the shared-variant guard.
75 of 76 mutations killed across the planned and review tables (the survivor
a proven semantic no-op pinned by an invariant test), and byte-idempotent API
reports. Consumer style is silently discarded by the headless picker and row
base hooks on both libraries; the specs assert that parity rather than mask it.

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

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…t sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…t sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

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

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

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

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ilies to flat sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…lat sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…el-identical to Griffel)

The increment and decrement glyphs follow the uniform slot-fallback rule —
both slots always materialise, so consumer children and slot-null removal
compose without a special case. The active-step visual selects on a
presence-based data attribute stamped per button, derived locally because the
headless hook only exposes the keyboard half of its spin state. The root
ships without a typography pin, matching a Griffel reset that authors no font
declarations, while the small size restates its full caption set so nested
typography scopes inherit identically.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x848, Griffel-vs-Griffel control zero), 35 mutations killed across
the planned and review tables, and byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…l-identical to Griffel)

SearchBox composes the shipped Input conventions: the magnifier and dismiss
glyphs follow the uniform slot-fallback rule, restored together in one
immutable state object since both slots always materialise, with consumer
children and slot-null removal independent per slot. Content-presence stamps
follow the ratified spelling and remain Input's consumer contract even where
the composed block re-writes the padding they gate. The dismiss interaction
clears through a single change event and returns focus to the input, matching
the reference behaviour exactly.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (0 of 1,457,664 pixels across 74 cells including the focused underline),
30 mutations killed across the planned and review tables with one proven
equivalent, and byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…r and ColorArea (headless + Tailwind, pixel-identical to Griffel)

The transparency checkerboard ships as an inlined data URI proven
byte-identical to the CDN asset the Griffel implementation fetches, so the
alpha rail and area paint without a network request — including offline and
under restrictive content-security policies, a divergence the migration notes
record. Channel-specific styling selects on an enumerated data attribute pair
added to the library catalog; the sliders' gradients, thumb geometry and
right-to-left mirroring reproduce the compiled reference buckets, with the
mirrored rails covered by their own visual band.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x2320 over 58 cells), 42 mutations killed across the planned and
review tables with a single-bit checkerboard flip proven caught by the visual
gate alone, and byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ng for Button and ToggleButton

The reference stylesheet emits every forced-colors media rule after all other
buckets, so those declarations win any equal-specificity contest. This
package carries the same cascade with source order inside a layer, and its
forced-colors blocks sat early enough for later appearance, state and checked
blocks to beat them — leaving checked-subtle surfaces on authored greys where
the reference shows Highlight on HighlightText, and diverging under hover,
press and focus. Every forced-colors block now trails its class and restates
what it must win back, in cascade order only: no importance, no new variants.

Verified by computed-style probes under forced-colors emulation across 457
button and toggle-button cells in four interaction states: 610 divergences
before, zero after, zero newly-broken, with a reference-vs-reference control
of zero and the baseline reproduced exactly on revert. The normal-mode
visual sweep holds every scene at its gate, so the change is invisible
outside forced-colors. Seventeen residual cells belong to the toolbar layer's
own modules and are recorded for their own fix.

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

A component rendered inside a providing container now reads the same look
contexts the reference implementation does: Button, CompoundButton,
MenuButton and ToggleButton take their size from ButtonContext, Link its
inline flag from LinkContext, Avatar its shape and size from AvatarContext,
and the field controls their size from FieldContext — narrowed to the look
key, since the base hooks already apply the aria half. Tag derives the
avatar shape and size its children consume. The contexts re-export from the
headless package so the provider instances connect; one shared helper folds
context into props ahead of destructuring, keeping local values authoritative
and letting context beat only the defaults.

Verified by container scenes that previously pinned these values and now
adjudicate them — message-bar and tag hold strict zero against the reference
and fail measurably when any fold is severed. 29 mutations caught, the full
visual sweep holds every scene, and the API report deltas are additive only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ng in the toolbar button variants

The toolbar's layer sits above the shared button layers, so its plain
declarations beat the forced-colors blocks below: the checked-subtle glyph
kept a brand hover colour the system palette replaces arbitrarily, and the
checked rest border swallowed the focus border colour. Both variants gain
trailing forced-colors re-asserts — the glyph takes the system Highlight
under hover and press, the checked root takes HighlightText border colour
under focus — repeating only what their resting blocks win back.

Verified by computed-style probes under forced-colors emulation across all
75 toolbar cells in four interaction states, including a widened focus walk
that reaches past the reference toolbar's roving tabindex: 22 divergences
before, zero after, zero newly broken, with the shared button layers
unmoved, a reference-vs-reference control at zero, and the baseline
reproduced exactly on revert. The normal-mode visual sweep holds every
scene, so the change is invisible outside forced-colors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ng for MenuButton and SplitButton

The open menu button's selected background, its focus border, and its
disabled colours all sat in blocks that outrank the shared button layer's
forced-colors rules, and the split button's divider edge declared a logical
border longhand that beats the physical focus shorthand below it — four
faces of the same cascade contest the reference resolves by emitting every
forced-colors rule last. Both modules gain trailing forced-colors re-asserts
inside the winning blocks, including the disabled colour crossing whose text
colour also restores the chevron glyph fill through currentColor.

Verified by computed-style probes under forced-colors emulation across all
250 menu-button and split-button cells with a widened focus walk: 106
divergences before, zero after, zero newly broken across every probed scene
and phase, with a reference-vs-reference control at zero and the baseline
reproduced exactly on revert. The survey now covers every module in the
package's upper layers; the two remaining divergences trace to reference
bugs and are recorded, not copied. The normal-mode visual sweep holds every
scene, so the change is invisible outside forced-colors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…lass-map keys to camelCase

The named font-size tokens across all seven themes become calc products of
the shared base-scale ratio, so text now scales with the root font size the
way spacing and stroke widths already do — identical at the default 16px
root. Icon glyph sizes gain their own named tokens. The CSS-module class-map
serializer and the storybook loader both export each kebab-case local under
a camelCase alias so styles hooks keep dot access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ponents A-C

Arbitrary-property utilities become plain declarations, class-map locals go
kebab-case behind the camelCase aliases, multi-branch ternaries flatten to
if-returns, single-use props destructure in the parameter list with state
literals inlined, and transition-property adopts the reference shorthands.
No public API, ident-independent pixel, or behaviour change; the full visual
sweep holds every scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ponents D-M

Arbitrary-property utilities become plain declarations, class-map locals go
kebab-case behind the camelCase aliases, multi-branch ternaries flatten to
if-returns, single-use props destructure in the parameter list with state
literals inlined, and transition-property adopts the reference shorthands.
No public API, ident-independent pixel, or behaviour change; the full visual
sweep holds every scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ponents N-S

Arbitrary-property utilities become plain declarations, class-map locals go
kebab-case behind the camelCase aliases, multi-branch ternaries flatten to
if-returns, single-use props destructure in the parameter list with state
literals inlined, and transition-property adopts the reference shorthands.
No public API, ident-independent pixel, or behaviour change; the full visual
sweep holds every scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ponents T-Z

Arbitrary-property utilities become plain declarations, class-map locals go
kebab-case behind the camelCase aliases, multi-branch ternaries flatten to
if-returns, single-use props destructure in the parameter list with state
literals inlined, and transition-property adopts the reference shorthands.
No public API, ident-independent pixel, or behaviour change; the full visual
sweep holds every scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…-identical to Griffel)

TagGroup is mostly wiring: one flex row with size-stepped column gaps, and
the look channel its children read. The headless context publishes no
appearance or size, so the group fills the reference context for any Tag
implementation inside it, and a local context carries the same pair to this
package's Tag, whose defaults now yield to the group exactly as the
reference resolves them — proven byte-identical out of a group across a
160-pair identity matrix. Dismiss and disabled behaviour were already wired
base-hook to base-hook. Selection interaction belongs to the interaction
tags and ships with them.

Verified pixel-identical to the Griffel implementation at strict zero on
both the new scene and the Tag guard scene, 24 planned and review mutations
killed with one gate-owned, and byte-idempotent API reports across all 76.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
… not only the aggregate

The root's setup block and setup.md's pre-flight list named only the all-seven
styles.css; setup.md's own recommended path (and the package README) is the
theme-less base.css plus one themes/<name>.css per theme shipped, with the
aggregate as the fallback. Both now say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ins the single-@apply rule

The consumer CSS Modules section no longer prescribes prettier: any Tailwind
class sorter (prettier-plugin-tailwindcss, oxfmt, Biome) works, pointed at the
same reference target the modules use. The paragraph now carries the why — a
sorter guarantees canonical order only within one @apply list, so two lists in
a rule hide which declaration wins — which is the reason for one @apply per
block position.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ilwind group/peer markers under CSS Modules

Extracts scripts/css-modules/globalize-group-markers.js into
@fluentui/postcss-tailwind-css-modules (packages/react-components, plain
CommonJS with a hand-written .d.ts, no build step) so consumers who author CSS
Modules against windmod can install the same step the library's own build
runs: Tailwind emits `.group\/name` / `.peer\/name`, postcss-modules would
hash them into selectors the DOM never matches, and this plugin wraps them in
:global() in between. Adds an `include` filter (default `.module.css`; `true`
to disable; an undefined `from` is left untouched unless disabled), keeps the
text-level idempotence, `onRewrite` and `globalizeSelector` exports, ships a
README with the ordering rule and the Vite note, and a 24-case spec.

The build executor and the storybook rules now require the package; the old
script is deleted. The windmod skill's setup reference recommends the package
(with @accelint/postcss-tailwind-css-modules noted as the equivalent
third-party plugin) instead of an inline copy.

Verified: package jest 24/24; react-windmod-preview build green with no
marker-leak assertion; lint green; static VR storybook build and the Button
scene at 0 strict-diff pixels through the new require path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Updated in 937888a after merging current upstream and the reviewed fixes from #36666, #36667, and #36670.

  • Resolved the main and fix(react-headless-components-preview): publish context readers from the button and link subpaths and context providers from avatar and avatar-group #36670 merge conflicts. The Avatar report retains upstream state changes and the requested context exports; MenuSplitGroup uses the upstream exported context-values hook.
  • Addressed the new TagPicker review: the mock flushes only uncancelled frames and tests handle 0. The three frame regressions pass with React/ReactDOM 18.3.1 and 19.2.0. On React 18, reverting to passive-effect cleanup fails the StrictMode test; reverting the explicit null check fails the zero-handle unmount test. fix(react-tag-picker): tie aside-width frame cancellation to observer detach #36667 includes the React 18 integration-test config. Local Windows verification required corrections to the generated RIT scaffold's paths and Jest launcher, documented in that PR's reply.
  • Addressed the new Dialog review: preserve an existing computed stable or stable both-edges gutter, including stylesheet-provided values, during locking and after unlocking. All 18 Dialog tests pass; restoring the unconditional fallback fails the preservation tests.
  • Removed four obsolete StackShim compiler tests. They came from the earlier whole-library CSS conversion and entered this PR with the build-infrastructure port, but their StackShim.module.css never did. The existing compatibility component and its Griffel StackShim.styles.ts remain intact. These four failures were introduced by this PR, rather than being upstream test debt.
  • Corrected the PostCSS plugin's declarations to match its callable CommonJS export. Strict NodeNext fixtures exercise CommonJS and ESM consumers; the previous declaration fails those call-site checks. The Storybook type boundary also accounts for the two resolved PostCSS versions. Updated the migration guide to describe the current first-line grid and existing visual allowances.

Validation on Node 22.12.0:

Check Result
Theme generation/verifiers; headless and Windmod builds Pass
Windmod and stories lint/type-check Pass, with an existing unrelated stories lint warning
Windmod unit tests 163 suites / 3,642 tests pass
Headless unit tests on #36670 79 suites / 1,083 tests pass
PostCSS runtime tests and CommonJS/ESM type fixtures 24 tests pass; type-check passes
Remaining CSS Modules compiler tests 34 pass
Static Storybook and full visual sweep 93 scenes: 67 strict-zero, 26 within ratified allowances, 0 failures
Workspace sync, immutable install, formatting, change-file check Pass

The visual registry still has 27 allowance rows; one rendered at strict zero in this run. No ceilings were raised for this follow-up. Historical bundle/runtime measurements in the deep dive have not been rerun.

Audit follow-up: the broader workspace-plugin suite still has 65 local failures, including Windows path assertions and output/snapshot mismatches. A controlled comparison with the original StackShim test group gives 69 failures; removing it gives 65, with exactly those four missing-file failures disappearing and all other failures unchanged. This establishes the cleanup's effect, not that the whole build-tool suite is green or that all 65 failures predate this PR. Those remaining failures need separate triage. The broader TagPicker default run also encountered an unrelated icon snapshot difference with the local icons dependency; no snapshot was updated.

The PR remains draft pending microsoft/fluentui-system-icons#1228 and a published dependency containing the icon stamp. The Carousel question in #36684 remains held for maintainer direction.

@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Implemented in 8ca3971.

The icons dependency blocker is removed: Windmod now targets the existing public fui-Icon-filled / fui-Icon-regular class tokens through its shared Tailwind variants, and uses published @fluentui/react-icons@2.0.339. The local tarball resolution is removed; the Windmod dependency floor is now ^2.0.339.

The selectors use [class~='…'] inside :where(), preserving exact class-token matching through CSS Modules and adding zero specificity. The existing theme import places icon rules in fui.base, below the component layers. No layer order or style declarations changed.

Verification:

  • 96 browser cascade comparisons passed across hover/checked states, direct-child/deeper glyph selectors, LTR/RTL, forced colors, and stylesheet order. A same-layer override still wins; changing :where() to :is() makes that specificity check fail.
  • All 82 affected compiled rules changed only their icon selectors; the theme's compiled rules are unchanged.
  • All 93 visual scenes passed: 67 strict-zero and 26 within existing allowances, with no ceiling changes.
  • All 163 Windmod unit suites / 3,642 tests passed after updating the InfoButton glyph assertion to the exported class constants.
  • Theme/headless/Windmod builds, theme verifiers, package/story lint and type checks, immutable installation, formatting, and beachball validation passed.

The unlayered-icons negative control fails with both selector approaches, confirming that consumers should keep the existing layered import and avoid an additional unlayered copy. The icon attribute proposal in microsoft/fluentui-system-icons#1228 is no longer required for this implementation.

@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Synced with the latest master through 43665d5 and pushed 94a9319.

The headless update in #36712 replaces NavCategoryItem's data-open with data-expanded. Windmod now uses the existing group-expanded variant for the chevron, and its state tests follow the new attribute. The remaining state attributes and AvatarGroup context-helper changes are incorporated, along with the new headless accessibility conformance checks from #36576. The component/export comparison found no new components requiring a Windmod implementation.

Validation on the merged tree:

  • Theme, headless, and Windmod builds; headless/Windmod/story type and lint checks; theme verifiers, formatting, and beachball checks passed.
  • Headless: 83 suites / 1,167 tests passed. Windmod: 163 suites / 3,642 tests passed.
  • Browser checks verified actual open/close clicks and chevron rotation in LTR, RTL, and forced-colors mode. A control restoring the former selector broke expanded chevrons; restoring the new selector passed.
  • All 93 visual comparisons passed: 67 strict-zero and 26 within existing documented allowances. No ceilings or baselines changed.

The PR remains ready for review. Published icon class selectors remain in use; the icons tarball dependency is removed.

@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Synced latest master through #36723 (50ddff229c) in merge commit 648e372219.

This update changes release pipelines and release-tool authentication, adds the ESRP release helper, and updates beachball. It introduces no new headless components or changes to component code, styles, runtime dependencies, or Storybook/build-tool sources. Windmod's Tailwind and formatter dependencies are preserved.

Scoped validation passed: immutable installation; all 4 scripts-executors suites / 10 tests; script type/lint checks; parsing all 11 changed YAML files; and beachball checks. No release or publish task was run. The September 9 component validation, including 93 visual scenes, remains the latest component sweep; those unchanged sources were not retested for this release-tooling-only merge.

@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Updated status and reposted below the commit history for visibility.

Thanks Dmytro Kirpa (@dmytrokirpa) — completely fair on size, and I'll take you up on the fixes offer immediately: each in-tree fix from this PR is now its own isolated PR, one per linked issue:

On the rest — before I move it to a community repo, I'd like to understand which cost is the actual blocker, because they point to different paths:

  1. If it's this PR's review surface, I can decompose much further — theme package alone first, then per-family batches, each pixel-verified independently.
  2. If it's the prospect of maintaining two styling systems indefinitely, that's the conversation I'd most value having, because the design goal was the opposite: the layer holds pixel-parity with Griffel across 93 VR scenes precisely so it can serve as a migration path — one that would let Griffel be deprecated over time and shrink the maintenance surface rather than double it. We proposed the big-bang version of this before and understood the appetite wasn't there; the preview-package shape was meant as the incremental route to the same destination. If there's any appetite for that direction, I'd gladly shape the work to whatever on-ramp the team could sustain.

If neither lands, I'll host it as a community package — in that case: would the team be open to a discoverability pointer (docs/community listing), and is the headless package's public API + DOM structure a contract you'd consider stable enough for an external styling layer to build against?

@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Updated status and reposted below the commit history for visibility.

Dmytro Kirpa (@dmytrokirpa) Following up with a proposed submission order if review size is the main concern:

  1. Theme package. Fluent tokens, seven theme stylesheets, the generator, and its checks. This can be useful independently of the component layer.
  2. PostCSS plugin and build infrastructure, with Button as the first complete example. The plugin is now a separately publishable package used by the build and Storybook paths. It could be reviewed separately from the infrastructure if that makes the first component PR easier to assess.
  3. Component-family batches. Order each batch after the families whose state selectors it depends on. Each submission would carry its build/type/lint results, visual comparisons, and relevant migration-guide entries.
  4. Remaining documentation and migration guidance. Include shared documentation with the change it explains, then finish the cross-package guidance.

The earlier sizing exercise estimated roughly 16 PRs and a five-PR critical path. That estimate predates the separately publishable PostCSS plugin and is a starting point, not a final count. I would recut the batches and sizes against the current tree once there is agreement on the approach and a review cadence the team can sustain.

The independent fixes are already separate (#36663#36673, plus #36690), and seven have merged. The remaining fixes can proceed independently of the decision on Windmod.

If the main concern is maintaining a second styling system over time, the longer-term direction in my earlier comment is the decision to settle first; the community-package route remains available.

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