From 4e0cfad2d9cb2405196e2def99fca7bcf4355c8a Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 23 Jun 2026 16:32:29 +0200 Subject: [PATCH 1/9] Components: migrate Theme away from Emotion --- packages/components/src/theme/index.tsx | 99 ++++++++++++++++--- .../components/src/theme/style.module.scss | 4 + packages/components/src/theme/styles.ts | 35 ------- packages/components/src/theme/test/index.tsx | 9 ++ 4 files changed, 100 insertions(+), 47 deletions(-) create mode 100644 packages/components/src/theme/style.module.scss delete mode 100644 packages/components/src/theme/styles.ts diff --git a/packages/components/src/theme/index.tsx b/packages/components/src/theme/index.tsx index fbb92f59278d1b..e29e3de5e6bcd8 100644 --- a/packages/components/src/theme/index.tsx +++ b/packages/components/src/theme/index.tsx @@ -1,3 +1,9 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; +import type { CSSProperties } from 'react'; + /** * WordPress dependencies */ @@ -6,11 +12,70 @@ import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import type { ThemeProps } from './types'; +import type { ThemeOutputValues, ThemeProps } from './types'; import type { WordPressComponentProps } from '../context'; -import { colorVariables, Wrapper } from './styles'; import { generateThemeVariables } from './color-algorithms'; -import { useCx } from '../utils'; +import styles from './style.module.scss'; +import { PolymorphicElement } from '../utils/polymorphic-element'; + +type ThemeGrayKey = keyof NonNullable< + ThemeOutputValues[ 'colors' ][ 'gray' ] +>; +type ThemeColorVariable = + | '--wp-components-color-accent' + | '--wp-components-color-accent-darker-10' + | '--wp-components-color-accent-darker-20' + | '--wp-components-color-accent-inverted' + | '--wp-components-color-background' + | '--wp-components-color-foreground' + | '--wp-components-color-foreground-inverted' + | `--wp-components-color-gray-${ ThemeGrayKey }`; + +type ThemeStyle = CSSProperties & + Partial< Record< ThemeColorVariable, string > >; + +const getColorVariables = ( { colors }: ThemeOutputValues ) => { + const style: ThemeStyle = {}; + + if ( colors.accent ) { + style[ '--wp-components-color-accent' ] = colors.accent; + } + + if ( colors.accentDarker10 ) { + style[ '--wp-components-color-accent-darker-10' ] = + colors.accentDarker10; + } + + if ( colors.accentDarker20 ) { + style[ '--wp-components-color-accent-darker-20' ] = + colors.accentDarker20; + } + + if ( colors.accentInverted ) { + style[ '--wp-components-color-accent-inverted' ] = + colors.accentInverted; + } + + if ( colors.background ) { + style[ '--wp-components-color-background' ] = colors.background; + } + + if ( colors.foreground ) { + style[ '--wp-components-color-foreground' ] = colors.foreground; + } + + if ( colors.foregroundInverted ) { + style[ '--wp-components-color-foreground-inverted' ] = + colors.foregroundInverted; + } + + Object.entries( colors.gray || {} ).forEach( ( [ key, value ] ) => { + style[ `--wp-components-color-gray-${ key }` as ThemeColorVariable ] = + value; + } ); + + return style; +}; /** * `Theme` allows defining theme variables for components in the `@wordpress/components` package. @@ -35,21 +100,31 @@ function Theme( { accent, background, className, + style, ...props }: WordPressComponentProps< ThemeProps, 'div', true > ) { - const cx = useCx(); - const classes = useMemo( + const themeVariables = useMemo( () => - cx( - ...colorVariables( - generateThemeVariables( { accent, background } ) - ), - className + getColorVariables( + generateThemeVariables( { accent, background } ) ), - [ accent, background, className, cx ] + [ accent, background ] + ); + const wrapperStyle = useMemo( + () => ( { + ...themeVariables, + ...style, + } ), + [ style, themeVariables ] ); - return ; + return ( + + ); } export default Theme; diff --git a/packages/components/src/theme/style.module.scss b/packages/components/src/theme/style.module.scss new file mode 100644 index 00000000000000..7dbb3487c7f9ca --- /dev/null +++ b/packages/components/src/theme/style.module.scss @@ -0,0 +1,4 @@ +.wrapper { + /* stylelint-disable-next-line declaration-property-value-disallowed-list -- Preserve Theme's currentColor fallback when no foreground variable is generated. */ + color: var(--wp-components-color-foreground, currentColor); +} diff --git a/packages/components/src/theme/styles.ts b/packages/components/src/theme/styles.ts deleted file mode 100644 index 948c2d586649fb..00000000000000 --- a/packages/components/src/theme/styles.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * External dependencies - */ -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; - -/** - * Internal dependencies - */ -import type { ThemeOutputValues } from './types'; - -export const colorVariables = ( { colors }: ThemeOutputValues ) => { - const shades = Object.entries( colors.gray || {} ) - .map( ( [ k, v ] ) => `--wp-components-color-gray-${ k }: ${ v };` ) - .join( '' ); - - return [ - css` - --wp-components-color-accent: ${ colors.accent }; - --wp-components-color-accent-darker-10: ${ colors.accentDarker10 }; - --wp-components-color-accent-darker-20: ${ colors.accentDarker20 }; - --wp-components-color-accent-inverted: ${ colors.accentInverted }; - - --wp-components-color-background: ${ colors.background }; - --wp-components-color-foreground: ${ colors.foreground }; - --wp-components-color-foreground-inverted: ${ colors.foregroundInverted }; - - ${ shades } - `, - ]; -}; - -export const Wrapper = styled.div` - color: var( --wp-components-color-foreground, currentColor ); -`; diff --git a/packages/components/src/theme/test/index.tsx b/packages/components/src/theme/test/index.tsx index 1b32fa1dfa2d66..49636b3dbe54d9 100644 --- a/packages/components/src/theme/test/index.tsx +++ b/packages/components/src/theme/test/index.tsx @@ -25,6 +25,15 @@ const MyThemableComponent = ( props: MyThemableComponentProps ) => { }; describe( 'Theme', () => { + it( 'should support the as prop', () => { + render( ); + + expect( screen.getByTestId( 'theme' ) ).toHaveProperty( + 'tagName', + 'SECTION' + ); + } ); + describe( 'accent color', () => { it( 'does not define the accent color (and its variations) as a CSS variable when the `accent` prop is undefined', () => { render( From 95df3628bbbcba4a1ee2286c65d9210ef22d762d Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 2 Jul 2026 11:34:20 +0200 Subject: [PATCH 2/9] Theme: Add Emotion migration changelog entry --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d643b9b7ab4a1d..ce11cde786423f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -49,6 +49,7 @@ - Update `@ariakit/react` to `0.4.32` ([#79860](https://github.com/WordPress/gutenberg/pull/79860)). - `Flex`: Migrate styles from Emotion to SCSS Modules ([#79450](https://github.com/WordPress/gutenberg/pull/79450)). - `Surface`: Migrate styles from Emotion to SCSS Modules and use WPDS tokens for migrated visual values ([#79445](https://github.com/WordPress/gutenberg/pull/79445)). +- `Theme`: Migrate styles from Emotion to SCSS Modules ([#79447](https://github.com/WordPress/gutenberg/pull/79447)). - `Truncate`: Migrate styles from Emotion to SCSS Modules ([#79446](https://github.com/WordPress/gutenberg/pull/79446)). - `View`: Migrate away from Emotion while preserving polymorphic `as` behavior and style cascade order ([#79443](https://github.com/WordPress/gutenberg/pull/79443)). From a93e840bd67cce60e93ec9becc9c1224e0e3d184 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 2 Jul 2026 12:05:15 +0200 Subject: [PATCH 3/9] Theme: Prune Emotion lint suppression --- tools/eslint/suppressions.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index a561536ec018ed..3784567c512942 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -892,11 +892,6 @@ "count": 2 } }, - "packages/components/src/theme/styles.ts": { - "no-restricted-imports": { - "count": 2 - } - }, "packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts": { "no-restricted-imports": { "count": 2 From 85cfc2d07d24d81a193f46eb20174ba63e253773 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 2 Jul 2026 12:30:59 +0200 Subject: [PATCH 4/9] Theme: Use shared CSS custom property types --- packages/components/src/theme/index.tsx | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/components/src/theme/index.tsx b/packages/components/src/theme/index.tsx index e29e3de5e6bcd8..c57cf3bfaefd93 100644 --- a/packages/components/src/theme/index.tsx +++ b/packages/components/src/theme/index.tsx @@ -18,24 +18,8 @@ import { generateThemeVariables } from './color-algorithms'; import styles from './style.module.scss'; import { PolymorphicElement } from '../utils/polymorphic-element'; -type ThemeGrayKey = keyof NonNullable< - ThemeOutputValues[ 'colors' ][ 'gray' ] ->; -type ThemeColorVariable = - | '--wp-components-color-accent' - | '--wp-components-color-accent-darker-10' - | '--wp-components-color-accent-darker-20' - | '--wp-components-color-accent-inverted' - | '--wp-components-color-background' - | '--wp-components-color-foreground' - | '--wp-components-color-foreground-inverted' - | `--wp-components-color-gray-${ ThemeGrayKey }`; - -type ThemeStyle = CSSProperties & - Partial< Record< ThemeColorVariable, string > >; - const getColorVariables = ( { colors }: ThemeOutputValues ) => { - const style: ThemeStyle = {}; + const style: CSSProperties = {}; if ( colors.accent ) { style[ '--wp-components-color-accent' ] = colors.accent; @@ -70,8 +54,8 @@ const getColorVariables = ( { colors }: ThemeOutputValues ) => { } Object.entries( colors.gray || {} ).forEach( ( [ key, value ] ) => { - style[ `--wp-components-color-gray-${ key }` as ThemeColorVariable ] = - value; + const customProperty = `--wp-components-color-gray-${ key }` as const; + style[ customProperty ] = value; } ); return style; From ca164a3646fecd8bcf06bf48470e869a15419be0 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 2 Jul 2026 12:42:33 +0200 Subject: [PATCH 5/9] Theme: Test user style precedence --- packages/components/CHANGELOG.md | 2 +- packages/components/src/theme/test/index.tsx | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ce11cde786423f..513805dd9a8434 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -49,7 +49,7 @@ - Update `@ariakit/react` to `0.4.32` ([#79860](https://github.com/WordPress/gutenberg/pull/79860)). - `Flex`: Migrate styles from Emotion to SCSS Modules ([#79450](https://github.com/WordPress/gutenberg/pull/79450)). - `Surface`: Migrate styles from Emotion to SCSS Modules and use WPDS tokens for migrated visual values ([#79445](https://github.com/WordPress/gutenberg/pull/79445)). -- `Theme`: Migrate styles from Emotion to SCSS Modules ([#79447](https://github.com/WordPress/gutenberg/pull/79447)). +- `Theme`: Migrate away from Emotion while preserving polymorphic `as` behavior and user style precedence ([#79447](https://github.com/WordPress/gutenberg/pull/79447)). - `Truncate`: Migrate styles from Emotion to SCSS Modules ([#79446](https://github.com/WordPress/gutenberg/pull/79446)). - `View`: Migrate away from Emotion while preserving polymorphic `as` behavior and style cascade order ([#79443](https://github.com/WordPress/gutenberg/pull/79443)). diff --git a/packages/components/src/theme/test/index.tsx b/packages/components/src/theme/test/index.tsx index 49636b3dbe54d9..87b5765c804cf4 100644 --- a/packages/components/src/theme/test/index.tsx +++ b/packages/components/src/theme/test/index.tsx @@ -34,6 +34,20 @@ describe( 'Theme', () => { ); } ); + it( 'lets user styles override generated theme variables', () => { + render( + + ); + + expect( screen.getByTestId( 'theme' ) ).toHaveStyle( { + '--wp-components-color-accent': '#654321', + } ); + } ); + describe( 'accent color', () => { it( 'does not define the accent color (and its variations) as a CSS variable when the `accent` prop is undefined', () => { render( From 4671ffbd0bf8b0672a2cdb49f36facdf8e102cb5 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 2 Jul 2026 14:18:04 +0200 Subject: [PATCH 6/9] Theme: Remove import comments --- packages/components/src/theme/index.tsx | 9 --------- packages/components/src/theme/test/index.tsx | 6 ------ 2 files changed, 15 deletions(-) diff --git a/packages/components/src/theme/index.tsx b/packages/components/src/theme/index.tsx index c57cf3bfaefd93..363b43d5d7908b 100644 --- a/packages/components/src/theme/index.tsx +++ b/packages/components/src/theme/index.tsx @@ -1,17 +1,8 @@ -/** - * External dependencies - */ import clsx from 'clsx'; import type { CSSProperties } from 'react'; -/** - * WordPress dependencies - */ import { useMemo } from '@wordpress/element'; -/** - * Internal dependencies - */ import type { ThemeOutputValues, ThemeProps } from './types'; import type { WordPressComponentProps } from '../context'; import { generateThemeVariables } from './color-algorithms'; diff --git a/packages/components/src/theme/test/index.tsx b/packages/components/src/theme/test/index.tsx index 87b5765c804cf4..18efa4e2d443ae 100644 --- a/packages/components/src/theme/test/index.tsx +++ b/packages/components/src/theme/test/index.tsx @@ -1,12 +1,6 @@ -/** - * External dependencies - */ import { render, screen } from '@testing-library/react'; import type { ReactNode } from 'react'; -/** - * Internal dependencies - */ import Theme from '../'; type MyThemableComponentProps = { From 0cc6376f407656b11c47c3c061c91df7b69441e6 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 7 Jul 2026 07:35:00 +0200 Subject: [PATCH 7/9] Theme: Simplify color variable generation --- packages/components/src/theme/index.tsx | 59 +++++++------------------ 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/packages/components/src/theme/index.tsx b/packages/components/src/theme/index.tsx index 363b43d5d7908b..f75cbff2933753 100644 --- a/packages/components/src/theme/index.tsx +++ b/packages/components/src/theme/index.tsx @@ -9,48 +9,23 @@ import { generateThemeVariables } from './color-algorithms'; import styles from './style.module.scss'; import { PolymorphicElement } from '../utils/polymorphic-element'; -const getColorVariables = ( { colors }: ThemeOutputValues ) => { - const style: CSSProperties = {}; - - if ( colors.accent ) { - style[ '--wp-components-color-accent' ] = colors.accent; - } - - if ( colors.accentDarker10 ) { - style[ '--wp-components-color-accent-darker-10' ] = - colors.accentDarker10; - } - - if ( colors.accentDarker20 ) { - style[ '--wp-components-color-accent-darker-20' ] = - colors.accentDarker20; - } - - if ( colors.accentInverted ) { - style[ '--wp-components-color-accent-inverted' ] = - colors.accentInverted; - } - - if ( colors.background ) { - style[ '--wp-components-color-background' ] = colors.background; - } - - if ( colors.foreground ) { - style[ '--wp-components-color-foreground' ] = colors.foreground; - } - - if ( colors.foregroundInverted ) { - style[ '--wp-components-color-foreground-inverted' ] = - colors.foregroundInverted; - } - - Object.entries( colors.gray || {} ).forEach( ( [ key, value ] ) => { - const customProperty = `--wp-components-color-gray-${ key }` as const; - style[ customProperty ] = value; - } ); - - return style; -}; +const getColorVariables = ( { + colors, +}: ThemeOutputValues ): CSSProperties => ( { + '--wp-components-color-accent': colors.accent, + '--wp-components-color-accent-darker-10': colors.accentDarker10, + '--wp-components-color-accent-darker-20': colors.accentDarker20, + '--wp-components-color-accent-inverted': colors.accentInverted, + '--wp-components-color-background': colors.background, + '--wp-components-color-foreground': colors.foreground, + '--wp-components-color-foreground-inverted': colors.foregroundInverted, + ...Object.fromEntries( + Object.entries( colors.gray ?? {} ).map( ( [ key, value ] ) => [ + `--wp-components-color-gray-${ key }`, + value, + ] ) + ), +} ); /** * `Theme` allows defining theme variables for components in the `@wordpress/components` package. From 10b07f036e56ab6d7a786d8802f3ee625a5609c6 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 7 Jul 2026 07:38:40 +0200 Subject: [PATCH 8/9] Theme: Move changelog entry --- packages/components/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 513805dd9a8434..bba0af2942d003 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,7 @@ - `Truncate` ([#79446](https://github.com/WordPress/gutenberg/pull/79446)) - `Divider` ([#79444](https://github.com/WordPress/gutenberg/pull/79444)) - `Surface` ([#79445](https://github.com/WordPress/gutenberg/pull/79445)) + - `Theme` ([#79447](https://github.com/WordPress/gutenberg/pull/79447)) - `View` ([#79443](https://github.com/WordPress/gutenberg/pull/79443)) - The `__next40pxDefaultSize` prop is now true by default. The prop can be safely removed from the following: - `BorderBoxControl` ([#79420](https://github.com/WordPress/gutenberg/pull/79420)) @@ -49,7 +50,6 @@ - Update `@ariakit/react` to `0.4.32` ([#79860](https://github.com/WordPress/gutenberg/pull/79860)). - `Flex`: Migrate styles from Emotion to SCSS Modules ([#79450](https://github.com/WordPress/gutenberg/pull/79450)). - `Surface`: Migrate styles from Emotion to SCSS Modules and use WPDS tokens for migrated visual values ([#79445](https://github.com/WordPress/gutenberg/pull/79445)). -- `Theme`: Migrate away from Emotion while preserving polymorphic `as` behavior and user style precedence ([#79447](https://github.com/WordPress/gutenberg/pull/79447)). - `Truncate`: Migrate styles from Emotion to SCSS Modules ([#79446](https://github.com/WordPress/gutenberg/pull/79446)). - `View`: Migrate away from Emotion while preserving polymorphic `as` behavior and style cascade order ([#79443](https://github.com/WordPress/gutenberg/pull/79443)). From 51b78ea2c032797bd6d7cced8b3dd14361e45494 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 7 Jul 2026 07:50:41 +0200 Subject: [PATCH 9/9] Components: Align Emotion migration changelog entries --- packages/components/CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index bba0af2942d003..f4496cd49cbc42 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -7,10 +7,10 @@ - `ExternalLink`: No longer sets the `rel` attribute by default. Consumers relying on the previous behavior should pass `rel` explicitly ([#79743](https://github.com/WordPress/gutenberg/pull/79743)). - `View`: The legacy Emotion `css` prop no longer applies styles and is now accepted as a no-op for compatibility. Use `style` for inline styles or `className` for CSS-based styling instead ([#79443](https://github.com/WordPress/gutenberg/pull/79443)). - Components that compose Emotion style fragments with `cx()` should pass source-order-dependent fragments in a single `css()` call. Passing separate fragments can change override order after the following components stopped rendering styles through Emotion: - - `Truncate` ([#79446](https://github.com/WordPress/gutenberg/pull/79446)) - - `Divider` ([#79444](https://github.com/WordPress/gutenberg/pull/79444)) + - `Flex` ([#79450](https://github.com/WordPress/gutenberg/pull/79450)) - `Surface` ([#79445](https://github.com/WordPress/gutenberg/pull/79445)) - `Theme` ([#79447](https://github.com/WordPress/gutenberg/pull/79447)) + - `Truncate` ([#79446](https://github.com/WordPress/gutenberg/pull/79446)) - `View` ([#79443](https://github.com/WordPress/gutenberg/pull/79443)) - The `__next40pxDefaultSize` prop is now true by default. The prop can be safely removed from the following: - `BorderBoxControl` ([#79420](https://github.com/WordPress/gutenberg/pull/79420)) @@ -50,6 +50,7 @@ - Update `@ariakit/react` to `0.4.32` ([#79860](https://github.com/WordPress/gutenberg/pull/79860)). - `Flex`: Migrate styles from Emotion to SCSS Modules ([#79450](https://github.com/WordPress/gutenberg/pull/79450)). - `Surface`: Migrate styles from Emotion to SCSS Modules and use WPDS tokens for migrated visual values ([#79445](https://github.com/WordPress/gutenberg/pull/79445)). +- `Theme`: Migrate styles from Emotion to SCSS Modules ([#79447](https://github.com/WordPress/gutenberg/pull/79447)). - `Truncate`: Migrate styles from Emotion to SCSS Modules ([#79446](https://github.com/WordPress/gutenberg/pull/79446)). - `View`: Migrate away from Emotion while preserving polymorphic `as` behavior and style cascade order ([#79443](https://github.com/WordPress/gutenberg/pull/79443)). @@ -59,6 +60,8 @@ ### Breaking Changes +- Components that compose Emotion style fragments with `cx()` should pass source-order-dependent fragments in a single `css()` call. Passing separate fragments can change override order after the following components stopped rendering styles through Emotion: + - `Divider` ([#79444](https://github.com/WordPress/gutenberg/pull/79444)) - The `__next40pxDefaultSize` prop is now true by default. The prop can be safely removed from the following: - `BoxControl` ([#79419](https://github.com/WordPress/gutenberg/pull/79419)) - `TextControl` ([#79386](https://github.com/WordPress/gutenberg/pull/79386))