feat(react-windmod-preview): a Tailwind v4 + CSS Modules styling layer for Fluent's headless components - #36656
Conversation
…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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
|
Updated in 937888a after merging current upstream and the reviewed fixes from #36666, #36667, and #36670.
Validation on Node 22.12.0:
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. |
|
Implemented in 8ca3971. The icons dependency blocker is removed: Windmod now targets the existing public The selectors use Verification:
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. |
|
Synced with the latest The headless update in #36712 replaces NavCategoryItem's Validation on the merged tree:
The PR remains ready for review. Published icon class selectors remain in use; the icons tarball dependency is removed. |
|
Synced latest 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. |
|
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:
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. |
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-componentsconsumers.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
@fluentui/react-tailwind-theme-preview@fluentui/react-windmod-preview@fluentui/postcss-tailwind-css-modulesThe 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
Omitstable both-edgesgutters after reviewThis 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
@fluentui/react-icons@2.0.339headless API. The shared Tailwind variants target its public filled/regular class tokens with zero added specificity; the local tarball resolution is removed. The extra attribute proposed in feat(react-icons): expose the bundled icon variant as a data attribute fluentui-system-icons#1228 is no longer required. All 93 visual scenes pass against the existing gates.TeachingPopoverCarouselFooterdrops thelayoutprop and the slot order it drives, so the offset footer's DOM and tab order cannot be reproduced through its renderer #36684 asks whether the headless Carousel layout omission was intentional. The question remains open; the current documented Windmod behavior is retained.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.