From b9b417eecadcbfe93a26f88494925a3efa9aaa66 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sat, 17 Oct 2026 00:13:21 +0200 Subject: [PATCH 1/4] Components: Retire completed Emotion migration guidance --- .../skills/design-system-code-review/SKILL.md | 2 +- .../design-system-contribution/SKILL.md | 2 +- .../skills/emotion-to-scss-modules/SKILL.md | 26 ---- packages/components/AGENTS.md | 1 - packages/components/CONTRIBUTING.md | 26 +++- .../components/emotion-to-scss-modules.md | 146 ------------------ .../src/border-control/style.module.scss | 4 +- .../src/color-palette/style.module.scss | 5 +- .../src/confirm-dialog/style.module.scss | 2 +- .../style.module.scss | 2 +- .../date-time/time-picker/style.module.scss | 2 +- .../src/number-control/style.module.scss | 2 +- .../src/palette-edit/style.module.scss | 2 +- .../components/src/range-control/utils.ts | 2 +- .../resize-tooltip/style.module.scss | 2 +- .../src/textarea-control/style.module.scss | 4 +- .../src/tools-panel/style.module.scss | 7 +- .../src/unit-control/style.module.scss | 2 +- .../components/src/z-stack/style.module.scss | 2 +- packages/scripts/docs/vitest-migration.md | 4 +- 20 files changed, 46 insertions(+), 199 deletions(-) delete mode 100644 .agents/skills/emotion-to-scss-modules/SKILL.md delete mode 100644 packages/components/emotion-to-scss-modules.md diff --git a/.agents/skills/design-system-code-review/SKILL.md b/.agents/skills/design-system-code-review/SKILL.md index f9f9dcc05abf41..719369ede0cd71 100644 --- a/.agents/skills/design-system-code-review/SKILL.md +++ b/.agents/skills/design-system-code-review/SKILL.md @@ -18,6 +18,6 @@ Read the affected package's `AGENTS.md` and [Working with WordPress Design Syste 5. **Interaction or accessibility regressions.** Check the semantics, accessible names, keyboard paths, focus, and disabled states the change can affect. Use the guide's [verification guidance](../../../docs/contributors/design/design-system-packages.md#verify-the-affected-behavior) for standards and browser evidence. Passing types or class assertions do not prove interaction or visual parity. 6. **Public documentation or generated output left behind.** Check affected prop documentation, stories, component status and recommendation metadata, generated token assets, and required changelogs. Do not request new stories or documentation for an unchanged capability. -Apply [package-runtime-compatibility](../package-runtime-compatibility/SKILL.md) only when the changed contract crosses independently updated package and WordPress versions. For an Emotion migration, use the [migration guide](../../../packages/components/emotion-to-scss-modules.md) to check cascade and consumer compatibility. +Apply [package-runtime-compatibility](../package-runtime-compatibility/SKILL.md) only when the changed contract crosses independently updated package and WordPress versions. For stylesheet changes, use the [styling conventions](../../../packages/components/CONTRIBUTING.md#styling) to check cascade and consumer compatibility. Keep findings tied to the change, including downstream effects. Missing tests alone are not a finding. Reuse existing coverage and request a new check only for a concrete, meaningful risk introduced by our code or integration. Combine concerns that have one coherent fix. diff --git a/.agents/skills/design-system-contribution/SKILL.md b/.agents/skills/design-system-contribution/SKILL.md index b4f2bb6d27c09d..4eeb1d9e5f943a 100644 --- a/.agents/skills/design-system-contribution/SKILL.md +++ b/.agents/skills/design-system-contribution/SKILL.md @@ -21,7 +21,7 @@ Keep product-specific behavior in the consuming package and use [design-system-u - Follow the package's component, prop/ref, state, and styling conventions. Reuse existing composition and helpers instead of rebuilding behavior already provided by a dependency. - For removals, replacements, or behavior changes, compare the affected old and new contracts using the [compatibility guidance](../../../docs/contributors/design/design-system-packages.md#compare-contracts-before-replacing-an-api). Include plugin and npm consumers, even when all Gutenberg callers have been migrated. -- For token changes, choose by semantic purpose and affected modes, then follow the [token source guide](../../../packages/theme/tokens/README.md) and [build procedure](../../../packages/theme/README.md#building). For Emotion migrations, use [emotion-to-scss-modules](../emotion-to-scss-modules/SKILL.md). +- For token changes, choose by semantic purpose and affected modes, then follow the [token source guide](../../../packages/theme/tokens/README.md) and [build procedure](../../../packages/theme/README.md#building). - Use [package-runtime-compatibility](../package-runtime-compatibility/SKILL.md) only when changing a contract between independently updated package and WordPress versions. ## Verify what changed diff --git a/.agents/skills/emotion-to-scss-modules/SKILL.md b/.agents/skills/emotion-to-scss-modules/SKILL.md deleted file mode 100644 index d11b1faccab1e5..00000000000000 --- a/.agents/skills/emotion-to-scss-modules/SKILL.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: emotion-to-scss-modules -description: Use when planning or implementing an Emotion-to-SCSS-Modules migration in @wordpress/components, including styled wrappers, css/useCx composition, and migration-related cascade fixes. ---- - -# Migrate Emotion styles to SCSS Modules - -In a Gutenberg checkout, read the relative links below from the checked-out revision. Outside a checkout, resolve them from `.agents/skills/emotion-to-scss-modules/` in [Gutenberg on GitHub](https://github.com/WordPress/gutenberg/tree/trunk). - -Read the [migration guide](../../../packages/components/emotion-to-scss-modules.md) and the package's [styling conventions](../../../packages/components/CONTRIBUTING.md#styling) before editing. - -## Preserve behavior and consumer overrides - -1. Identify the requested component and baseline. Search for consumers of its wrappers, style fragments, and public classes, including outside `packages/components`. Use the guide's [contract audit](../../../packages/components/emotion-to-scss-modules.md#establish-the-component-and-consumer-contract) to select affected states and compositions for comparison. -2. Capture the baseline for those cases. For a reported regression, reproduce it first. Apply the [translation rules](../../../packages/components/emotion-to-scss-modules.md#translate-the-styles), distinguishing static, conditional, and genuinely dynamic values. Preserve the wrapper's element, props, and ref behavior. -3. Check which declarations win after removing the wrapper. Follow the guide's [cascade checks](../../../packages/components/emotion-to-scss-modules.md#preserve-the-winning-declarations) for consumer overrides and remaining Emotion composition. Justify a specificity change with the actual conflicting declaration. - -Keep intentional API, interaction, or visual changes within the agreed scope. Consult a [merged example](../../../packages/components/emotion-to-scss-modules.md#reference-migrations) only when it answers a migration question; an older exception is not a rule for this component. - -## Verify the migration - -Reuse existing tests and stories. A behavior-preserving migration may need no new tests. Add a focused test only for an uncovered risk introduced by the migration; do not duplicate shared helper or dependency suites. Follow the guide's [verification choices](../../../packages/components/emotion-to-scss-modules.md#verify-css-delivery-and-behavior) and the [testing overview](../../../docs/contributors/code/testing-overview.md#folder-structure). - -Compare actual styles for the affected states and consumers, including RTL or another document when relevant. Class assertions and mocked styles cannot prove cascade or visual parity. Recheck affected consumers after subsequent selector changes or a rebase that changes relevant styles. - -Follow the [completion checks](../../../packages/components/emotion-to-scss-modules.md#complete-the-migration) for scoped cleanup, affected downstream snapshots, generated files, required checks, and the migration changelog. Report any unverified styling case with reproducible manual steps. diff --git a/packages/components/AGENTS.md b/packages/components/AGENTS.md index be4fef880564ff..b30f8bd7836d85 100644 --- a/packages/components/AGENTS.md +++ b/packages/components/AGENTS.md @@ -4,6 +4,5 @@ Use [design-system-contribution](../../.agents/skills/design-system-contribution - [CONTRIBUTING.md](./CONTRIBUTING.md) for component APIs, compatibility, styling, stories, and documentation. Existing components can have compatibility constraints that differ from the conventions for new components. - The affected component's README, types, stories, and consumers before changing its contract. -- [emotion-to-scss-modules](../../.agents/skills/emotion-to-scss-modules/SKILL.md) for an Emotion migration. For consumer component selection, follow [Choose a recommended component](../../docs/contributors/design/design-system-packages.md#choose-a-recommended-component). This package remains supported; do not migrate existing consumers to `@wordpress/ui` just because it is newer. diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index 46cdd96a0c634a..32f8567713cd5d 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -422,9 +422,31 @@ Place component-local styles in a `style.module.scss` file next to the component Use `--_wp-components-*` for new private custom properties. These are implementation details, not public theming APIs. Keep supported customization in component props and existing public styles. -Components no longer use Emotion. Do not add new Emotion imports. +Components use SCSS Modules for styling. Do not add Emotion styling imports. The completed migration and its release notes are tracked in [#66806](https://github.com/WordPress/gutenberg/issues/66806). -The [Emotion-to-SCSS migration guide](./emotion-to-scss-modules.md) records compatibility checks, cascade and custom-property pitfalls, verification, and examples from the completed source migrations. +### Preserve styling contracts + +Before changing styles, inspect the component's types, README, stories, and consumers. Keep the existing rendered element, `as` behavior, refs, prop forwarding, public classes, and consumer `className` and `style` handling. A styled wrapper around a shared component may also supply layout or accessibility behavior. Keep that component when replacing its styles. + +Use [`PolymorphicElement`](./src/utils/polymorphic-element.tsx) for existing polymorphic components that need intrinsic HTML/SVG prop filtering. Reuse the [`WordPressComponent` types](./src/context/wordpress-component.ts) instead of adding another polymorphic type system. `View` continues to accept its legacy `css` prop as a no-op and consumes it before rendering. + +Keep private module classes separate from public classes supplied by callers. Check real consumers of shared labels and wrappers, including visually hidden content and `as="legend"`. + +### Dynamic values and the cascade + +Keep fixed values and calculations in Sass when JavaScript does not need them. For dynamic custom properties, supply units explicitly where required and distinguish zero from an absent value. Custom properties inherit, so reset instance-specific values when nested components must not inherit them. + +Preserve the component's existing precedence for consumer styles, shorthands, longhands, and per-axis values. CSS-wide keywords such as `inherit` and `initial` apply to a custom property itself, so passing them through `var()` does not preserve their meaning for another CSS property. Moving a declaration directly to `style` also changes whether consumer stylesheet rules can override it. + +Check the declarations that win in composed components and in each destination document. Sass nesting and repeated classes change specificity; use them only to preserve an established override. Keep selector relationships and scope intact. Use `:global()` for supported public classes, and justify specificity changes with the conflicting declaration. Do not depend on stylesheet import order. + +### Styles across documents and verification + +`StyleProvider` registers a document with `@wordpress/style-runtime` for CSS Module delivery. Preserve its insertion-effect timing and cleanup. Applications rendering their own Emotion styles in another document must configure their own cache or styling instance. See [styles across documents](/packages/theme/README.md#across-documents-iframes-and-other-portals) for the destination stylesheet and cache requirements. + +Start with existing tests and stories. Use Node or jsdom for DOM structure, semantics, props, refs, and state. Use Browser Mode for real styles, specificity, custom-property resolution, layout, focus, and scrolling. Load the styles that the consumer uses. Class assertions and snapshots alone do not prove that CSS loads or wins. + +Check the states and consumers affected by the change, including portals, iframes, RTL, consumer overrides, and nested instances when relevant. For cross-document behavior, use an attached iframe with a real document and assert a computed style there. Follow the [testing overview](/docs/contributors/code/testing-overview.md#folder-structure), and add coverage only for a concrete risk that existing tests do not protect. ### Deprecating styles diff --git a/packages/components/emotion-to-scss-modules.md b/packages/components/emotion-to-scss-modules.md deleted file mode 100644 index e24d614b6da655..00000000000000 --- a/packages/components/emotion-to-scss-modules.md +++ /dev/null @@ -1,146 +0,0 @@ -# Migrating components from Emotion to SCSS Modules - -An Emotion migration should preserve the component's public API, rendered elements, interactions, and resulting styles. Use the existing SCSS Modules and `@wordpress/style-runtime` pipeline. A component migration does not require a new CSS extraction pipeline. - -The component source migration is complete. This guide records the [migration guidelines in #66806](https://github.com/WordPress/gutenberg/issues/66806) and lessons from its merged PRs. The [reference migrations](#reference-migrations) identify useful examples and their limitations. Read the current implementation before copying an older diff. Some merged PRs include explicitly agreed visual or behavior changes that are not part of a mechanical migration. - -## Establish the component and consumer contract - -Before editing, read the component's types, README, stories, style hooks, and composed primitives. Search for consumers of its exported wrappers, style fragments, and public classes, including outside `packages/components`. A shared style file can serve several components, as with `ColorPalette` and `GradientPicker`. - -Record the behavior that needs comparison: - -- Default rendered element, supported `as` targets, refs, prop forwarding, and intrinsic HTML/SVG prop filtering. -- Existing public classes and which element adds each one. `useContextSystem` may already supply the root class. Keep legacy spellings, including singular `component-*` names. -- Consumer `className` and `style` handling, including spread order and conditional forwarding. -- Defaults, variants, nested instances, focus and disabled states, directional styles, and animation state changes. -- Selectors that override the component and selectors the component uses to override its children. -- Portal and iframe paths, including the document that receives styles. - -Capture a focused baseline in Storybook or the editor for the states and consumers at risk. If a regression motivates the migration, reproduce it before changing the implementation. An unchanged isolated story cannot establish compatibility for a consumer that adds its own styles. - -### Replace wrappers without changing their role - -For `styled( ExistingComponent )`, apply the module class to that same component when it already supplies the needed behavior. Do not replace `Flex`, `Heading`, `Text`, or an input control with a native element just because its wrapper only contained CSS. - -For a wrapper that relied directly on Emotion's `as` support, inspect and reuse [`PolymorphicElement`](/packages/components/src/utils/polymorphic-element.tsx). Preserve the original default tag and the component's existing ref and prop contract. `View` already delegates to this utility. A fixed internal element with no exposed polymorphic path can remain a plain element. - -Check the current [`WordPressComponent` types](/packages/components/src/context/wordpress-component.ts) instead of introducing another polymorphic type system. [#80705](https://github.com/WordPress/gutenberg/pull/80705) deliberately reduced intrinsic-prop type complexity to control compilation cost. Removing Emotion does not remove the compatibility obligation for `as`. - -Keep shared wrappers' private module classes separate from public classes added by callers. In [#80001](https://github.com/WordPress/gutenberg/pull/80001), adding BaseControl label/help classes inside shared wrappers made unrelated consumers match BaseControl selectors; adding classes on both sides also produced duplicates. Test a real `as="legend"` consumer when migrating shared labels, including its visible and visually hidden branches. - -`View`'s legacy `css` prop is an explicit exception established by [#79443](https://github.com/WordPress/gutenberg/pull/79443). It remains accepted as a no-op and is consumed before rendering. Do not restore its styling behavior, forward it to the DOM, or treat that decision as permission to remove other props. - -## Translate the styles - -Follow the package's [styling conventions](/packages/components/CONTRIBUTING.md#styling). Put static declarations in a colocated `style.module.scss`, use kebab-case classes, and compose variants with `clsx`. Use existing semantic attributes and pseudo-classes where appropriate; do not add data attributes solely to select styles. - -Name modifier classes for their state and make their owning element clear. When scoping a modifier under a base class, account for the extra specificity. A standalone `.is-active` and `.root.is-active` do not have equal specificity. - -Keep fixed values and calculations in Sass when JavaScript does not need them. `ProgressBar` moved its constant indeterminate indicator width into Sass and shared it with the keyframes. CSS Module keyframes can have descriptive local names without component prefixes. - -### Preserve dynamic values and overrides - -Prefer inline custom properties for genuinely dynamic values, with the actual property declared in the stylesheet. Emotion adds units to many numeric CSS values; custom properties do not. Preserve the original conversion, such as `space( value )` or an explicit `px` suffix, and keep zero distinct from an absent value. - -Check these differences before choosing the representation: - -- **Cascade.** Moving `z-index` from a CSS rule to `style.zIndex` prevents ordinary consumer CSS from overriding it. [ZStack's review](https://github.com/WordPress/gutenberg/pull/80514#discussion_r3845479235) led to passing the number through a custom property and keeping `z-index` in the module rule. -- **Precedence.** Preserve consumer `style` values and the component's existing override rules. Do not copy a universal spread order from another migration. `Spacer` lets consumer custom properties win; `Flex` supplies its internal variables after the consumer style object. Ordinary inline CSS properties still override stylesheet declarations. -- **Inheritance.** Internal custom properties inherit unless reset. Reset them on each instance when nested components must not inherit a parent's values, as in `Spacer`. Do not reset intentionally inherited theme variables. -- **Shorthands.** A later longhand with an absent custom property can undo the shorthand result. Preserve the fallback and precedence of all-sides, axis, and individual-side values. Check shorthand-only, longhand-only, and combined inputs, including supported CSS strings. The `Spacer` review caught missing longhand fallbacks. -- **Accepted CSS values.** CSS-wide keywords apply to a custom property itself. `--style: inherit; border-style: var(--style, solid)` does not mean `border-style: inherit`. In [#80437](https://github.com/WordPress/gutenberg/pull/80437#discussion_r3608442254), the indicator retained direct `borderStyle` and `borderColor` values and the original `color || fallback` behavior. Check empty strings and CSS-wide values when the API accepts arbitrary CSS. Direct inline properties can be appropriate, but assess their effect on consumer overrides explicitly. - -### Preserve direction, tokens, and state changes - -Use logical properties where they preserve the intended behavior. Compare LTR and RTL, and relevant writing modes, before changing a physical property. `Surface` exposes physical border props; `TextareaControl` retains `resize: vertical`; `DropdownContentWrapper` offsets the Popover's physical padding. These are reasons for narrow lint suppressions, not a blanket ban on logical properties. `padding: 0` also resets more than `padding-inline: 0`. - -Preserve necessary browser declarations and pseudo-elements. Examples include Truncate's `-webkit-box-orient: vertical`, SearchControl's WebKit search decorations, and TextareaControl's placeholder selectors and global dark-theme ancestor. - -Prefer equivalent WPDS tokens and existing Sass variables or mixins. Check what they expand to, including fallback values and validated-control accent overrides. A similarly named token can change the color or geometry. Keep intentional visual changes explicit, as in `Surface` and `ToggleGroupControl`, instead of presenting them as style parity. - -Compare transitions between states as well as static states. [ProgressBar's review](https://github.com/WordPress/gutenberg/pull/80512#discussion_r3830324726) caught a width transition newly applying when switching to indeterminate mode. Preserve the original animation direction, duration, transition conditions, reduced-motion rules, and forced-colors treatment where present. - -## Preserve the winning declarations - -CSS Module scoping prevents class-name collisions. It does not control specificity or stylesheet insertion order. The order of class names passed to `clsx` does not control the cascade either. - -Compare matched declarations on the component and its consumers: - -- Keep low-specificity defaults overridable. The Divider migration added attribute-selector specificity and broke `CardDivider` and complementary-area border overrides. [#79534](https://github.com/WordPress/gutenberg/pull/79534) wrapped orientation attributes in `:where()` to remove that extra specificity. -- Preserve deliberate Emotion `&&` or `&&&` increases. Sass can express these as `.wrapper.wrapper` or `.label.label.label`; raw `&&&` is not valid Dart Sass. ConfirmDialog's doubled selector keeps its overlay above the parent Popover. -- Preserve declarations that had to override primitives during the staged migration. ColorPalette initially protected `font-size` and margin but missed `line-height`; Heading's later Emotion rule won in the root document. ToolsPanel had a similar heading conflict. Check current consumers before reducing the resulting specificity. -- Keep selector relationships and scope. Nesting `.layered > .child` under `.z-stack` adds specificity. Replacing Emotion component interpolation requires checking which real elements the new selector matches. -- Mark existing external classes with `:global(...)` inside a CSS Module, for example `:global(.components-panel__row) &`. Do not accidentally hash a legacy class, broaden its matching elements, or add a public class solely to replace an internal selector. - -Use the smallest selector change that preserves the required winner. Do not put every rule in `:where()`, increase every selector, or add `!important` to hide an unexplained conflict. Record why a deliberate specificity adjustment is needed. - -### Preserve external Emotion composition - -Earlier migration stages used `useCx` to combine component and consumer classes. That hook has been removed, and the context system now composes class names with `clsx`. Consumers that still author Emotion styles must combine source-order-dependent fragments in one `css()` call before passing the resulting class name to a component: - -```js -import { css } from '@emotion/css'; - -const className = css( baseStyles, condition && overrideStyles ); -``` - -This is compatibility guidance for external Emotion consumers. New component styles use SCSS Modules. Do not mechanically combine unrelated fragments. Audit downstream callers when changing composition: [#79443](https://github.com/WordPress/gutenberg/pull/79443) updated several consumers but missed the BorderBoxControl gutter regression fixed in [#79967](https://github.com/WordPress/gutenberg/pull/79967). - -Test mixed Emotion/Module composition in both the root and iframe documents when used there. [Scrollable's review](https://github.com/WordPress/gutenberg/pull/80694#discussion_r3845389532) found that `CardBody isScrollable` kept `height: 100%` in an iframe but resolved to `auto` in the root document because insertion order differed. - -## Verify CSS delivery and behavior - -Reuse existing tests and stories first. Add or extend a test only for a concrete migration risk that existing coverage does not protect. Choose the smallest check that observes the behavior and would fail if it regressed. A behavior-preserving migration may need no new tests. When automated tests cannot observe the real styling path, document a focused before/after browser check instead of adding a test that cannot detect the regression. - -[`StyleProvider`](/packages/components/src/style-provider/index.tsx) registers a document with `@wordpress/style-runtime`. Keep this registration path for CSS Module delivery, including its `useInsertionEffect` timing and cleanup. The provider no longer supplies an Emotion cache. Consumers using `@emotion/react` or `@emotion/styled` in another document must provide their own `CacheProvider` with a cache whose `container` is that document's ``. Consumers using `@emotion/css` must create an instance with `@emotion/css/create-instance`, set its `container` to that document's ``, and use the instance's styling functions. See [Emotion's package summary](https://emotion.sh/docs/package-summary) and [custom-instance guidance](https://emotion.sh/docs/@emotion/css#custom-instances). - -For SlotFill or portals, use an attached iframe with a real `contentDocument` and `defaultView`. Assert a computed style in the destination document. A class on the element or a style tag in the parent document does not prove delivery. [#80384](https://github.com/WordPress/gutenberg/pull/80384) pairs a registration test with a Storybook example that renders migrated Spacer styles in an iframe. - -Match the evidence to the behavior at risk: - -| What needs proof | Suitable evidence | -| --- | --- | -| DOM, public classes, refs, props, variants, and interactions | Existing component tests; extend them only for an uncovered integration risk. Assert the actual styled node, including the outer Grid child when an inner wrapper has the same class. | -| Real SCSS declarations, specificity, custom-property resolution, dimensions, and animation | Storybook or editor comparison using production styles, computed styles, and relevant interactions. Include a consumer override and the affected document/RTL states. | -| Cross-document registration or a particular mixed-style composition mechanism | Reuse shared registration coverage. Add a component-specific test only for an uncovered registration or composition risk. A small fixture stylesheet can test that mechanism; pair it with a real-style check. | - -The Node and jsdom projects mock stylesheet imports; Browser Mode loads the real styles. Public-class assertions can protect a compatibility contract; private module-class assertions establish wiring only. Neither proves that the real CSS loads or wins. Do not add class assertions or snapshots solely to mirror the implementation. Do not inject the desired production selector into a test and claim it protects that selector: the test would still pass if the SCSS rule were deleted. This distinction led to removing tests in [#81792](https://github.com/WordPress/gutenberg/pull/81792#discussion_r3811746653). - -Keep new tests focused on the migrated component's integration. A wrapper test can protect its `as`, ref, or prop forwarding even when the shared helper is already tested. Do not copy PolymorphicElement's entire filtering suite into every wrapper, duplicate a full editor setup for a small assertion, or introduce a new Storybook testing convention just for the migration. Check the relevant states and combinations from the contract audit; do not generate every prop combination or supported tag. When a real-style assertion is unavailable, state the gap and give reproducible manual steps. - -## Complete the migration - -Remove obsolete Emotion imports, files, lint/type suppressions, and utilities that become unused because of this migration. Search all consumers before deleting shared constants. Preserve useful comments explaining layout calculations or compatibility workarounds. The compiler transforms, `useCx`, and Emotion `rtl` utility have been removed. Audit direct and transitive dependencies across workspaces before removing remaining package declarations or lockfile entries. Leave unrelated cleanup outside the migration. - -Follow [worktree setup](/docs/contributors/code/getting-started-with-code-contribution.md#set-up-each-worktree) and read the [testing overview](/docs/contributors/code/testing-overview.md) before adding tests. All unit tests use Vitest. Follow the [filename-based environment conventions](/docs/contributors/code/testing-overview.md#folder-structure). From the repository root, use the appropriate focused command below, then run lint and the repository gates: - -```sh -npm run test:unit -- -npm run lint:js -- -npm run lint:css -npm run typecheck -npm run build -npm run test:unit:update -- -git diff --check -``` - -Update snapshots for the migrated component and affected downstream consumers. Omit the path only when the change requires a repository-wide snapshot update. Review every snapshot change for removed or duplicated public classes, changed elements, forwarded props, and consumer output. Do not accept a snapshot solely because Emotion classes disappeared. Rerun the exact failing file or shard before attributing a broad failure to the migration. - -For generated-file verification, start from a clean committed worktree and run the applicable generators and suppression updates. `npm run other:check-local-changes` runs the documentation and theme generators through its npm prehook, then checks the unstaged diff. It does not run every generator or suppression update; consult the current [static-checks workflow](/.github/workflows/static-checks.yml) for those required by the migration. Inspect generated changes before staging them, since staged changes are invisible to the checker. Commit required updates, then repeat the applicable generation and check commands from the clean worktree. - -Recheck the final selectors and consumers after the last edit or rebase; a previous visual check does not cover a later specificity change. Preserve newer trunk changes, such as focus-ring updates, when moving declarations out of an old style file. - -Follow [package changelog guidance](/docs/contributors/code/managing-packages.md). For production migrations, use the current unreleased section and this PR's link. The merged migrations record the `cx()` composition limitation under **Breaking Changes**, even when normal component usage is visually unchanged. Do not copy an early pilot's `Internal` entry as the only release note. Record any separately agreed behavior change and its verification independently, as #80715 did for Reset focus retention. - -## Reference migrations - -Choose an example for the risk in the current migration, then check it against current source. The full migration history is maintained in [#66806](https://github.com/WordPress/gutenberg/issues/66806). - -| Migration risk | Merged example | What to inspect | -| --- | --- | --- | -| Polymorphic wrappers | View, [#79443](https://github.com/WordPress/gutenberg/pull/79443) | Shared polymorphic replacement, legacy `css` no-op, and downstream consumers. | -| Dynamic spacing | Spacer, [#79449](https://github.com/WordPress/gutenberg/pull/79449) | Nested custom-property resets and shorthand/longhand fallback. | -| Selector specificity | Divider, [#79444](https://github.com/WordPress/gutenberg/pull/79444), and [#79534](https://github.com/WordPress/gutenberg/pull/79534) | Orientation selectors and the correction that restored consumer overrides. | -| Mixed Emotion composition | Border controls, [#80437](https://github.com/WordPress/gutenberg/pull/80437) | Overrides, CSS-wide values, and the linked-control gutter. | -| Root and iframe delivery | Scrollable, [#80694](https://github.com/WordPress/gutenberg/pull/80694) | CardBody height and stylesheet insertion order in each document. | diff --git a/packages/components/src/border-control/style.module.scss b/packages/components/src/border-control/style.module.scss index 12f47d1127c06d..c1f58f69023b08 100644 --- a/packages/components/src/border-control/style.module.scss +++ b/packages/components/src/border-control/style.module.scss @@ -20,7 +20,7 @@ flex: 1 1 40%; } - /* Preserve the specificity that overrode UnitSelect's former Emotion min-height. */ + /* Preserve the established specificity for UnitSelect's min-height override. */ &#{&} :global(.components-unit-control__select):global(.components-unit-control__select) { min-height: 0; @@ -37,7 +37,7 @@ .dropdown { background: #fff; - /* Preserve the specificity that overrode the former Emotion Button base styles. */ + /* Preserve the established specificity for Button overrides. */ &#{&} > button { aspect-ratio: 1; padding: 0; diff --git a/packages/components/src/color-palette/style.module.scss b/packages/components/src/color-palette/style.module.scss index 4456536c64e7f7..ce7a841295853a 100644 --- a/packages/components/src/color-palette/style.module.scss +++ b/packages/components/src/color-palette/style.module.scss @@ -1,9 +1,8 @@ .color-heading { text-transform: uppercase; - // Triple specificity to override the base Heading styles, matching the - // pre-migration Emotion `&&&` behavior. Dart Sass disallows `&&&`, so the - // selector is repeated via interpolation to produce the same specificity. + // Triple specificity overrides the base Heading styles. Repeat the selector + // via interpolation because Dart Sass disallows `&&&`. &#{&}#{&} { font-size: 11px; line-height: 24px; diff --git a/packages/components/src/confirm-dialog/style.module.scss b/packages/components/src/confirm-dialog/style.module.scss index e93f4c8bfaef2a..8b9e58d6d282fa 100644 --- a/packages/components/src/confirm-dialog/style.module.scss +++ b/packages/components/src/confirm-dialog/style.module.scss @@ -1,6 +1,6 @@ @use "@wordpress/base-styles/z-index" as *; -// Match the previous Emotion `&&` specificity so this rule continues to +// Preserve the established specificity so this rule continues to // override the Modal overlay's default z-index regardless of source order. .wrapper.wrapper { z-index: z-index(".components-confirm-dialog"); diff --git a/packages/components/src/custom-select-control-v2/style.module.scss b/packages/components/src/custom-select-control-v2/style.module.scss index 611916f18c4a3a..bef5d49c274f7c 100644 --- a/packages/components/src/custom-select-control-v2/style.module.scss +++ b/packages/components/src/custom-select-control-v2/style.module.scss @@ -2,7 +2,7 @@ @use "@wordpress/base-styles/breakpoints" as *; @use "../utils/theme-variables" as *; -// Variant selectors use :where() to retain the single-class Emotion specificity. +// Variant selectors use :where() to retain single-class specificity. .select { display: block; background-color: $components-color-background; diff --git a/packages/components/src/date-time/time-picker/style.module.scss b/packages/components/src/date-time/time-picker/style.module.scss index 0e96d03b862d78..ddcce117c6f965 100644 --- a/packages/components/src/date-time/time-picker/style.module.scss +++ b/packages/components/src/date-time/time-picker/style.module.scss @@ -26,7 +26,7 @@ .minutes-input.minutes-input.minutes-input, .day-input.day-input.day-input, .year-input.year-input.year-input { - // Preserve the specificity that overrode InputControl's former Emotion styles. + // Preserve the established specificity for InputControl overrides. :global(.components-input-control__input) { /* stylelint-disable logical-css/require-logical-properties -- Time fields remain physically LTR, including in RTL locales. */ padding-left: $grid-unit-10; diff --git a/packages/components/src/number-control/style.module.scss b/packages/components/src/number-control/style.module.scss index 67d168c39b12c1..b86fe8e1fad40d 100644 --- a/packages/components/src/number-control/style.module.scss +++ b/packages/components/src/number-control/style.module.scss @@ -14,7 +14,7 @@ } .spin-button { - // Preserve the specificity of the Emotion wrapper over Button styles. + // Preserve the established specificity over Button styles. &.spin-button.spin-button.spin-button.spin-button { color: $components-color-accent; } diff --git a/packages/components/src/palette-edit/style.module.scss b/packages/components/src/palette-edit/style.module.scss index 413d4ca5032ce7..e0ecf1e42d8465 100644 --- a/packages/components/src/palette-edit/style.module.scss +++ b/packages/components/src/palette-edit/style.module.scss @@ -13,7 +13,7 @@ background: $gray-100; border-radius: $radius-x-small; - // Preserve the specificity of the previous Emotion component selectors. + // Preserve the established specificity for InputControl overrides. :global( .components-input-control__input.components-input-control__input.components-input-control__input.components-input-control__input ) { diff --git a/packages/components/src/range-control/utils.ts b/packages/components/src/range-control/utils.ts index af9bfa8489856f..71037db2666b3f 100644 --- a/packages/components/src/range-control/utils.ts +++ b/packages/components/src/range-control/utils.ts @@ -90,7 +90,7 @@ export function getCSSWideKeyword( value: CSSProperties[ 'color' ] ) { : 0xfffd ); } - return character ?? ''; // Emotion removed comments before parsing values. + return character ?? ''; // Remove CSS comments before matching keywords. } ) .replace( /^[ \t\n\r\f]+|[ \t\n\r\f]+$/g, '' ) diff --git a/packages/components/src/resizable-box/resize-tooltip/style.module.scss b/packages/components/src/resizable-box/resize-tooltip/style.module.scss index c0e2ddf12c7d8a..0bc2ba515c171c 100644 --- a/packages/components/src/resizable-box/resize-tooltip/style.module.scss +++ b/packages/components/src/resizable-box/resize-tooltip/style.module.scss @@ -29,7 +29,7 @@ position: relative; } -// Match the previous Emotion `&&&` specificity so label styles continue to +// Preserve the established specificity so label styles continue to // override Text / consumer typography. .label-text.label-text.label-text { color: $components-color-foreground-inverted; diff --git a/packages/components/src/textarea-control/style.module.scss b/packages/components/src/textarea-control/style.module.scss index 48413b3561cac2..75c2faa8987333 100644 --- a/packages/components/src/textarea-control/style.module.scss +++ b/packages/components/src/textarea-control/style.module.scss @@ -6,7 +6,7 @@ display: block; background: $components-color-background; color: $components-color-foreground; - /* stylelint-disable-next-line logical-css/require-logical-keywords -- Preserve Emotion `resize: vertical`; `block` is not equivalent in vertical writing modes. */ + /* stylelint-disable-next-line logical-css/require-logical-keywords -- Preserve `resize: vertical`; `block` is not equivalent in vertical writing modes. */ resize: vertical; @include input-control; @@ -28,7 +28,7 @@ } // Use opacity to work in various editor styles. - // Keep the legacy vendor placeholders from the Emotion styles for parity + // Keep the supported vendor placeholder selectors. &::-webkit-input-placeholder { color: $components-color-dark-gray-placeholder; } diff --git a/packages/components/src/tools-panel/style.module.scss b/packages/components/src/tools-panel/style.module.scss index 1fcdb8b901883c..b16cc85f2ccae8 100644 --- a/packages/components/src/tools-panel/style.module.scss +++ b/packages/components/src/tools-panel/style.module.scss @@ -36,7 +36,7 @@ } } -// Match the previous Emotion `&&&&` specificity so these styles continue to +// Preserve the established specificity so these styles continue to // override the DropdownMenu toggle defaults. .tools-panel-header.tools-panel-header.tools-panel-header.tools-panel-header { :global(.components-dropdown-menu__toggle) { @@ -65,8 +65,7 @@ } } -// Match the previous Emotion `&&` specificity and target BaseControl's -// preserved public class names instead of its generated Emotion classes. +// Preserve the established specificity and target BaseControl's public classes. .tools-panel-item.tools-panel-item :global(.components-base-control) { margin-bottom: 0; @@ -101,7 +100,7 @@ color: $gray-900; } -// Match the previous Emotion `&&` specificity so disabled MenuItem styles +// Preserve the established specificity so disabled MenuItem styles // continue to override its defaults. .default-controls-item.default-controls-item[aria-disabled="true"] { color: $gray-700; diff --git a/packages/components/src/unit-control/style.module.scss b/packages/components/src/unit-control/style.module.scss index 2d49f9e438a6a8..486f8888127730 100644 --- a/packages/components/src/unit-control/style.module.scss +++ b/packages/components/src/unit-control/style.module.scss @@ -60,7 +60,7 @@ } } -// Match the Emotion &&& selector, which counteracts wp-admin forms.css. +// Triple specificity counteracts wp-admin forms.css. .unit-select.unit-select.unit-select { appearance: none; background: transparent; diff --git a/packages/components/src/z-stack/style.module.scss b/packages/components/src/z-stack/style.module.scss index 7aac3a4c3af844..5a99c989293ffa 100644 --- a/packages/components/src/z-stack/style.module.scss +++ b/packages/components/src/z-stack/style.module.scss @@ -11,7 +11,7 @@ // When the ZStack is layered, all items overlap in the same grid cell. // Kept at the top level so that the selector retains the same specificity -// as `.z-stack > .child` above, matching the original Emotion styles. +// as `.z-stack > .child` above. .layered > .child { grid-row-start: 1; grid-column-start: 1; diff --git a/packages/scripts/docs/vitest-migration.md b/packages/scripts/docs/vitest-migration.md index c892d67d685daa..2a4d452d390930 100644 --- a/packages/scripts/docs/vitest-migration.md +++ b/packages/scripts/docs/vitest-migration.md @@ -205,7 +205,7 @@ export default defineConfig( { } ); ``` -Vitest discovers `vitest.config.*` before `vite.config.*`. `--config` takes precedence over both. To reuse Vite plugins with a separate test config, use [`mergeConfig`](https://vitest.dev/config/). Babel and Jest transforms do not carry over. Rename JSX-bearing `.js` files to `.jsx`, and configure the React Vite plugin when testing React or Emotion source. +Vitest discovers `vitest.config.*` before `vite.config.*`. `--config` takes precedence over both. To reuse Vite plugins with a separate test config, use [`mergeConfig`](https://vitest.dev/config/). Babel and Jest transforms do not carry over. Rename JSX-bearing `.js` files to `.jsx`, and configure the React Vite plugin when testing React source. ## Opt into jsdom @@ -328,7 +328,7 @@ A `.jsdom.test.*` or `.browser.test.*` filename alone does not select an environ - Import `describe`, `test`, `expect`, hooks, and `vi` from `vitest`. Replace Jest mocks using the [Jest migration guide](https://vitest.dev/guide/migration/jest). Mock factories, module reset behavior, and timers can differ. - Convert `testMatch` to `test.include`, `moduleNameMapper` to Vite aliases, and `setupFilesAfterEnv` to `test.setupFiles`. Choose the required cleanup options explicitly. Vitest's defaults do not reproduce the WordPress Jest preset. - Replace `@wordpress/jest-console` matchers with local `vi.spyOn( console, 'error' )` assertions and restore the spy. Unexpected console output does not fail a Vitest test by default. If your suite requires that contract, implement and test it in your own setup. Do not import Gutenberg's console setup. -- Review snapshots when converting them. Keep runner-neutral `@testing-library/jest-dom` and snapshot-diff matchers where needed. +- Review snapshots when converting them, and prefer direct assertions for the behavior that changes between states. Runner-neutral `@testing-library/jest-dom` matchers remain supported. - Use `--update` for snapshots and `--no-file-parallelism` for debugging. Jest options such as `--runInBand`, `--updateSnapshot`, and `--ci` are not interchangeable with Vitest options. - The public `test-unit` ESLint config uses `@vitest/eslint-plugin` recommended rules without declaring globals. Projects retaining Jest should install `eslint-plugin-jest` and select its `flat/recommended` config explicitly. From 407ef78002ebc63e0efeebd040c53369b1341568 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sat, 17 Oct 2026 00:15:50 +0200 Subject: [PATCH 2/4] Components: Correct the DOM test environment guidance --- packages/components/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index 32f8567713cd5d..20b88b74cf884e 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -444,7 +444,7 @@ Check the declarations that win in composed components and in each destination d `StyleProvider` registers a document with `@wordpress/style-runtime` for CSS Module delivery. Preserve its insertion-effect timing and cleanup. Applications rendering their own Emotion styles in another document must configure their own cache or styling instance. See [styles across documents](/packages/theme/README.md#across-documents-iframes-and-other-portals) for the destination stylesheet and cache requirements. -Start with existing tests and stories. Use Node or jsdom for DOM structure, semantics, props, refs, and state. Use Browser Mode for real styles, specificity, custom-property resolution, layout, focus, and scrolling. Load the styles that the consumer uses. Class assertions and snapshots alone do not prove that CSS loads or wins. +Start with existing tests and stories. Use jsdom for DOM structure, semantics, props, refs, and state. Use Browser Mode for real styles, specificity, custom-property resolution, layout, focus, and scrolling. Load the styles that the consumer uses. Class assertions and snapshots alone do not prove that CSS loads or wins. Check the states and consumers affected by the change, including portals, iframes, RTL, consumer overrides, and nested instances when relevant. For cross-document behavior, use an attached iframe with a real document and assert a computed style there. Follow the [testing overview](/docs/contributors/code/testing-overview.md#folder-structure), and add coverage only for a concrete risk that existing tests do not protect. From 3c8cffb1d5277cbd854184d4301e4f02a6a3b26b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 19 Oct 2026 11:55:27 +0200 Subject: [PATCH 3/4] Components: Address Emotion migration guidance feedback --- packages/components/CHANGELOG.md | 1 + packages/components/src/textarea-control/style.module.scss | 2 +- packages/scripts/docs/vitest-migration.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 08a99c0d8ed2cd..1d417afcbdbbb6 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -71,6 +71,7 @@ ### Documentation +- Retire completed Emotion migration guidance and keep permanent styling advice in the contribution guide ([#85023](https://github.com/WordPress/gutenberg/pull/85023)). - Update styling guidance and the `ToolsPanel` example after the Emotion migration ([#85020](https://github.com/WordPress/gutenberg/pull/85020)). ## 42.0.0 (2026-10-07) diff --git a/packages/components/src/textarea-control/style.module.scss b/packages/components/src/textarea-control/style.module.scss index 75c2faa8987333..ec5dba1c0e745b 100644 --- a/packages/components/src/textarea-control/style.module.scss +++ b/packages/components/src/textarea-control/style.module.scss @@ -28,7 +28,7 @@ } // Use opacity to work in various editor styles. - // Keep the supported vendor placeholder selectors. + // Keep the legacy vendor placeholder selectors. &::-webkit-input-placeholder { color: $components-color-dark-gray-placeholder; } diff --git a/packages/scripts/docs/vitest-migration.md b/packages/scripts/docs/vitest-migration.md index 2a4d452d390930..02827460c945cd 100644 --- a/packages/scripts/docs/vitest-migration.md +++ b/packages/scripts/docs/vitest-migration.md @@ -205,7 +205,7 @@ export default defineConfig( { } ); ``` -Vitest discovers `vitest.config.*` before `vite.config.*`. `--config` takes precedence over both. To reuse Vite plugins with a separate test config, use [`mergeConfig`](https://vitest.dev/config/). Babel and Jest transforms do not carry over. Rename JSX-bearing `.js` files to `.jsx`, and configure the React Vite plugin when testing React source. +Vitest discovers `vitest.config.*` before `vite.config.*`. `--config` takes precedence over both. To reuse Vite plugins with a separate test config, use [`mergeConfig`](https://vitest.dev/config/). Babel and Jest transforms do not carry over. Rename JSX-bearing `.js` files to `.jsx`, and configure the React Vite plugin when testing React or Emotion source. ## Opt into jsdom From 22f2bc6a83cfb92cf46ddbc4c3dc79cd337ed1c6 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 19 Oct 2026 12:05:14 +0200 Subject: [PATCH 4/4] Components: Trim styling contribution guidance --- packages/components/CONTRIBUTING.md | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index 20b88b74cf884e..1941a69cae7876 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -422,31 +422,9 @@ Place component-local styles in a `style.module.scss` file next to the component Use `--_wp-components-*` for new private custom properties. These are implementation details, not public theming APIs. Keep supported customization in component props and existing public styles. -Components use SCSS Modules for styling. Do not add Emotion styling imports. The completed migration and its release notes are tracked in [#66806](https://github.com/WordPress/gutenberg/issues/66806). +Components no longer use Emotion. Do not add new Emotion imports. -### Preserve styling contracts - -Before changing styles, inspect the component's types, README, stories, and consumers. Keep the existing rendered element, `as` behavior, refs, prop forwarding, public classes, and consumer `className` and `style` handling. A styled wrapper around a shared component may also supply layout or accessibility behavior. Keep that component when replacing its styles. - -Use [`PolymorphicElement`](./src/utils/polymorphic-element.tsx) for existing polymorphic components that need intrinsic HTML/SVG prop filtering. Reuse the [`WordPressComponent` types](./src/context/wordpress-component.ts) instead of adding another polymorphic type system. `View` continues to accept its legacy `css` prop as a no-op and consumes it before rendering. - -Keep private module classes separate from public classes supplied by callers. Check real consumers of shared labels and wrappers, including visually hidden content and `as="legend"`. - -### Dynamic values and the cascade - -Keep fixed values and calculations in Sass when JavaScript does not need them. For dynamic custom properties, supply units explicitly where required and distinguish zero from an absent value. Custom properties inherit, so reset instance-specific values when nested components must not inherit them. - -Preserve the component's existing precedence for consumer styles, shorthands, longhands, and per-axis values. CSS-wide keywords such as `inherit` and `initial` apply to a custom property itself, so passing them through `var()` does not preserve their meaning for another CSS property. Moving a declaration directly to `style` also changes whether consumer stylesheet rules can override it. - -Check the declarations that win in composed components and in each destination document. Sass nesting and repeated classes change specificity; use them only to preserve an established override. Keep selector relationships and scope intact. Use `:global()` for supported public classes, and justify specificity changes with the conflicting declaration. Do not depend on stylesheet import order. - -### Styles across documents and verification - -`StyleProvider` registers a document with `@wordpress/style-runtime` for CSS Module delivery. Preserve its insertion-effect timing and cleanup. Applications rendering their own Emotion styles in another document must configure their own cache or styling instance. See [styles across documents](/packages/theme/README.md#across-documents-iframes-and-other-portals) for the destination stylesheet and cache requirements. - -Start with existing tests and stories. Use jsdom for DOM structure, semantics, props, refs, and state. Use Browser Mode for real styles, specificity, custom-property resolution, layout, focus, and scrolling. Load the styles that the consumer uses. Class assertions and snapshots alone do not prove that CSS loads or wins. - -Check the states and consumers affected by the change, including portals, iframes, RTL, consumer overrides, and nested instances when relevant. For cross-document behavior, use an attached iframe with a real document and assert a computed style there. Follow the [testing overview](/docs/contributors/code/testing-overview.md#folder-structure), and add coverage only for a concrete risk that existing tests do not protect. +For styling compatibility and verification, follow [Change a package safely](/docs/contributors/design/design-system-packages.md#change-a-package-safely) and the [cross-document setup guidance](/packages/theme/README.md#across-documents-iframes-and-other-portals). ### Deprecating styles