diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index ed8e1ac11dce85..b0cb0b13ff9e9b 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -407,7 +407,7 @@ Introduce new sections and organize content to help visitors (and search engines
- **Name:** core/heading
- **Category:** text
-- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fitText, fontSize, lineHeight, textAlign)
+- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, background (backgroundClip, backgroundImage, gradient), className, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fitText, fontSize, lineHeight, textAlign)
- **Attributes:** content, level, levelOptions, placeholder
## Home Link
@@ -606,7 +606,7 @@ Start with the basic building block of all narrative. ([Source](https://github.c
- **Name:** core/paragraph
- **Category:** text
-- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fitText, fontSize, lineHeight, textAlign, textColumns, textIndent), ~~className~~
+- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, background (backgroundClip, gradient), color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fitText, fontSize, lineHeight, textAlign, textColumns, textIndent), ~~className~~
- **Attributes:** content, direction, dropCap, placeholder
## Pattern Placeholder
diff --git a/packages/block-editor/src/components/background-clip-control/index.js b/packages/block-editor/src/components/background-clip-control/index.js
new file mode 100644
index 00000000000000..9fcec69112807e
--- /dev/null
+++ b/packages/block-editor/src/components/background-clip-control/index.js
@@ -0,0 +1,183 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ Dropdown,
+ Button,
+ __experimentalDropdownContentWrapper as DropdownContentWrapper,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+ FlexBlock,
+} from '@wordpress/components';
+import { useState } from '@wordpress/element';
+
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+
+export const ALL_BACKGROUND_CLIP_VALUES = [
+ 'border-box',
+ 'padding-box',
+ 'content-box',
+ 'text',
+];
+
+const BACKGROUND_CLIP_OPTIONS = [
+ {
+ label: __( 'Border box' ),
+ value: 'border-box',
+ },
+ {
+ label: __( 'Padding box' ),
+ value: 'padding-box',
+ },
+ {
+ label: __( 'Content box' ),
+ value: 'content-box',
+ },
+ {
+ label: __( 'Text' ),
+ value: 'text',
+ },
+];
+
+const BACKGROUND_POPOVER_PROPS = {
+ placement: 'left-start',
+ offset: 36,
+ shift: true,
+ className: 'block-editor-background-clip-control__popover',
+};
+
+function BackgroundClipPreview( { value } ) {
+ const isText = value === 'text';
+
+ return (
+
+ { isText && 'Ab' }
+
+ );
+}
+
+function BackgroundClipOption( { label, value, isActive, onSelect } ) {
+ return (
+
+ );
+}
+
+function BackgroundClipToggle( { value, toggleProps } ) {
+ const activeOption = BACKGROUND_CLIP_OPTIONS.find(
+ ( option ) => option.value === value
+ );
+ const label = activeOption ? activeOption.label : __( 'Border box' );
+
+ return (
+
+ );
+}
+
+export default function BackgroundClipControl( {
+ value,
+ onChange,
+ allowedValues,
+} ) {
+ const [ isOpen, setIsOpen ] = useState( false );
+
+ const options = allowedValues
+ ? BACKGROUND_CLIP_OPTIONS.filter( ( opt ) =>
+ allowedValues.includes( opt.value )
+ )
+ : BACKGROUND_CLIP_OPTIONS;
+
+ return (
+
+
{
+ return (
+ {
+ onToggle();
+ setIsOpen( ! dropdownIsOpen );
+ },
+ className:
+ 'block-editor-background-clip-control__dropdown-toggle',
+ 'aria-expanded': dropdownIsOpen,
+ 'aria-label': __( 'Background clip options' ),
+ } }
+ />
+ );
+ } }
+ onClose={ () => setIsOpen( false ) }
+ renderContent={ () => (
+
+
+
+ { __( 'Background clip' ) }
+
+
+ { options.map( ( option ) => (
+
+ onChange(
+ value === option.value
+ ? undefined
+ : option.value
+ )
+ }
+ />
+ ) ) }
+
+
+
+ ) }
+ />
+
+ );
+}
diff --git a/packages/block-editor/src/components/background-clip-control/style.scss b/packages/block-editor/src/components/background-clip-control/style.scss
new file mode 100644
index 00000000000000..02e7c311012334
--- /dev/null
+++ b/packages/block-editor/src/components/background-clip-control/style.scss
@@ -0,0 +1,158 @@
+@use "@wordpress/base-styles/variables" as *;
+@use "@wordpress/base-styles/colors" as *;
+
+.block-editor-background-clip-control__container {
+ position: relative;
+
+ &.is-open {
+ background-color: $gray-100;
+ }
+
+ .components-dropdown {
+ display: block;
+ }
+}
+
+.block-editor-background-clip-control__dropdown-toggle {
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ height: $button-size-next-default-40px;
+ width: 100%;
+ padding-left: $grid-unit-15;
+ padding-right: $grid-unit-15;
+ color: $gray-900;
+
+ &:hover {
+ color: var(--wp-admin-theme-color);
+ }
+
+ &:focus {
+ box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
+ }
+}
+
+// Popover content
+.block-editor-background-clip-control__dropdown-content-wrapper {
+ min-width: 260px;
+}
+
+.block-editor-background-clip-control__popover-title {
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+ color: $gray-700;
+}
+
+.block-editor-background-clip-control__options {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: $grid-unit-10;
+}
+
+.block-editor-background-clip-control__option {
+ appearance: none;
+ background: $gray-100;
+ color: $gray-900;
+ border: $border-width solid $gray-200;
+ border-radius: $radius-small;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: $grid-unit-10;
+ padding: $grid-unit-15;
+ box-sizing: border-box;
+
+ @media not (prefers-reduced-motion) {
+ transition:
+ border-color 0.1s ease,
+ background-color 0.1s ease;
+ }
+
+ &:hover {
+ background-color: $gray-200;
+ border-color: $gray-400;
+ }
+
+ &:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
+ }
+
+ &.is-active {
+ background-color: $white;
+ border-color: var(--wp-admin-theme-color);
+ box-shadow: inset 0 0 0 $border-width var(--wp-admin-theme-color);
+ }
+}
+
+.block-editor-background-clip-control__option-label {
+ font-size: 12px;
+ line-height: 1;
+}
+
+// Preview swatch shared styles
+.block-editor-background-clip-control__preview {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border-radius: 2px;
+ box-sizing: border-box;
+
+ // Background gradient as the "fill" that gets clipped.
+ background: linear-gradient(135deg, var(--wp-admin-theme-color) 0%, var(--wp-admin-theme-color-darker-20, #{$gray-700}) 100%);
+
+ // Default: border-box — fill extends to border edge.
+ // Show a dashed border to indicate the border area is included.
+ border: 3px dashed rgba(0, 0, 0, 0.15);
+ background-clip: border-box;
+
+ &.is-padding-box {
+ // Fill clips to inside the border — border area is empty.
+ border: 3px dashed $gray-400;
+ background-clip: padding-box;
+ }
+
+ &.is-content-box {
+ // Fill clips to content only — padding and border are empty.
+ border: 3px dashed $gray-400;
+ padding: 4px;
+ background-clip: content-box;
+ }
+
+ &.is-text {
+ font-size: 18px;
+ font-weight: 800;
+ line-height: 1;
+ color: transparent;
+ border: none;
+ padding: 0;
+ -webkit-background-clip: text;
+ background-clip: text;
+ }
+}
+
+// Smaller preview in the toggle button row.
+.block-editor-background-clip-control__toggle-inner {
+ height: 100%;
+
+ .block-editor-background-clip-control__preview {
+ width: 20px;
+ height: 20px;
+ border-width: 2px;
+ flex-shrink: 0;
+
+ &.is-content-box {
+ padding: 2px;
+ border-width: 2px;
+ }
+
+ &.is-text {
+ font-size: 11px;
+ }
+ }
+}
diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js
index 23a8ec5a6eb981..aa6315ec73dfdf 100644
--- a/packages/block-editor/src/components/block-inspector/index.js
+++ b/packages/block-editor/src/components/block-inspector/index.js
@@ -53,7 +53,7 @@ function StyleInspectorSlots( {
/>
0 || areCustomGradientsEnabled;
+ // Determine whether backgroundClip is currently set to text (text gradient).
+ const isTextGradient = value?.background?.backgroundClip === 'text';
+
const hasBackgroundGradientControl = useHasBackgroundControl(
settings,
'gradient'
@@ -150,11 +160,48 @@ export default function BackgroundImagePanel( {
'backgroundImage'
);
+ const clipSetting = settings?.background?.backgroundClip;
+ let allowedClipValues = [];
+ if ( clipSetting === true ) {
+ allowedClipValues = ALL_BACKGROUND_CLIP_VALUES;
+ } else if ( Array.isArray( clipSetting ) ) {
+ allowedClipValues = clipSetting;
+ }
+ // When text gradient support is active (the color panel's text section
+ // handles setting backgroundClip to 'text'), exclude 'text' from the
+ // background panel's clip control. This avoids shared-state confusion
+ // between the two panels.
+ const hasTextGradientSupport =
+ allowedClipValues.includes( 'text' ) && hasBackgroundGradientControl;
+ if ( hasTextGradientSupport ) {
+ allowedClipValues = allowedClipValues.filter( ( v ) => v !== 'text' );
+ }
+ const showBackgroundClipControl = allowedClipValues.length > 0;
+
+ const resetBackgroundClip = () =>
+ onChange(
+ setImmutably( value, [ 'background', 'backgroundClip' ], undefined )
+ );
+
const resetAllFilter = useCallback(
( previousValue ) => {
+ const prevClip = previousValue?.background?.backgroundClip;
+ const isTextGrad = prevClip === 'text';
+
return {
...previousValue,
- background: {},
+ background: {
+ // When a text gradient is active, the color panel owns
+ // gradient and backgroundClip. Preserve them here so the
+ // background panel's "Reset all" only clears background-
+ // panel values (image, size, position, etc.).
+ ...( isTextGrad
+ ? {
+ gradient: previousValue?.background?.gradient,
+ backgroundClip: prevClip,
+ }
+ : {} ),
+ },
color: hasBackgroundGradientControl
? {
...previousValue?.color,
@@ -166,7 +213,11 @@ export default function BackgroundImagePanel( {
[ hasBackgroundGradientControl ]
);
- if ( ! showBackgroundGradientControl && ! showBackgroundImageControl ) {
+ if (
+ ! showBackgroundGradientControl &&
+ ! showBackgroundImageControl &&
+ ! showBackgroundClipControl
+ ) {
return null;
}
@@ -184,7 +235,7 @@ export default function BackgroundImagePanel( {
: gradientValue;
};
- const resetBackground = () =>
+ const resetBackgroundImage = () =>
onChange(
setImmutably(
value,
@@ -200,6 +251,15 @@ export default function BackgroundImagePanel( {
undefined
);
newValue = setImmutably( newValue, [ 'color', 'gradient' ], undefined );
+ // If the gradient was used as a text gradient, also clear backgroundClip
+ // to avoid leaving text invisible with no gradient applied.
+ if ( value?.background?.backgroundClip === 'text' ) {
+ newValue = setImmutably(
+ newValue,
+ [ 'background', 'backgroundClip' ],
+ undefined
+ );
+ }
onChange( newValue );
};
@@ -240,7 +300,7 @@ export default function BackgroundImagePanel( {
className="block-editor-background-panel__item"
hasValue={ () => hasBackgroundImageValue( value ) }
label={ __( 'Image' ) }
- onDeselect={ resetBackground }
+ onDeselect={ resetBackgroundImage }
isShownByDefault={ defaultControls.backgroundImage }
panelId={ panelId }
>
@@ -258,7 +318,9 @@ export default function BackgroundImagePanel( {
hasBackgroundGradientValue( value ) }
+ hasValue={ () =>
+ hasBackgroundGradientValue( value ) && ! isTextGradient
+ }
resetValue={ resetGradient }
isShownByDefault={ defaultControls.gradient }
indicators={ [ currentGradient ] }
@@ -280,6 +342,33 @@ export default function BackgroundImagePanel( {
panelId={ panelId }
/>
) }
+ { showBackgroundClipControl && (
+
+ !! value?.background?.backgroundClip &&
+ value?.background?.backgroundClip !== 'text'
+ }
+ label={ __( 'Clip' ) }
+ onDeselect={ resetBackgroundClip }
+ isShownByDefault={ defaultControls.backgroundClip }
+ panelId={ panelId }
+ >
+ {
+ onChange(
+ setImmutably(
+ value,
+ [ 'background', 'backgroundClip' ],
+ newClip
+ )
+ );
+ } }
+ allowedValues={ allowedClipValues }
+ />
+
+ ) }
);
}
diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js
index 04fd9817880423..2512d18e7155d1 100644
--- a/packages/block-editor/src/components/global-styles/color-panel.js
+++ b/packages/block-editor/src/components/global-styles/color-panel.js
@@ -21,7 +21,7 @@ import {
Button,
privateApis as componentsPrivateApis,
} from '@wordpress/components';
-import { useCallback, useRef } from '@wordpress/element';
+import { useCallback, useMemo, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { getValueFromVariable } from '@wordpress/global-styles-engine';
import { reset as resetIcon } from '@wordpress/icons';
@@ -30,6 +30,7 @@ import { reset as resetIcon } from '@wordpress/icons';
* Internal dependencies
*/
import ColorGradientControl from '../colors-gradients/control';
+import { ALL_BACKGROUND_CLIP_VALUES } from '../background-clip-control';
import { useColorsPerOrigin, useGradientsPerOrigin } from './hooks';
import { useToolsPanelDropdownMenuProps } from './utils';
import { setImmutably } from '../../utils/object';
@@ -372,11 +373,17 @@ export default function ColorPanel( {
const showBackgroundPanel = useHasBackgroundColorPanel( settings );
const backgroundColor = decodeValue( inheritedValue?.color?.background );
const userBackgroundColor = decodeValue( value?.color?.background );
- const gradient = decodeValue( inheritedValue?.color?.gradient );
- const userGradient = decodeValue( value?.color?.gradient );
- const hasBackground = () =>
- !! userBackgroundColor ||
- ( ! hasBackgroundGradientSupport && !! userGradient );
+ // Exclude gradient from background panel when it's being used as a text gradient.
+ const inheritedIsTextGradient =
+ inheritedValue?.background?.backgroundClip === 'text';
+ const isTextGradient = value?.background?.backgroundClip === 'text';
+ const gradient = inheritedIsTextGradient
+ ? undefined
+ : decodeValue( inheritedValue?.color?.gradient );
+ const userGradient = isTextGradient
+ ? undefined
+ : decodeValue( value?.color?.gradient );
+ const hasBackground = () => !! userBackgroundColor || !! userGradient;
const setBackgroundColor = ( newColor ) => {
const newValue = setImmutably(
value,
@@ -475,62 +482,134 @@ export default function ColorPanel( {
onChange( changedObject );
};
- const resetTextColor = () => setTextColor( undefined );
+ // Text Gradient (background-clip: text)
+ const clipSetting = settings?.background?.backgroundClip;
+ let allowedClipValues = [];
+ if ( clipSetting === true ) {
+ allowedClipValues = ALL_BACKGROUND_CLIP_VALUES;
+ } else if ( Array.isArray( clipSetting ) ) {
+ allowedClipValues = clipSetting;
+ }
+ const showTextGradient =
+ allowedClipValues.includes( 'text' ) && hasBackgroundGradientSupport;
+ // Text gradient is stored at background.gradient, discriminated from a
+ // regular background gradient by backgroundClip === 'text'.
+ const textGradient = inheritedIsTextGradient
+ ? decodeValue( inheritedValue?.background?.gradient )
+ : undefined;
+ const userTextGradient = isTextGradient
+ ? decodeValue( value?.background?.gradient )
+ : undefined;
+ const hasTextGradientValue = () =>
+ !! userTextGradient && value?.background?.backgroundClip === 'text';
+ const setTextGradient = ( newGradient ) => {
+ let newValue = setImmutably(
+ value,
+ [ 'background', 'gradient' ],
+ encodeGradientValue( newGradient )
+ );
+ newValue = setImmutably(
+ newValue,
+ [ 'background', 'backgroundClip' ],
+ newGradient ? 'text' : undefined
+ );
+ onChange( newValue );
+ };
+ const resetTextAndGradient = () => {
+ let newValue = setImmutably( value, [ 'color', 'text' ], undefined );
+ if ( textColor === linkColor ) {
+ newValue = setImmutably(
+ newValue,
+ [ 'elements', 'link', 'color', 'text' ],
+ undefined
+ );
+ }
+ if ( hasTextGradientValue() ) {
+ newValue = setImmutably(
+ newValue,
+ [ 'background', 'gradient' ],
+ undefined
+ );
+ newValue = setImmutably(
+ newValue,
+ [ 'background', 'backgroundClip' ],
+ undefined
+ );
+ }
+ onChange( newValue );
+ };
// Elements
- const elements = [
- {
- name: 'caption',
- label: __( 'Captions' ),
- showPanel: useHasCaptionPanel( settings ),
- },
- {
- name: 'button',
- label: __( 'Button' ),
- showPanel: useHasButtonPanel( settings ),
- },
- {
- name: 'heading',
- label: __( 'Heading' ),
- showPanel: useHasHeadingPanel( settings ),
- },
- {
- name: 'h1',
- label: __( 'H1' ),
- showPanel: useHasHeadingPanel( settings ),
- },
- {
- name: 'h2',
- label: __( 'H2' ),
- showPanel: useHasHeadingPanel( settings ),
- },
- {
- name: 'h3',
- label: __( 'H3' ),
- showPanel: useHasHeadingPanel( settings ),
- },
- {
- name: 'h4',
- label: __( 'H4' ),
- showPanel: useHasHeadingPanel( settings ),
- },
- {
- name: 'h5',
- label: __( 'H5' ),
- showPanel: useHasHeadingPanel( settings ),
- },
- {
- name: 'h6',
- label: __( 'H6' ),
- showPanel: useHasHeadingPanel( settings ),
- },
- ];
+ const showCaptionPanel = useHasCaptionPanel( settings );
+ const showButtonPanel = useHasButtonPanel( settings );
+ const showHeadingPanel = useHasHeadingPanel( settings );
+ const elements = useMemo(
+ () => [
+ {
+ name: 'caption',
+ label: __( 'Captions' ),
+ showPanel: showCaptionPanel,
+ },
+ {
+ name: 'button',
+ label: __( 'Button' ),
+ showPanel: showButtonPanel,
+ },
+ {
+ name: 'heading',
+ label: __( 'Heading' ),
+ showPanel: showHeadingPanel,
+ },
+ {
+ name: 'h1',
+ label: __( 'H1' ),
+ showPanel: showHeadingPanel,
+ },
+ {
+ name: 'h2',
+ label: __( 'H2' ),
+ showPanel: showHeadingPanel,
+ },
+ {
+ name: 'h3',
+ label: __( 'H3' ),
+ showPanel: showHeadingPanel,
+ },
+ {
+ name: 'h4',
+ label: __( 'H4' ),
+ showPanel: showHeadingPanel,
+ },
+ {
+ name: 'h5',
+ label: __( 'H5' ),
+ showPanel: showHeadingPanel,
+ },
+ {
+ name: 'h6',
+ label: __( 'H6' ),
+ showPanel: showHeadingPanel,
+ },
+ ],
+ [ showCaptionPanel, showButtonPanel, showHeadingPanel ]
+ );
const resetAllFilter = useCallback(
( previousValue ) => {
+ // If a text gradient is active (background.gradient + backgroundClip:
+ // text), it is owned by this panel and must be cleared on reset all.
+ const isTextGradientSet =
+ previousValue?.background?.backgroundClip === 'text';
return {
...previousValue,
color: undefined,
+ ...( isTextGradientSet && {
+ background: {
+ ...previousValue?.background,
+ gradient: undefined,
+ backgroundClip: undefined,
+ },
+ } ),
elements: {
...previousValue?.elements,
link: {
@@ -559,10 +638,14 @@ export default function ColorPanel( {
showTextPanel && {
key: 'text',
label: __( 'Text' ),
- hasValue: hasTextColor,
- resetValue: resetTextColor,
+ hasValue: () => hasTextColor() || hasTextGradientValue(),
+ resetValue: resetTextAndGradient,
isShownByDefault: defaultControls.text,
- indicators: [ textColor ],
+ indicators: [
+ hasTextGradientValue()
+ ? userTextGradient ?? textGradient
+ : textColor,
+ ],
tabs: [
{
key: 'text',
@@ -571,7 +654,16 @@ export default function ColorPanel( {
setValue: setTextColor,
userValue: userTextColor,
},
- ],
+ showTextGradient &&
+ hasGradientColors && {
+ key: 'text-gradient',
+ label: __( 'Gradient' ),
+ inheritedValue: textGradient,
+ setValue: setTextGradient,
+ userValue: userTextGradient,
+ isGradient: true,
+ },
+ ].filter( Boolean ),
},
showBackgroundPanel && {
key: 'background',
diff --git a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js
index 664bc454e8f01f..7a97cd55f64534 100644
--- a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js
+++ b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js
@@ -87,7 +87,7 @@ const StylesTab = ( {
/>
diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js
index 64e093206af48e..09931e8bc7fbde 100644
--- a/packages/block-editor/src/hooks/background.js
+++ b/packages/block-editor/src/hooks/background.js
@@ -127,6 +127,8 @@ function BackgroundInspectorControl( {
} ) {
const resetAllFilter = useCallback(
( attributes ) => {
+ const prevClip = attributes.style?.background?.backgroundClip;
+ const isTextGradient = prevClip === 'text';
const updatedClassName = attributes.className?.includes(
'has-background'
)
@@ -140,7 +142,13 @@ function BackgroundInspectorControl( {
className: updatedClassName,
style: cleanEmptyObject( {
...attributes.style,
- background: undefined,
+ background: isTextGradient
+ ? {
+ gradient:
+ attributes.style?.background?.gradient,
+ backgroundClip: prevClip,
+ }
+ : undefined,
color: backgroundGradientSupported
? {
...attributes.style?.color,
@@ -276,6 +284,9 @@ export function BackgroundImagePanel( {
backgroundSize:
settings?.background?.backgroundSize &&
hasBackgroundSupport( name, 'backgroundSize' ),
+ backgroundClip:
+ settings?.background?.backgroundClip &&
+ hasBackgroundSupport( name, 'backgroundClip' ),
},
};
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index 257f6a190065ea..d494884efe5efe 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -1,6 +1,7 @@
@use "@wordpress/base-styles/default-custom-properties";
@use "@wordpress/base-styles/mixins" as *;
@use "./autocompleters/style.scss" as *;
+@use "./components/background-clip-control/style.scss" as *;
@use "./components/background-image-control/style.scss" as *;
@use "./components/block-alignment-control/style.scss" as *;
@use "./components/block-canvas/style.scss" as *;
diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json
index 7bc491e1658b1b..57871fe27be51d 100644
--- a/packages/block-library/src/heading/block.json
+++ b/packages/block-library/src/heading/block.json
@@ -36,6 +36,11 @@
"style": true,
"width": true
},
+ "background": {
+ "backgroundClip": true,
+ "backgroundImage": true,
+ "gradient": true
+ },
"color": {
"gradients": true,
"link": true,
diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json
index 556c2870557f7c..2c1891f2d06602 100644
--- a/packages/block-library/src/paragraph/block.json
+++ b/packages/block-library/src/paragraph/block.json
@@ -37,6 +37,10 @@
"style": true,
"width": true
},
+ "background": {
+ "backgroundClip": true,
+ "gradient": true
+ },
"color": {
"gradients": true,
"link": true,
diff --git a/packages/global-styles-ui/src/screen-block.tsx b/packages/global-styles-ui/src/screen-block.tsx
index 3918403cdbb39e..9f3e61e4964be9 100644
--- a/packages/global-styles-ui/src/screen-block.tsx
+++ b/packages/global-styles-ui/src/screen-block.tsx
@@ -187,6 +187,12 @@ function ScreenBlock( { name, variation }: ScreenBlockProps ) {
disableAspectRatio = true;
}
+ // In global styles, enable backgroundClip if the block declares support
+ // for it, even when the theme hasn't opted in via settings.
+ const enableBackgroundClip =
+ ! settingsForBlockElement?.background?.backgroundClip &&
+ blockType?.supports?.background?.backgroundClip;
+
const settings = useMemo( () => {
const updatedSettings = structuredClone( settingsForBlockElement );
if ( disableBlockGap ) {
@@ -195,8 +201,19 @@ function ScreenBlock( { name, variation }: ScreenBlockProps ) {
if ( disableAspectRatio ) {
updatedSettings.dimensions.aspectRatio = false;
}
+ if ( enableBackgroundClip ) {
+ updatedSettings.background = {
+ ...updatedSettings.background,
+ backgroundClip: true,
+ };
+ }
return updatedSettings;
- }, [ settingsForBlockElement, disableBlockGap, disableAspectRatio ] );
+ }, [
+ settingsForBlockElement,
+ disableBlockGap,
+ disableAspectRatio,
+ enableBackgroundClip,
+ ] );
const blockVariations = useBlockVariations( name );
const hasBackgroundPanel = useHasBackgroundPanel( settings );
@@ -383,6 +400,9 @@ function ScreenBlock( { name, variation }: ScreenBlockProps ) {
value={ style }
onChange={ setStyle }
settings={ settings }
+ defaultControls={ {
+ backgroundImage: true,
+ } }
defaultValues={ BACKGROUND_BLOCK_DEFAULT_VALUES }
/>
) }
diff --git a/test/e2e/specs/editor/various/contrast-checker.spec.js b/test/e2e/specs/editor/various/contrast-checker.spec.js
index 3a3f49b4e99923..e4acd005a4d81e 100644
--- a/test/e2e/specs/editor/various/contrast-checker.spec.js
+++ b/test/e2e/specs/editor/various/contrast-checker.spec.js
@@ -35,6 +35,7 @@ test.describe( 'Contrast Checker', () => {
} );
const backgroundButton = editorSettings.getByRole( 'button', {
name: 'Background',
+ exact: true,
} );
await expect( textButton ).toBeVisible();
@@ -102,6 +103,7 @@ test.describe( 'Contrast Checker', () => {
} );
const backgroundButton = editorSettings.getByRole( 'button', {
name: 'Background',
+ exact: true,
} );
await expect( textButton ).toBeVisible();
await textButton.click();
@@ -143,6 +145,7 @@ test.describe( 'Contrast Checker', () => {
} );
const backgroundButton = editorSettings.getByRole( 'button', {
name: 'Background',
+ exact: true,
} );
await expect( textButton ).toBeVisible();
await textButton.click();
@@ -191,6 +194,7 @@ test.describe( 'Contrast Checker', () => {
// Set background to black first
const backgroundButton = editorSettings.getByRole( 'button', {
name: 'Background',
+ exact: true,
} );
await backgroundButton.click();
await page.getByRole( 'option', { name: 'Black' } ).click();
@@ -246,6 +250,7 @@ test.describe( 'Contrast Checker', () => {
// Set background to black (poor contrast with black text)
const backgroundButton = editorSettings.getByRole( 'button', {
name: 'Background',
+ exact: true,
} );
await backgroundButton.click();
await page.getByRole( 'option', { name: 'Black' } ).click();
@@ -286,6 +291,7 @@ test.describe( 'Contrast Checker', () => {
// Set background to white (good contrast with black text)
const backgroundButton = editorSettings.getByRole( 'button', {
name: 'Background',
+ exact: true,
} );
await backgroundButton.click();
await page.getByRole( 'option', { name: 'White' } ).click();