Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Breaking Changes

- Components no longer use Emotion for styling, class-name composition, or cross-document style injection. Consumers that compose source-order-dependent Emotion fragments must combine them in a single `css()` call before passing the resulting `className`. Consumers using `@emotion/react` or `@emotion/styled` that relied on StyleProvider's cross-document cache must provide their own cache and `CacheProvider`; `StyleProvider` continues to register CSS Module styles. This includes:
- Components no longer use Emotion for styling, class-name composition, or cross-document style injection. Consumers that compose source-order-dependent Emotion fragments must combine them in a single `css()` call before passing the resulting `className`. Consumers using `@emotion/react` or `@emotion/styled` that relied on StyleProvider's cross-document cache must provide their own cache targeting that document's `<head>` and wrap the subtree in `CacheProvider`; `StyleProvider` continues to register CSS Module styles. This includes:
- Context-system class-name composition ([#84925](https://github.com/WordPress/gutenberg/pull/84925)).
- `StyleProvider` cache removal ([#85019](https://github.com/WordPress/gutenberg/pull/85019)).
- `SelectControl` ([#84732](https://github.com/WordPress/gutenberg/pull/84732))
Expand Down Expand Up @@ -68,6 +68,10 @@
- Update Ariakit to 0.4.41 ([#84539](https://github.com/WordPress/gutenberg/pull/84539)).
- Refactor to TypeScript ([#84611](https://github.com/WordPress/gutenberg/pull/84611)).

### Documentation

- 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)

### Breaking Changes
Expand Down
8 changes: 4 additions & 4 deletions packages/components/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This set of guidelines should apply especially to newly introduced components. I
- [Unit tests](#unit-tests)
- [Storybook](#storybook)
- [Documentation](#documentation)
- [README example](#README-example)
- [README example](#readme-example)
- [Folder structure](#folder-structure)
- [Component versioning](#component-versioning)

Expand Down Expand Up @@ -422,9 +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.

Legacy components may still use Emotion while they are being migrated, but new Emotion usage should not be added.
Components no longer use Emotion. Do not add new Emotion imports.

Before migrating existing styles, read [Migrating components from Emotion to SCSS Modules](./emotion-to-scss-modules.md) for compatibility checks, cascade and custom-property pitfalls, verification, and examples from merged migrations.
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.

### Deprecating styles

Expand Down Expand Up @@ -619,7 +619,7 @@ Primary.args = {

A great tool to use when writing stories is the [Storybook Controls addon](https://storybook.js.org/addons/@storybook/addon-controls). Ideally props should be exposed by using this addon, which provides a graphical UI to interact dynamically with the component without needing to write code. Historically, we used [Knobs](https://storybook.js.org/addons/@storybook/addon-knobs), but it was deprecated and later removed in [#47152](https://github.com/WordPress/gutenberg/pull/47152).

The default value of each control should coincide with the default value of the props (i.e. it should be `undefined` if a prop is not required). A story should, therefore, also explicitly show how values from the Context System are applied to (sub)components. A good example of how this may look like is the [`Card` story](https://wordpress.github.io/gutenberg/?path=/story/components-card--default) (code [here](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/card/stories/index.story.tsx)).
The default value of each control should coincide with the default value of the props (i.e. it should be `undefined` if a prop is not required). A story should, therefore, also explicitly show how values from the Context System are applied to (sub)components. A good example of how this may look like is the [`Card` story](https://wordpress.github.io/gutenberg/?path=/story/components-card--default) ([source code](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/card/stories/index.story.tsx)).

Storybook can be started on a local machine by running `npm run storybook:dev`. Alternatively, the components' catalogue (up to date with the latest code on `trunk`) can be found at [wordpress.github.io/gutenberg/](https://wordpress.github.io/gutenberg/).

Expand Down
21 changes: 10 additions & 11 deletions packages/components/emotion-to-scss-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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.

This temporary guide collects 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.
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

Expand Down Expand Up @@ -69,32 +69,31 @@ 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.
- Protect every declaration that must beat a still-Emotion-based primitive. ColorPalette initially protected `font-size` and margin but missed `line-height`; Heading's later rule won in the root document. ToolsPanel had a similar heading conflict.
- 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.

### Audit remaining Emotion consumers
### Preserve external Emotion composition

Removing an Emotion wrapper can expose source-order dependencies in callers that still use `useCx`. Where fragments overlap on a property, shorthand/longhand pair, or nested selector, compose them in one `css()` call before passing them to `cx()`:
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
const classes = cx(
css( baseStyles, condition && overrideStyles ),
className
);
import { css } from '@emotion/css';

const className = css( baseStyles, condition && overrideStyles );
```

This is an interim compatibility fix for existing Emotion consumers, not a pattern for newly migrated styles. Do not mechanically combine unrelated fragments. Audit callers outside the migrated directory: [#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).
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) both supplies an Emotion cache and registers a document with `@wordpress/style-runtime`. Keep the relevant provider and registration path while either responsibility is needed. Removing a component's Emotion import does not make this infrastructure redundant.
[`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 `<head>`. Consumers using `@emotion/css` must create an instance with `@emotion/css/create-instance`, set its `container` to that document's `<head>`, 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.

Expand All @@ -112,7 +111,7 @@ Keep new tests focused on the migrated component's integration. A wrapper test c

## 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. Leave unrelated cleanup and global Emotion dependencies, Babel wiring, serializers, `useCx`, and the `rtl` utility until their remaining users are gone.
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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const BorderLabel = ( props: LabelProps & { id?: string } ) => {

// The visible label is rendered as `BaseControl.VisualLabel` so it carries
// the stable `.components-base-control__label` className consumers style
// against; `StyledLabel` is an emotion component with a generated one.
// against, preserving the visible label's public styling hook.
return hideLabelFromVision ? (
<VisuallyHidden as="span" id={ id }>
{ label }
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/border-control/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
flex: 1 1 40%;
}

/* Raised specificity to beat UnitSelect's runtime-injected Emotion min-height. */
/* Preserve the specificity that overrode UnitSelect's former Emotion min-height. */
&#{&}
:global(.components-unit-control__select):global(.components-unit-control__select) {
min-height: 0;
Expand All @@ -37,7 +37,7 @@
.dropdown {
background: #fff;

/* Raised specificity to stay ahead of the Emotion Button base styles. */
/* Preserve the specificity that overrode the former Emotion Button base styles. */
&#{&} > button {
aspect-ratio: 1;
padding: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
.minutes-input.minutes-input.minutes-input,
.day-input.day-input.day-input,
.year-input.year-input.year-input {
// Keep the original specificity over InputControl's Emotion styles.
// Preserve the specificity that overrode InputControl's former Emotion styles.
: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;
Expand Down
5 changes: 2 additions & 3 deletions packages/components/src/slot-fill/fill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ export default function Fill( { name, children }: FillComponentProps ) {
: children;

// When using a `Fill`, the `children` will be rendered in the document of the
// `Slot`. Wrap the children in a `StyleProvider` so CSS module styles and
// legacy Emotion styles target that document/iframe instead of the document
// containing the `Fill`.
// `Slot`. Wrap the children in a `StyleProvider` so CSS Module styles target
// that document/iframe instead of the document containing the `Fill`.
return createPortal(
<StyleProvider document={ portalEl.ownerDocument }>
{ wrappedChildren }
Expand Down
37 changes: 22 additions & 15 deletions packages/components/src/tools-panel/tools-panel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,15 @@ both columns.
## Usage

```jsx
import styled from '@emotion/styled';
import {
BoxControl,
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
__experimentalUnitControl as UnitControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

const PanelDescription = styled.div`
grid-column: span 2;
`;

const SingleColumnItem = styled( ToolsPanelItem )`
grid-column: span 1;
`;
import styles from './style.module.scss';

export function DimensionPanel() {
const [ height, setHeight ] = useState();
Expand All @@ -81,11 +74,12 @@ export function DimensionPanel() {

return (
<ToolsPanel label={ __( 'Dimensions' ) } resetAll={ resetAll }>
<PanelDescription>
<div className={ styles[ 'panel-description' ] }>
Select dimensions or spacing related settings from the menu for
additional controls.
</PanelDescription>
<SingleColumnItem
</div>
<ToolsPanelItem
className={ styles[ 'single-column-item' ] }
hasValue={ () => !! height }
label={ __( 'Height' ) }
onDeselect={ () => setHeight( undefined ) }
Expand All @@ -97,8 +91,9 @@ export function DimensionPanel() {
onChange={ setHeight }
value={ height }
/>
</SingleColumnItem>
<SingleColumnItem
</ToolsPanelItem>
<ToolsPanelItem
className={ styles[ 'single-column-item' ] }
hasValue={ () => !! width }
label={ __( 'Width' ) }
onDeselect={ () => setWidth( undefined ) }
Expand All @@ -110,7 +105,7 @@ export function DimensionPanel() {
onChange={ setWidth }
value={ width }
/>
</SingleColumnItem>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={ () => !! padding }
label={ __( 'Padding' ) }
Expand Down Expand Up @@ -140,6 +135,18 @@ export function DimensionPanel() {
}
```

Add these styles to `style.module.scss`. The repeated selector keeps the single-column override above ToolsPanelItem's default full-width rule, regardless of stylesheet order.

```scss
.panel-description {
grid-column: span 2;
}

.single-column-item.single-column-item {
grid-column: span 1;
}
```

## Props

### `hasInnerWrapper`: `boolean`
Expand Down
2 changes: 1 addition & 1 deletion packages/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ When you render React content into a different document (typically an iframe), t

For custom iframes, the consumer is responsible for loading it in the iframe document. Within WordPress, enqueue the `wp-theme` stylesheet for that document. Outside WordPress, import `@wordpress/theme/design-tokens.css` from a stylesheet that the iframe already loads, or inject the CSS string directly.

2. **Dynamically injected component styles are routed to the iframe document.** Some `@wordpress/components` styles are injected into the document at runtime rather than shipped as static CSS — for example Emotion-based styles, and styles from CSS modules built with `@wordpress/build`. `StyleProvider` tells that machinery which document's `<head>` to inject into. Wrap the iframe subtree in `<StyleProvider document={ iframeDocument }>`.
2. **CSS Module styles are registered in the iframe document.** Styles from CSS Modules built with `@wordpress/build` are injected at runtime through `@wordpress/style-runtime`. Wrap the iframe subtree in `<StyleProvider document={ iframeDocument }>` so registered styles reach that document's `<head>` before children measure layout.

The canonical pattern combines both with a `<ThemeProvider isRoot>` to apply any overrides to the iframe's `:root`:

Expand Down
10 changes: 8 additions & 2 deletions tools/eslint/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ export default dedupePlugins( [
},

// Override: Components src — allow ariakit/framer-motion imports and
// prevent new Emotion usage while existing styles are migrated.
// prevent Emotion imports from returning after the migration.
{
files: [ 'packages/components/src/**' ],
rules: {
Expand All @@ -1002,7 +1002,13 @@ export default dedupePlugins( [
name
) && ! name.startsWith( '@emotion/' )
),
patterns: [ '@emotion/**' ],
patterns: [
{
group: [ '@emotion/**' ],
message:
'Please use SCSS Modules for styles and `clsx` to compose class names.',
},
],
},
],
},
Expand Down
Loading