Commit 28d0447918 for wordpress.org

commit 28d0447918dbe0c28e37db5f1f89d575b58c1ff6
Author: aaronrobertshaw <aaronrobertshaw@git.wordpress.org>
Date:   Tue Oct 20 06:10:51 2026 +0000

    Editor: Add a background clip block support.

    Adds a `background.backgroundClip` block support, so a block can paint its background into the border box, padding box, content box or the text itself. Clipping to the text gives blocks a gradient text fill.

    The style engine gains the `backgroundClip` definition and a `VALID_BACKGROUND_CLIP_VALUES` constant, `WP_Theme_JSON` gains the setting and style, and KSES allows `background-clip` and its vendor prefixed companions. A color clipped to the text no longer adds the `has-background` class.

    Pseudo states and breakpoints that change the clip, or set a text color over a text gradient, now escape the inherited text clip, in both block instance styles and global styles.

    This brings in changes from https://github.com/WordPress/gutenberg/pull/77141, https://github.com/WordPress/gutenberg/pull/77142 and https://github.com/WordPress/gutenberg/pull/85057.

    Developed in: https://github.com/WordPress/wordpress-develop/pull/13491

    Props westonruter, isabel_brison, andrewserong, peterwilsoncc, ramonopoly, scruffian, joen, talldanwp.
    Fixes #66094.

    Built from https://develop.svn.wordpress.org/trunk@64590


    git-svn-id: http://core.svn.wordpress.org/trunk@63740 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/block-supports/background.php b/wp-includes/block-supports/background.php
index 3b3e277c37..8d464cc789 100644
--- a/wp-includes/block-supports/background.php
+++ b/wp-includes/block-supports/background.php
@@ -44,21 +44,39 @@ function wp_register_background_support( $block_type ) {
  * @since 6.6.0 Removed requirement for `backgroundImage.source`. A file/url is the default.
  * @since 6.7.0 Added support for `backgroundAttachment` output.
  * @since 7.1.0 Added support for `background.gradient` output.
+ * @since 7.2.0 Added support for `background.backgroundClip` output.
  *
  * @access private
  *
  * @param string $block_content Rendered block content.
  * @param array  $block         Block object.
  * @return string Filtered block content.
+ *
+ * @phpstan-param array{
+ *     blockName: string,
+ *     attrs: array{
+ *         style?: array{
+ *             background?: array<string, mixed>,
+ *             ...<string, mixed>
+ *         },
+ *         ...<string, mixed>
+ *     },
+ *     ...
+ * } $block
  */
 function wp_render_background_support( $block_content, $block ) {
-	$block_type                      = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+	$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+	if ( ! $block_type ) {
+		return $block_content;
+	}
+
 	$block_attributes                = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ) ? $block['attrs'] : array();
 	$has_background_image_support    = block_has_support( $block_type, array( 'background', 'backgroundImage' ), false );
 	$has_background_gradient_support = block_has_support( $block_type, array( 'background', 'gradient' ), false );
+	$has_background_clip_support     = block_has_support( $block_type, array( 'background', 'backgroundClip' ), false );

 	if (
-		( ! $has_background_image_support && ! $has_background_gradient_support ) ||
+		( ! $has_background_image_support && ! $has_background_gradient_support && ! $has_background_clip_support ) ||
 		! isset( $block_attributes['style']['background'] )
 	) {
 		return $block_content;
@@ -67,8 +85,9 @@ function wp_render_background_support( $block_content, $block ) {
 	// Check serialization skip for each feature individually.
 	$skip_background_image    = ! $has_background_image_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' );
 	$skip_background_gradient = ! $has_background_gradient_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'gradient' );
+	$skip_background_clip     = ! $has_background_clip_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundClip' );

-	if ( $skip_background_image && $skip_background_gradient ) {
+	if ( $skip_background_image && $skip_background_gradient && $skip_background_clip ) {
 		return $block_content;
 	}

@@ -95,6 +114,10 @@ function wp_render_background_support( $block_content, $block ) {
 		$background_styles['gradient'] = $block_attributes['style']['background']['gradient'] ?? null;
 	}

+	if ( ! $skip_background_clip ) {
+		$background_styles['backgroundClip'] = $block_attributes['style']['background']['backgroundClip'] ?? null;
+	}
+
 	$styles = wp_style_engine_get_styles( array( 'background' => $background_styles ) );

 	if ( ! empty( $styles['css'] ) ) {
@@ -111,7 +134,19 @@ function wp_render_background_support( $block_content, $block ) {
 			}

 			$tags->set_attribute( 'style', $updated_style );
-			$tags->add_class( 'has-background' );
+
+			/*
+			 * A background clipped to the text paints the glyphs rather than the
+			 * block's box, so there is no block background to announce. Read the
+			 * clip from the serialized styles so an unsupported or skipped value
+			 * left in the block's attributes cannot suppress the class.
+			 */
+			$background_clip = $background_styles['backgroundClip'] ?? null;
+			$has_background  = ! empty( $background_styles['backgroundImage'] ) || ! empty( $background_styles['gradient'] );
+
+			if ( $has_background && 'text' !== $background_clip ) {
+				$tags->add_class( 'has-background' );
+			}
 		}

 		return $tags->get_updated_html();
diff --git a/wp-includes/block-supports/colors.php b/wp-includes/block-supports/colors.php
index 567a542249..3c3396c587 100644
--- a/wp-includes/block-supports/colors.php
+++ b/wp-includes/block-supports/colors.php
@@ -74,13 +74,14 @@ function wp_register_colors_support( $block_type ) {
  * @since 5.6.0
  * @since 6.1.0 Implemented the style engine to generate CSS and classnames.
  * @since 7.1.0 Suppresses color.gradient when background.gradient is supported and set.
+ * @since 7.2.0 Skips `has-background` for a background color clipped to the text.
  * @access private
  *
- * @param WP_Block_Type $block_type       Block type.
- * @param array         $block_attributes Block attributes.
- * @return array Colors CSS classes and inline styles.
+ * @param WP_Block_Type        $block_type       Block type.
+ * @param array<string, mixed> $block_attributes Block attributes.
+ * @return array<string, mixed> Colors CSS classes and inline styles.
  */
-function wp_apply_colors_support( $block_type, $block_attributes ) {
+function wp_apply_colors_support( WP_Block_Type $block_type, array $block_attributes ): array {
 	$color_support = $block_type->supports['color'] ?? false;

 	if (
@@ -133,6 +134,18 @@ function wp_apply_colors_support( $block_type, $block_attributes ) {
 	$attributes = array();
 	$styles     = wp_style_engine_get_styles( array( 'color' => $color_block_styles ), array( 'convert_vars_to_classnames' => true ) );

+	/*
+	 * Mirror of the background support: a color clipped to the text paints the
+	 * glyphs, not the box, so only a gradient still adds `has-background`.
+	 */
+	$clips_to_text = 'text' === ( $block_attributes['style']['background']['backgroundClip'] ?? null ) &&
+		block_has_support( $block_type, array( 'background', 'backgroundClip' ), false ) &&
+		! wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundClip' );
+
+	if ( $clips_to_text && ! empty( $styles['classnames'] ) && empty( $color_block_styles['gradient'] ) ) {
+		$styles['classnames'] = implode( ' ', array_diff( explode( ' ', $styles['classnames'] ), array( 'has-background' ) ) );
+	}
+
 	if ( ! empty( $styles['classnames'] ) ) {
 		$attributes['class'] = $styles['classnames'];
 	}
diff --git a/wp-includes/block-supports/states.php b/wp-includes/block-supports/states.php
index ebd2c0915a..2657558486 100644
--- a/wp-includes/block-supports/states.php
+++ b/wp-includes/block-supports/states.php
@@ -147,6 +147,96 @@ function wp_get_state_declarations_with_background_resets( $declarations ) {
 	return $declarations;
 }

+/**
+ * Merges one state style object over another.
+ *
+ * Mirror of `mergeStyleObjects` in packages/block-editor/src/hooks/style.jsx.
+ *
+ * @since 7.2.0
+ *
+ * @param array<string, mixed> $target Styles underneath.
+ * @param array<string, mixed> $source Styles layered over them.
+ * @return array<string, mixed> Merged style object.
+ */
+function wp_merge_state_style_objects( $target, $source ) {
+	$target = is_array( $target ) ? $target : array();
+	$source = is_array( $source ) ? $source : array();
+	$merged = $target;
+
+	foreach ( $source as $key => $value ) {
+		$merged[ $key ] = is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] )
+			? wp_merge_state_style_objects( $merged[ $key ], $value )
+			: $value;
+	}
+
+	return $merged;
+}
+
+/**
+ * Checks whether a state paints its own background.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @param array<string, mixed> $state_style State style object.
+ * @return bool Whether the state sets a gradient or background image.
+ */
+function wp_state_paints_own_background( array $state_style ): bool {
+	return ! empty( $state_style['background']['gradient'] ) ||
+		! empty( $state_style['background']['backgroundImage'] ) ||
+		! empty( $state_style['color']['gradient'] );
+}
+
+/**
+ * Returns the declarations that unclip a text clip when a state sets a different clip.
+ *
+ * Mirror of `getStateTextClipEscapeCSS` in packages/block-editor/src/hooks/style.jsx.
+ *
+ * @since 7.2.0
+ *
+ * @param array<string, mixed> $state_style State style object.
+ * @param string               $base_clip   Background clip of the styles the state layers over.
+ * @return array<string, string> CSS declarations, empty when no escape is needed.
+ *
+ * @phpstan-return array<non-falsy-string, non-falsy-string>
+ */
+function wp_get_state_text_clip_escape( array $state_style, string $base_clip ): array {
+	$state_clip = $state_style['background']['backgroundClip'] ?? '';
+	if ( ! in_array( $state_clip, WP_Style_Engine::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
+		$state_clip = '';
+	}
+	if ( empty( $state_clip ) || 'text' === $state_clip || 'text' !== $base_clip || wp_state_paints_own_background( $state_style ) ) {
+		return array();
+	}
+	return array( 'background-image' => 'unset' );
+}
+
+/**
+ * Returns the declarations that refill the letters when a state sets a text color but no clip.
+ *
+ * The fill is added even without a known text clip, since a block instance
+ * cannot see a clip set in global styles. It does nothing when no clip applies.
+ *
+ * Mirror of `getStateTextFillResetCSS` in packages/block-editor/src/hooks/style.jsx.
+ *
+ * @since 7.2.0
+ *
+ * @param array<string, mixed> $state_style State style object.
+ * @return array<string, string> CSS declarations, empty when no reset is needed.
+ *
+ * @phpstan-return array<non-falsy-string, non-falsy-string>
+ */
+function wp_get_state_text_fill_reset( array $state_style ): array {
+	$state_clip = $state_style['background']['backgroundClip'] ?? '';
+	if ( ! in_array( $state_clip, WP_Style_Engine::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
+		$state_clip = '';
+	}
+	if ( ! empty( $state_clip ) || empty( $state_style['color']['text'] ) || wp_state_paints_own_background( $state_style ) ) {
+		return array();
+	}
+	return array( '-webkit-text-fill-color' => 'currentColor' );
+}
+
 /**
  * Adds fallback dimension styles for aspectRatio and height block-support values.
  *
@@ -365,14 +455,16 @@ function wp_get_block_state_element_selectors( $root_selector ) {
  *
  * @since 7.1.0
  * @since 7.2.0 Added fallback dimension styles.
- *
- * @param array       $css_rules   Style rules.
- * @param string      $state       Pseudo-state selector.
- * @param string|null $selector    Block, feature, or element selector.
- * @param array       $style       Style object.
- * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
+ *              Added the `$base_style` parameter.
+ *
+ * @param array[]                   $css_rules   Style rules.
+ * @param string                    $state       Pseudo-state selector.
+ * @param string|null               $selector    Block, feature, or element selector.
+ * @param array<string, mixed>      $style       Style object.
+ * @param string|null               $rules_group Optional CSS grouping rule, e.g. a media query.
+ * @param array<string, mixed>|null $base_style  Optional styles the state layers over.
  */
-function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null ) {
+function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null, $base_style = null ) {
 	if ( empty( $style ) || ! is_array( $style ) ) {
 		return;
 	}
@@ -389,6 +481,16 @@ function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style,
 		$declarations['text-align'] = $text_align;
 	}

+	// Only block states pass a base; element states match the editor and skip it.
+	if ( null !== $base_style ) {
+		$base_clip    = $base_style['background']['backgroundClip'] ?? '';
+		$declarations = array_merge(
+			$declarations,
+			wp_get_state_text_clip_escape( $style, is_string( $base_clip ) ? $base_clip : '' ),
+			wp_get_state_text_fill_reset( $style )
+		);
+	}
+
 	if ( empty( $declarations ) ) {
 		return;
 	}
@@ -407,13 +509,20 @@ function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style,
  * Builds compiled state style rules, preserving the selector each rule targets.
  *
  * @since 7.1.0
+ * @since 7.2.0 Added the `$base_style` parameter.
  *
- * @param array         $state_styles Map of state to style array.
- * @param WP_Block_Type $block_type   Block type.
- * @param string|null   $rules_group  Optional CSS grouping rule, e.g. a media query.
+ * @param array<string, mixed>      $state_styles Map of state to style array.
+ * @param WP_Block_Type             $block_type   Block type.
+ * @param string|null               $rules_group  Optional CSS grouping rule, e.g. a media query.
+ * @param array<string, mixed>|null $base_style   Optional styles the state layers over.
  * @return array[] State style rules.
  */
-function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null ) {
+function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null, $base_style = null ) {
+	// Only a block that supports the clip can sit under a text clip to escape.
+	if ( ! block_has_support( $block_type, array( 'background', 'backgroundClip' ), false ) ) {
+		$base_style = null;
+	}
+
 	$css_rules       = array();
 	$block_selectors = is_array( $block_type->selectors )
 		? $block_type->selectors
@@ -430,7 +539,8 @@ function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_grou
 				$state,
 				$group['selector'],
 				$group['style'],
-				$rules_group
+				$rules_group,
+				$base_style
 			);
 		}
 	}
@@ -576,6 +686,10 @@ function wp_render_block_states_support( $block_content, $block ) {

 	$supported_pseudo_states  = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
 	$css_rules                = array();
+	$default_root_style       = wp_get_root_state_style(
+		$style,
+		array_merge( array( 'elements' ), $supported_pseudo_states )
+	);
 	$viewport_settings        = wp_get_global_settings( array( 'viewport' ) );
 	$responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );

@@ -588,7 +702,9 @@ function wp_render_block_states_support( $block_content, $block ) {
 			$css_rules,
 			wp_get_block_state_style_rules(
 				array( $pseudo_state => $style[ $pseudo_state ] ),
-				$block_type
+				$block_type,
+				null,
+				$default_root_style
 			)
 		);
 	}
@@ -609,7 +725,8 @@ function wp_render_block_states_support( $block_content, $block ) {
 				wp_get_block_state_style_rules(
 					array( '' => $root_state_style ),
 					$block_type,
-					$media_query
+					$media_query,
+					$default_root_style
 				)
 			);
 		}
@@ -675,7 +792,11 @@ function wp_render_block_states_support( $block_content, $block ) {
 				wp_get_block_state_style_rules(
 					array( $pseudo_state => $style[ $breakpoint ][ $pseudo_state ] ),
 					$block_type,
-					$media_query
+					$media_query,
+					wp_merge_state_style_objects(
+						$default_root_style,
+						$root_state_style
+					)
 				)
 			);
 		}
diff --git a/wp-includes/class-wp-theme-json.php b/wp-includes/class-wp-theme-json.php
index 116a8ad951..f57d473065 100644
--- a/wp-includes/class-wp-theme-json.php
+++ b/wp-includes/class-wp-theme-json.php
@@ -259,6 +259,7 @@ class WP_Theme_JSON {
 	 * @since 7.0.0 Added `dimensions.width` and `dimensions.height`.
 	 *              Added `text-indent` property.
 	 * @since 7.1.0 Added `min-width` and `text-shadow`.
+	 * @since 7.2.0 Added `background-clip`.
 	 * @var array
 	 */
 	const PROPERTIES_METADATA = array(
@@ -270,6 +271,7 @@ class WP_Theme_JSON {
 		'background-repeat'                 => array( 'background', 'backgroundRepeat' ),
 		'background-size'                   => array( 'background', 'backgroundSize' ),
 		'background-attachment'             => array( 'background', 'backgroundAttachment' ),
+		'background-clip'                   => array( 'background', 'backgroundClip' ),
 		'border-radius'                     => array( 'border', 'radius' ),
 		'border-top-left-radius'            => array( 'border', 'radius', 'topLeft' ),
 		'border-top-right-radius'           => array( 'border', 'radius', 'topRight' ),
@@ -428,6 +430,7 @@ class WP_Theme_JSON {
 	 *              Added support for `dimensions.width` and `dimensions.height`.
 	 *              Added support for `typography.textIndent`.
 	 * @since 7.1.0 Added `viewport` property.
+	 * @since 7.2.0 Added `background.backgroundClip` property.
 	 *              Added support for `background.gradient`, `dimensions.minWidth` and `blockVisibility.allowEditing`.
 	 * @since 7.2.0 Added support for `typography.textShadow`, `typography.textShadowPresets`,
 	 *              and `typography.defaultTextShadowPresets`.
@@ -437,6 +440,7 @@ class WP_Theme_JSON {
 		'appearanceTools'               => null,
 		'useRootPaddingAwareAlignments' => null,
 		'background'                    => array(
+			'backgroundClip'  => null,
 			'backgroundImage' => null,
 			'backgroundSize'  => null,
 			'gradient'        => null,
@@ -582,10 +586,12 @@ class WP_Theme_JSON {
 	 * @since 7.0.0 Added support for `dimensions.width` and `dimensions.height`.
 	 * @since 7.1.0 Added support for `background.gradient`,`dimensions.minWidth`,
 	 *              and `typography.textShadow`.
+	 * @since 7.2.0 Added support for `background.backgroundClip`.
 	 * @var array
 	 */
 	const VALID_STYLES = array(
 		'background' => array(
+			'backgroundClip'       => null,
 			'backgroundImage'      => null,
 			'backgroundPosition'   => null,
 			'backgroundRepeat'     => null,
@@ -1000,6 +1006,15 @@ class WP_Theme_JSON {
 			return $pseudo_declarations;
 		}

+		// Nearest first: this node, then its variation's block.
+		$base_styles = array( $node );
+		foreach ( array( $style_variation['path'] ?? null, $block_metadata['path'] ?? null ) as $path ) {
+			$layer = $path ? _wp_array_get( $this->theme_json, $path ) : null;
+			if ( is_array( $layer ) ) {
+				$base_styles[] = $layer;
+			}
+		}
+
 		foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
 			if ( isset( $node[ $pseudo_selector ] ) ) {
 				$pseudo_node = $node[ $pseudo_selector ];
@@ -1021,6 +1036,7 @@ class WP_Theme_JSON {

 				$combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
 				$declarations      = static::compute_style_properties( $pseudo_node, $settings, null, null );
+				$declarations      = self::get_state_declarations_with_text_clip_escapes( $declarations, $pseudo_node, $base_styles );
 				$add_declarations( $combined_selector, $declarations );
 			}
 		}
@@ -3224,6 +3240,34 @@ class WP_Theme_JSON {
 				'name'  => $css_property,
 				'value' => $value,
 			);
+
+			/*
+			 * Background clipping needs vendor prefixed properties for
+			 * cross-browser support. The `text` value clips the background to
+			 * the text and makes it visible through a transparent fill. The box
+			 * values only reset the fill color, to cancel any inherited text
+			 * clipping. `-webkit-background-clip` is an alias of
+			 * `background-clip` in Chromium, so resetting it would discard the
+			 * value set above. Values outside the set the style engine accepts
+			 * get neither, so a typo cannot reset the fill color.
+			 */
+			if ( 'background-clip' === $css_property ) {
+				if ( 'text' === $value ) {
+					$declarations[] = array(
+						'name'  => '-webkit-background-clip',
+						'value' => 'text',
+					);
+					$declarations[] = array(
+						'name'  => '-webkit-text-fill-color',
+						'value' => 'transparent',
+					);
+				} elseif ( in_array( $value, array( 'border-box', 'padding-box', 'content-box' ), true ) ) {
+					$declarations[] = array(
+						'name'  => '-webkit-text-fill-color',
+						'value' => 'currentColor',
+					);
+				}
+			}
 		}

 		// If a variable value is added to the root, the corresponding property should be removed.
@@ -3921,6 +3965,108 @@ class WP_Theme_JSON {
 		return $nodes;
 	}

+	/**
+	 * Returns the node keys that hold a state's styles: a breakpoint, a pseudo
+	 * state or a custom state.
+	 *
+	 * @since 7.2.0
+	 *
+	 * @param array<string, mixed> $settings Settings from theme.json.
+	 * @return string[] Style-state keys.
+	 *
+	 * @phpstan-return list<non-falsy-string>
+	 */
+	private static function get_style_state_keys( array $settings ): array {
+		$keys = array_keys( static::get_viewport_media_queries( $settings['viewport'] ?? null ) );
+
+		foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS as $pseudo_selectors ) {
+			$keys = array_merge( $keys, $pseudo_selectors );
+		}
+
+		foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS as $pseudo_selectors ) {
+			$keys = array_merge( $keys, $pseudo_selectors );
+		}
+
+		foreach ( static::VALID_BLOCK_CUSTOM_STATES as $custom_states ) {
+			$keys = array_merge( $keys, $custom_states );
+		}
+
+		return array_values( array_unique( $keys ) );
+	}
+
+	/**
+	 * Returns the style objects a state node layers over, nearest first.
+	 *
+	 * Drops the path's state keys (e.g. `@mobile`) from the innermost outward.
+	 *
+	 * @since 7.2.0
+	 *
+	 * @param string[]             $path     Path to the node within the theme.json tree.
+	 * @param array<string, mixed> $tree     The theme.json tree to read from.
+	 * @param array<string, mixed> $settings Settings from theme.json.
+	 * @return array[] Style objects, nearest first.
+	 */
+	private static function get_state_base_styles( array $path, array $tree, array $settings ): array {
+		$state_keys  = self::get_style_state_keys( $settings );
+		$base_path   = $path;
+		$base_styles = array();
+
+		foreach ( array_reverse( array_keys( $path ) ) as $index ) {
+			if ( ! in_array( $path[ $index ], $state_keys, true ) ) {
+				continue;
+			}
+
+			unset( $base_path[ $index ] );
+			$layer = _wp_array_get( $tree, array_values( $base_path ) );
+			if ( is_array( $layer ) ) {
+				$base_styles[] = $layer;
+			}
+		}
+
+		return $base_styles;
+	}
+
+	/**
+	 * Adds the declarations a state needs to escape a text clip set below it.
+	 *
+	 * @since 7.2.0
+	 *
+	 * @param array[]              $declarations Declarations generated for the state.
+	 * @param array<string, mixed> $state_style  State style object.
+	 * @param array[]              $base_styles  Style objects the state layers over, nearest first.
+	 * @return array[] Declarations with the escapes applied where needed.
+	 *
+	 * @phpstan-param list<array{ name: non-falsy-string, value: scalar }> $declarations
+	 * @phpstan-return list<array{ name: non-falsy-string, value: scalar }>
+	 */
+	private static function get_state_declarations_with_text_clip_escapes( array $declarations, array $state_style, array $base_styles ): array {
+		$base_clip = '';
+		foreach ( $base_styles as $base_style ) {
+			if ( ! empty( $base_style['background']['backgroundClip'] ) ) {
+				$base_clip = is_string( $base_style['background']['backgroundClip'] ) ? $base_style['background']['backgroundClip'] : '';
+				break;
+			}
+		}
+
+		if ( 'text' !== $base_clip ) {
+			return $declarations;
+		}
+
+		$escapes = array_merge(
+			wp_get_state_text_clip_escape( $state_style, $base_clip ),
+			wp_get_state_text_fill_reset( $state_style )
+		);
+
+		foreach ( $escapes as $name => $value ) {
+			$declarations[] = array(
+				'name'  => $name,
+				'value' => $value,
+			);
+		}
+
+		return $declarations;
+	}
+
 	/**
 	 * Gets the CSS rules for a particular block from theme.json.
 	 *
@@ -4056,6 +4202,11 @@ class WP_Theme_JSON {

 					// Process base properties for this breakpoint.
 					$breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json );
+					$breakpoint_declarations = self::get_state_declarations_with_text_clip_escapes(
+						$breakpoint_declarations,
+						$breakpoint_node,
+						array( $style_variation_node, $node )
+					);
 					if ( ! empty( $breakpoint_declarations ) ) {
 						$base_ruleset              = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations );
 						$variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}';
@@ -4180,11 +4331,18 @@ class WP_Theme_JSON {
 		 * element then compute the style properties for it.
 		 * Otherwise just compute the styles for the default selector as normal.
 		 */
+		$base_styles = self::get_state_base_styles( $block_metadata['path'] ?? array(), $this->theme_json, $settings );
+
 		if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
 			isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
 			&& in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
 		) {
 			$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
+			$declarations = self::get_state_declarations_with_text_clip_escapes(
+				$declarations,
+				$node[ $pseudo_selector ],
+				array_merge( array( $node ), $base_styles )
+			);
 		} else {
 			/*
 			 * For block pseudo-selector nodes (e.g. ':hover'), $node has already had any
@@ -4192,6 +4350,7 @@ class WP_Theme_JSON {
 			 * so those properties are not output twice.
 			 */
 			$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
+			$declarations = self::get_state_declarations_with_text_clip_escapes( $declarations, $node, $base_styles );
 		}

 		$block_rules = '';
diff --git a/wp-includes/kses.php b/wp-includes/kses.php
index cbe2f4d04c..c68b71fcc1 100644
--- a/wp-includes/kses.php
+++ b/wp-includes/kses.php
@@ -3692,6 +3692,7 @@ function kses_init() {
  *              Added support for transform functions, `clip-path` basic shapes,
  *              and URLs in the SVG element reference properties.
  * @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`.
+ *              Added support for `background-clip`.
  *
  * @param string $css        A string of CSS rules, decoded from an HTML `style` attribute.
  * @param string $deprecated Not used.
@@ -3717,6 +3718,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
 	 *
 	 * @since 2.8.1
 	 * @since 7.1.0 Added support for SVG presentation attributes.
+	 * @since 7.2.0 Added support for `background-clip` and its vendor prefixed companions.
 	 *
 	 * @param string[] $attr Array of allowed CSS attributes.
 	 */
@@ -3731,6 +3733,9 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
 			'background-size',
 			'background-attachment',
 			'background-blend-mode',
+			'background-clip',
+			'-webkit-background-clip',
+			'-webkit-text-fill-color',

 			'border',
 			'border-radius',
diff --git a/wp-includes/style-engine/class-wp-style-engine.php b/wp-includes/style-engine/class-wp-style-engine.php
index b2988c7140..81499d15e5 100644
--- a/wp-includes/style-engine/class-wp-style-engine.php
+++ b/wp-includes/style-engine/class-wp-style-engine.php
@@ -31,6 +31,14 @@
  */
 #[AllowDynamicProperties]
 final class WP_Style_Engine {
+	/**
+	 * The values `background-clip` accepts.
+	 *
+	 * @since 7.2.0
+	 * @var string[]
+	 */
+	const VALID_BACKGROUND_CLIP_VALUES = array( 'border-box', 'padding-box', 'content-box', 'text' );
+
 	/**
 	 * Style definitions that contain the instructions to parse/output valid Gutenberg styles from a block's attributes.
 	 *
@@ -50,6 +58,7 @@ final class WP_Style_Engine {
 	 *
 	 * @since 6.1.0
 	 * @since 7.1.0 Added `background.gradient` property.
+	 * @since 7.2.0 Added `background.backgroundClip` property.
 	 * @var array
 	 */
 	const BLOCK_STYLE_DEFINITIONS_METADATA = array(
@@ -97,6 +106,10 @@ final class WP_Style_Engine {
 					'has-background' => true,
 				),
 			),
+			'backgroundClip'       => array(
+				'value_func' => array( self::class, 'get_background_clip_css_declarations' ),
+				'path'       => array( 'background', 'backgroundClip' ),
+			),
 		),
 		'color'      => array(
 			'text'       => array(
@@ -768,6 +781,46 @@ final class WP_Style_Engine {
 		return $css_declarations;
 	}

+	/**
+	 * Style value parser that returns the CSS declarations for background clipping.
+	 *
+	 * For the `text` value the background is clipped to the block's text, which
+	 * requires the vendor prefixed properties and a transparent fill color.
+	 *
+	 * @since 7.2.0
+	 *
+	 * @param string $style_value      A single raw style value from $block_styles array.
+	 * @param array  $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
+	 * @return array<string, string> An associative array of CSS definitions, mapping properties to values.
+	 *
+	 * @phpstan-return array<non-falsy-string, non-falsy-string>
+	 */
+	protected static function get_background_clip_css_declarations( $style_value, $style_definition ) {
+		if ( ! in_array( $style_value, static::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
+			return array();
+		}
+
+		$css_declarations = array(
+			'background-clip' => $style_value,
+		);
+
+		if ( 'text' === $style_value ) {
+			$css_declarations['-webkit-background-clip'] = 'text';
+			$css_declarations['-webkit-text-fill-color'] = 'transparent';
+		} else {
+			/*
+			 * Only the fill color is restored. `-webkit-background-clip` is an
+			 * alias of `background-clip` in Chromium, so resetting it here would
+			 * discard the value set above. The fill color is inherited, so it
+			 * needs its initial value rather than `unset`, which would take a
+			 * transparent fill from an ancestor clipping to text.
+			 */
+			$css_declarations['-webkit-text-fill-color'] = 'currentColor';
+		}
+
+		return $css_declarations;
+	}
+
 	/**
 	 * Returns compiled CSS from CSS declarations.
 	 *
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 2a6ea03638..ce61ecff64 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-64589';
+$wp_version = '7.2-alpha-64590';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.