Commit db8889cbf95 for woocommerce

commit db8889cbf9536010396e7f4a6ebdc02bb8565e75
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Thu May 21 12:29:52 2026 +0200

    Use the same inner blocks from Product Filters in Add to Cart + Options: Variation Selector (#64887)

    * Update outdated comment

    * Add changelog

    * Introduce Product Filters Dropdown inner block

    * PHPStan

    * Remove unnecessary navigation

    * Make sure selectableItems is an array before iterating through it

    * Update comment about supported attribute types

    * Make sure aria-label always exists

    * Rename block_context to selectable_items

    * Make sure we don't print empty options

    * Add back navigate

    * Small nits

    * Add tests

    * Unify labels

    * Update Add to Cart + Options to use same inner blocks as Product Filters

    * Improve styles

    * Add changelog

    * Set different display limit for Product Filters and Add to Cart + Options

    * Linting

    * Remove no longer used test

    * Undo unnecessary change

    * Clean up

    * Make it so Dropdown inner block can only be added inside the Add to Cart + Options block

    * Further improvements

    * Fix Dropdown block not being visible when loading the editor

    * Typo

    * Get rid of the Variation Selector: Attribute Options block

    * Prevent Varition Selector: Attribute Name from appearing in the list of styles

    * Reduce tests flakiness

    * Remove changes moved to a separate PR

    * Clean up

    * Clean up 2

    * Remove unnecessary changelog file

    * Clean up 3

    * Clean up 4

    * Fix wrong role in template chips

    * Guard variationAttributeOptions before iterating through it

    * Document 'hidden' prop

    * Remove role='button' from AttributeItem inactive rows

    * Simplify types

    * Add extra guards

    * Improve guards

    * Improve storeNamespace guard

    * Remove unnecessary item

    * Linting

    * Simplify code

    * Remove dependency on FilterItemFields

    * Optimize EXCLUDED_BLOCKS list

    * Make Dropdown a top-level block

    * Make sure selected values are taken into account during SSR

    * Make sure elements have the correct labels and ids

    * Strengthen PHP checks

    * Fix wrong labels for Chips block

    * Fix PHPStan notices

    * Improve types

    * Add backwards compatibility to new Variation Selector inner blocks (#65144)

    * Add backwards compatibility to new Variation Selector inner blocks

    * Add changelog

    * Fix wrapper blocks disappearing on migration

    * Migrate all block attributes

    * Migrate all block attributes

    * Update tests so we use the real optionStyle value

    * Add testdox to PHP tests

    * Unify prefixes of context keys in Inner Blocks Protocol (#65153)

    * Unify prefixes of context keys in Inner Blocks Protocol

    * Add testdox to PHP tests

    * Linting

    * Move logic to init chips colors to the Chips inner block (#65200)

    * Move logic to init chips colors to the Chips inner block

    * Add changelog

    * Linting

    * reuse exisiting variables, generate minimal colors, better fallback chain

    ---------

    Co-authored-by: Tung Du <dinhtungdu@gmail.com>

    ---------

    Co-authored-by: Tung Du <dinhtungdu@gmail.com>

diff --git a/plugins/woocommerce/changelog/update-add-to-cart-with-options-filter-inner-blocks b/plugins/woocommerce/changelog/update-add-to-cart-with-options-filter-inner-blocks
new file mode 100644
index 00000000000..d0b1ba8cbb7
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-add-to-cart-with-options-filter-inner-blocks
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Add to Cart + Options: Variation Selector block now can use the same inner blocks as Product Filters
diff --git a/plugins/woocommerce/changelog/update-add-to-cart-with-options-filter-inner-blocks-backwards-compatibility b/plugins/woocommerce/changelog/update-add-to-cart-with-options-filter-inner-blocks-backwards-compatibility
new file mode 100644
index 00000000000..d1e9aa72319
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-add-to-cart-with-options-filter-inner-blocks-backwards-compatibility
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: Add backwards compatibility to new Variation Selector inner blocks
+
+
diff --git a/plugins/woocommerce/changelog/update-inner-blocks-protocol-unify-context-prefix b/plugins/woocommerce/changelog/update-inner-blocks-protocol-unify-context-prefix
new file mode 100644
index 00000000000..79c44e7f382
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-inner-blocks-protocol-unify-context-prefix
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: Unify prefixes of context keys in Inner Blocks Protocol
+
+
diff --git a/plugins/woocommerce/changelog/update-move-init-colors-to-chips-block b/plugins/woocommerce/changelog/update-move-init-colors-to-chips-block
new file mode 100644
index 00000000000..8b4b1aa790c
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-move-init-colors-to-chips-block
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Move logic to init chips colors to the Chips inner block
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/test/block.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/test/block.ts
index 643e6978f6f..47ccd16ac45 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/test/block.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/test/block.ts
@@ -30,7 +30,6 @@ import '../grouped-product-selector/product-item-selector';
 import '../variation-selector';
 import '../variation-selector/attribute';
 import '../variation-selector/attribute-name';
-import '../variation-selector/attribute-options';
 import '../variation-description';

 const mockTemplatePartsHTML: Record< string, string > = {
@@ -325,7 +324,6 @@ describe( 'Add to Cart + Options block', () => {

 		await expectHasBlock( 'Variation Selector (Beta)' );
 		await expectHasBlock( 'Variation Selector: Attribute Name (Beta)' );
-		await expectHasBlock( 'Variation Selector: Attribute Options (Beta)' );
 		await expectHasBlock( 'Variation Selector: Template (Beta)' );
 		await expectHasBlock( 'Variation Description (Beta)' );
 		await expectHasBlock( 'Product Stock Indicator' );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/block.json
deleted file mode 100644
index d745d5632bd..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/block.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-	"name": "woocommerce/add-to-cart-with-options-variation-selector-attribute-options",
-	"title": "Variation Selector: Attribute Options (Beta)",
-	"description": "Display the attribute options associated with a variable product.",
-	"category": "woocommerce-product-elements",
-	"keywords": [ "WooCommerce" ],
-	"ancestor": [
-		"woocommerce/add-to-cart-with-options-variation-selector-attribute"
-	],
-	"attributes": {
-		"optionStyle": {
-			"type": "string",
-			"enum": [ "pills", "dropdown" ],
-			"default": "pills"
-		},
-		"autoselect": {
-			"type": "boolean",
-			"default": false
-		},
-		"disabledAttributesAction": {
-			"type": "string",
-			"enum": [ "disable", "hide" ],
-			"default": "disable"
-		}
-	},
-	"textdomain": "woocommerce",
-	"apiVersion": 3,
-	"$schema": "https://schemas.wp.org/trunk/block.json",
-	"supports": {
-		"inserter": false,
-		"interactivity": true
-	},
-	"usesContext": [
-		"woocommerce/attributeId",
-		"woocommerce/attributeName",
-		"woocommerce/attributeTerms"
-	],
-	"style": "file:../woocommerce/add-to-cart-with-options-variation-selector-attribute-options-style.css"
-}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/edit.tsx
deleted file mode 100644
index 3b4f08f70d8..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/edit.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-/**
- * External dependencies
- */
-import clsx from 'clsx';
-import { useCustomDataContext } from '@woocommerce/shared-context';
-import type { ProductResponseAttributeItem } from '@woocommerce/types';
-import { __ } from '@wordpress/i18n';
-import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
-import { type BlockEditProps } from '@wordpress/blocks';
-import {
-	Disabled,
-	SelectControl,
-	ToggleControl,
-	__experimentalToggleGroupControl as ToggleGroupControl,
-	__experimentalToggleGroupControlOption as ToggleGroupControlOption,
-	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
-	__experimentalToolsPanel as ToolsPanel,
-	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
-	__experimentalToolsPanelItem as ToolsPanelItem,
-} from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import { useThemeColors } from '../../../../shared/hooks/use-theme-colors';
-
-interface Attributes {
-	className?: string;
-	optionStyle: 'pills' | 'dropdown';
-	autoselect: boolean;
-	disabledAttributesAction: 'disable' | 'hide';
-}
-
-function Pills( {
-	id,
-	options,
-}: {
-	id: string;
-	options: SelectControl.Option[];
-} ) {
-	return (
-		<ul
-			id={ id }
-			className="wc-block-add-to-cart-with-options-variation-selector-attribute-options__pills"
-		>
-			{ options.map( ( option, index ) => (
-				<li
-					key={ option.value }
-					className={ clsx(
-						'wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill',
-						{
-							'wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill--selected':
-								index === 0,
-							'wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill--disabled':
-								option.disabled,
-						}
-					) }
-				>
-					{ option.label }
-				</li>
-			) ) }
-		</ul>
-	);
-}
-
-export default function AttributeOptionsEdit(
-	props: BlockEditProps< Attributes >
-) {
-	const { attributes, setAttributes } = props;
-	const { className, optionStyle, autoselect, disabledAttributesAction } =
-		attributes;
-
-	const blockProps = useBlockProps( {
-		className,
-	} );
-
-	// Apply selected variation pill styles based on Site Editor's background and text colors.
-	useThemeColors(
-		'add-to-cart-with-options-variation-selector-attribute-options',
-		( { editorBackgroundColor, editorColor } ) => `
-			:where(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill--selected) {
-				--pill-color: ${ editorBackgroundColor };
-				--pill-background-color: ${ editorColor };
-			}
-		`
-	);
-
-	const { data: attribute } =
-		useCustomDataContext< ProductResponseAttributeItem >( 'attribute' );
-
-	if ( ! attribute ) return null;
-
-	const options = attribute.terms.map( ( term, index ) => ( {
-		value: term.slug,
-		label: term.name,
-		disabled: index > 1 && index === attribute.terms.length - 1,
-	} ) );
-
-	return (
-		<div { ...blockProps }>
-			<InspectorControls>
-				<ToolsPanel
-					label={ __( 'Style', 'woocommerce' ) }
-					resetAll={ () => setAttributes( { optionStyle: 'pills' } ) }
-				>
-					<ToolsPanelItem
-						hasValue={ () => optionStyle !== 'pills' }
-						label={ __( 'Style', 'woocommerce' ) }
-						onDeselect={ () =>
-							setAttributes( { optionStyle: 'pills' } )
-						}
-						isShownByDefault
-					>
-						<ToggleGroupControl
-							label={ __( 'Style', 'woocommerce' ) }
-							value={ optionStyle }
-							onChange={ ( newOptionStyle ) => {
-								if (
-									newOptionStyle === 'pills' ||
-									newOptionStyle === 'dropdown'
-								) {
-									setAttributes( {
-										optionStyle: newOptionStyle,
-									} );
-								}
-							} }
-							isBlock
-							hideLabelFromVision
-							size="__unstable-large"
-						>
-							<ToggleGroupControlOption
-								value="pills"
-								label={ __( 'Pills', 'woocommerce' ) }
-							/>
-							<ToggleGroupControlOption
-								value="dropdown"
-								label={ __( 'Dropdown', 'woocommerce' ) }
-							/>
-						</ToggleGroupControl>
-					</ToolsPanelItem>
-				</ToolsPanel>
-				<ToolsPanel
-					label={ __( 'Auto-select', 'woocommerce' ) }
-					resetAll={ () =>
-						setAttributes( {
-							autoselect: false,
-							disabledAttributesAction: 'disable',
-						} )
-					}
-				>
-					<ToolsPanelItem
-						label={ __(
-							'Auto-select when only one option is available',
-							'woocommerce'
-						) }
-						hasValue={ () => autoselect }
-						onDeselect={ () =>
-							setAttributes( { autoselect: false } )
-						}
-						isShownByDefault
-					>
-						<ToggleControl
-							label={ __(
-								'Auto-select when only one option is available',
-								'woocommerce'
-							) }
-							help={ __(
-								'Automatically select options on page load or after the shopper changes attributes, when only one valid choice is available.',
-								'woocommerce'
-							) }
-							checked={ autoselect }
-							onChange={ () =>
-								setAttributes( { autoselect: ! autoselect } )
-							}
-							__nextHasNoMarginBottom
-						/>
-					</ToolsPanelItem>
-					<ToolsPanelItem
-						label={ __( 'Invalid options', 'woocommerce' ) }
-						hasValue={ () =>
-							disabledAttributesAction !== 'disable'
-						}
-						onDeselect={ () =>
-							setAttributes( {
-								disabledAttributesAction: 'disable',
-							} )
-						}
-						isShownByDefault
-					>
-						<ToggleGroupControl
-							label={ __( 'Invalid options', 'woocommerce' ) }
-							help={ __(
-								'Control the display of invalid options.',
-								'woocommerce'
-							) }
-							value={ disabledAttributesAction }
-							onChange={ ( value ) => {
-								if ( value === 'hide' || value === 'disable' ) {
-									setAttributes( {
-										disabledAttributesAction: value,
-									} );
-								}
-							} }
-							isBlock
-							size="__unstable-large"
-						>
-							<ToggleGroupControlOption
-								value="disable"
-								label={ __( 'Grayed-out', 'woocommerce' ) }
-							/>
-							<ToggleGroupControlOption
-								value="hide"
-								label={ __( 'Hidden', 'woocommerce' ) }
-							/>
-						</ToggleGroupControl>
-					</ToolsPanelItem>
-				</ToolsPanel>
-			</InspectorControls>
-
-			<Disabled>
-				{ optionStyle === 'dropdown' ? (
-					<select
-						id={ attribute.taxonomy }
-						className="wc-block-add-to-cart-with-options-variation-selector-attribute-options__dropdown"
-					>
-						{ options.map( ( option ) => (
-							<option key={ option.value } value={ option.value }>
-								{ option.label }
-							</option>
-						) ) }
-					</select>
-				) : (
-					<Pills id={ attribute.taxonomy } options={ options } />
-				) }
-			</Disabled>
-		</div>
-	);
-}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/index.tsx
deleted file mode 100644
index 0c1f7356a95..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * External dependencies
- */
-import { registerBlockType } from '@wordpress/blocks';
-import { Icon, buttons } from '@wordpress/icons';
-
-/**
- * Internal dependencies
- */
-import metadata from './block.json';
-import AttributeOptionsEdit from './edit';
-import './style.scss';
-
-registerBlockType( metadata, {
-	edit: AttributeOptionsEdit,
-	attributes: metadata.attributes,
-	icon: {
-		src: <Icon icon={ buttons } />,
-	},
-	save: () => null,
-} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/style.scss
deleted file mode 100644
index 0250b5a71de..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/style.scss
+++ /dev/null
@@ -1,72 +0,0 @@
-:where(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pills) {
-	display: inline-flex;
-	gap: $grid-unit-10;
-	list-style: none;
-	padding: 0;
-	margin: 0;
-	flex-wrap: wrap;
-}
-
-:where(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill) {
-	--pill-color: currentColor;
-	--pill-background-color: transparent;
-	background-color: var(--pill-background-color);
-	border-radius: 2px;
-	border: 1px solid color-mix(in srgb, var(--pill-color) 40%, transparent);
-	color: var(--pill-color);
-	cursor: pointer;
-	font-size: 0.8em;
-	padding: 0.25em 0.75em;
-	user-select: none;
-
-	&:where(:has(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input:disabled)) {
-		color: color-mix(in srgb, var(--pill-color) 40%, transparent);
-		cursor: not-allowed;
-		text-decoration: line-through;
-	}
-
-	&:where(:hover:not(:has(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input:disabled))) {
-		background-color: color-mix(
-			in srgb,
-			var(--pill-color) 10%,
-			transparent
-		);
-	}
-
-	&:where(:hover:has(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input:checked)) {
-		background-color: color-mix(
-			in srgb,
-			var(--pill-background-color) 85%,
-			transparent
-		);
-	}
-
-	/* When the pill button is hidden, we also need to hide the label (&) */
-	&:has(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input[hidden]) {
-		display: none;
-	}
-
-	&:where(:focus-within) {
-		outline: 1px solid var(--pill-color);
-		outline-offset: 1px;
-	}
-
-	&:where(:focus-within:has(
-		.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input:checked
-	)) {
-		outline-color: var(--pill-background-color);
-	}
-
-	// We can't make it display: none, otherwise it's not focusable.
-	:where(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input) {
-		height: 0;
-		width: 0;
-		opacity: 0;
-		position: absolute;
-	}
-}
-
-:where(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__dropdown) {
-	height: 3em;
-	padding: 0.25em 1em;
-}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/block.json
index 910b2b1fc16..21c9ac561f0 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/block.json
@@ -6,11 +6,27 @@
 	"keywords": [ "WooCommerce" ],
 	"usesContext": [ "postId" ],
 	"ancestor": [ "woocommerce/add-to-cart-with-options-variation-selector" ],
+	"attributes": {
+		"displayStyle": {
+			"type": "string",
+			"default": "woocommerce/product-filter-chips"
+		},
+		"autoselect": {
+			"type": "boolean",
+			"default": false
+		},
+		"disabledAttributesAction": {
+			"type": "string",
+			"enum": [ "disable", "hide" ],
+			"default": "disable"
+		}
+	},
 	"textdomain": "woocommerce",
 	"apiVersion": 3,
 	"$schema": "https://schemas.wp.org/trunk/block.json",
 	"supports": {
 		"inserter": false,
 		"interactivity": true
-	}
+	},
+	"style": "file:../woocommerce/add-to-cart-with-options-variation-selector-attribute-style.css"
 }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts
index add385f823a..c643193027a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts
@@ -34,9 +34,7 @@ export const ATTRIBUTE_ITEM_TEMPLATE: TemplateArray = [
 							fontSize: 'medium',
 						},
 					],
-					[
-						'woocommerce/add-to-cart-with-options-variation-selector-attribute-options',
-					],
+					[ 'woocommerce/product-filter-chips' ],
 				],
 			],
 		],
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/deprecated.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/deprecated.tsx
new file mode 100644
index 00000000000..ccd48164d15
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/deprecated.tsx
@@ -0,0 +1,125 @@
+/**
+ * External dependencies
+ */
+import { BlockInstance, createBlock } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import save from './save';
+
+const LEGACY_ATTRIBUTE_OPTIONS_BLOCK =
+	'woocommerce/add-to-cart-with-options-variation-selector-attribute-options';
+const INNER_CHIPS = 'woocommerce/product-filter-chips';
+const INNER_DROPDOWN = 'woocommerce/dropdown';
+
+interface MigratedAttributeSettings {
+	displayStyle: string;
+	autoselect: boolean;
+	disabledAttributesAction: 'disable' | 'hide';
+}
+
+function containsLegacyAttributeOptionsBlock(
+	blocks: BlockInstance[]
+): boolean {
+	return blocks.some( ( block ) => {
+		if (
+			block.name === 'core/missing' &&
+			block.attributes.originalName === LEGACY_ATTRIBUTE_OPTIONS_BLOCK
+		) {
+			return true;
+		}
+
+		if ( block.innerBlocks?.length ) {
+			return containsLegacyAttributeOptionsBlock( block.innerBlocks );
+		}
+
+		return false;
+	} );
+}
+
+function migrateInnerBlocks(
+	innerBlocks: BlockInstance[],
+	settings: MigratedAttributeSettings
+): BlockInstance[] {
+	return innerBlocks.flatMap( ( block ) => {
+		if (
+			block.name === 'core/missing' &&
+			block.attributes.originalName === LEGACY_ATTRIBUTE_OPTIONS_BLOCK
+		) {
+			if ( block.originalContent?.includes( '"autoselect":true' ) ) {
+				settings.autoselect = true;
+			}
+			if (
+				block.originalContent?.includes(
+					'"disabledAttributesAction":"hide"'
+				)
+			) {
+				settings.disabledAttributesAction = 'hide';
+			}
+			if (
+				block.originalContent?.includes( '"optionStyle":"dropdown"' )
+			) {
+				settings.displayStyle = INNER_DROPDOWN;
+				return [ createBlock( INNER_DROPDOWN ) ];
+			}
+			settings.displayStyle = INNER_CHIPS;
+			return [ createBlock( INNER_CHIPS ) ];
+		}
+
+		if ( block.innerBlocks?.length ) {
+			return [
+				{
+					...block,
+					innerBlocks: migrateInnerBlocks(
+						block.innerBlocks,
+						settings
+					),
+				},
+			];
+		}
+
+		return [ block ];
+	} );
+}
+
+const deprecated = [
+	{
+		save,
+		isEligible(
+			_attributes: Record< string, unknown >,
+			innerBlocks: BlockInstance[]
+		) {
+			return containsLegacyAttributeOptionsBlock( innerBlocks );
+		},
+		migrate(
+			attributes: Record< string, unknown >,
+			innerBlocks: BlockInstance[]
+		) {
+			const settings: MigratedAttributeSettings = {
+				displayStyle: metadata.attributes.displayStyle.default,
+				autoselect: metadata.attributes.autoselect.default,
+				disabledAttributesAction: metadata.attributes
+					.disabledAttributesAction.default as 'disable' | 'hide',
+			};
+
+			const migratedInnerBlocks = migrateInnerBlocks(
+				innerBlocks,
+				settings
+			);
+
+			return [
+				{
+					...attributes,
+					displayStyle: settings.displayStyle,
+					autoselect: settings.autoselect,
+					disabledAttributesAction: settings.disabledAttributesAction,
+				},
+				migratedInnerBlocks,
+			];
+		},
+	},
+];
+
+export default deprecated;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
index 756cb7f0d88..17bed069750 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
@@ -1,8 +1,10 @@
 /**
  * External dependencies
  */
-import { useState } from '@wordpress/element';
+import { useMemo, useState } from '@wordpress/element';
 import {
+	BlockContextProvider,
+	InspectorControls,
 	useBlockProps,
 	useInnerBlocksProps,
 	store as blockEditorStore,
@@ -12,17 +14,37 @@ import { BlockInstance, type BlockEditProps } from '@wordpress/blocks';
 import { useSelect } from '@wordpress/data';
 import {
 	CustomDataProvider,
+	useCustomDataContext,
 	useProductDataContext,
 } from '@woocommerce/shared-context';
 import { isProductResponseItem } from '@woocommerce/entities';
+import type { ProductResponseAttributeItem } from '@woocommerce/types';
+import { __ } from '@wordpress/i18n';
+import {
+	ToggleControl,
+	__experimentalToggleGroupControl as ToggleGroupControl,
+	__experimentalToggleGroupControlOption as ToggleGroupControlOption,
+	__experimentalToolsPanel as ToolsPanel,
+	__experimentalToolsPanelItem as ToolsPanelItem,
+} from '@wordpress/components';

 /**
  * Internal dependencies
  */
 import { DEFAULT_ATTRIBUTES } from './constants';
+import {
+	DisplayStyleSwitcher,
+	resetDisplayStyleBlock,
+} from '../../../product-filters/components/display-style-switcher';
+import type { SelectableItemsContext } from '../../../../types/type-defs/selectable-items';
+
+const INNER_CHIPS = 'woocommerce/product-filter-chips';

 interface Attributes {
 	className?: string;
+	displayStyle: string;
+	autoselect: boolean;
+	disabledAttributesAction: 'disable' | 'hide';
 }

 type AttributeItemProps = {
@@ -32,39 +54,76 @@ type AttributeItemProps = {
 };

 function AttributeItem( { blocks, isSelected, onSelect }: AttributeItemProps ) {
+	const { data: attribute } =
+		useCustomDataContext< ProductResponseAttributeItem >( 'attribute' );
+
+	const selectableContext = useMemo( () => {
+		let items = [];
+		if (
+			attribute &&
+			Array.isArray( attribute?.terms ) &&
+			attribute.terms.length > 0
+		) {
+			items = attribute.terms.map( ( term ) => ( {
+				id: `${ attribute.taxonomy }-${ term.slug }`,
+				label: term.name,
+				value: term.slug,
+				ariaLabel: term.name,
+			} ) );
+		}
+
+		return {
+			items,
+			selectionMode: 'single' as const,
+			storeNamespace: 'woocommerce/add-to-cart-with-options',
+			groupLabel: '',
+		} satisfies SelectableItemsContext< {
+			label: string;
+			ariaLabel: string;
+		} >;
+	}, [ attribute ] );
+
 	const blockPreviewProps = useBlockPreview( {
 		blocks,
 	} );
-	const innerBlocksProps = useInnerBlocksProps(
-		{ role: 'listitem' },
-		{ templateLock: 'insert' }
-	);
+	const innerBlocksProps = useInnerBlocksProps( { role: 'listitem' } );

-	return (
-		<>
-			{ isSelected ? <div { ...innerBlocksProps } /> : <></> }
+	if ( ! attribute ) {
+		return null;
+	}

-			<div
-				role="listitem"
-				style={ { display: isSelected ? 'none' : undefined } }
-			>
+	return (
+		<BlockContextProvider
+			value={ {
+				'woocommerce/selectableItems': selectableContext,
+			} }
+		>
+			{ isSelected ? (
+				<div { ...innerBlocksProps } />
+			) : (
+				// We don't need these elements to be interactive with the
+				// keyboard because the first attribute blocks are always
+				// editable. We allow clicking on the blocks of other attributes
+				// but it's not critical, so we disable the keyboard events.
+				// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
 				<div
 					{ ...blockPreviewProps }
-					role="button"
 					tabIndex={ 0 }
 					onClick={ onSelect }
-					onKeyDown={ onSelect }
-				/>
-			</div>
-		</>
+				>
+					<div { ...innerBlocksProps } />
+				</div>
+			) }
+		</BlockContextProvider>
 	);
 }

 export default function AttributeItemTemplateEdit(
 	props: BlockEditProps< Attributes >
 ) {
-	const { clientId } = props;
-	const { className } = props.attributes;
+	const { attributes, setAttributes, clientId } = props;
+	const { className, displayStyle, autoselect, disabledAttributesAction } =
+		attributes;

 	const blockProps = useBlockProps( {
 		className,
@@ -80,7 +139,9 @@ export default function AttributeItemTemplateEdit(
 	const { blocks } = useSelect(
 		( select ) => {
 			const { getBlocks } = select( blockEditorStore );
-			return { blocks: getBlocks( clientId ) };
+			return {
+				blocks: getBlocks( clientId ),
+			};
 		},
 		[ clientId ]
 	);
@@ -89,25 +150,139 @@ export default function AttributeItemTemplateEdit(
 		useState< number >();

 	return (
-		<div { ...blockProps } role="list">
-			{ productAttributes.map( ( attribute ) => (
-				<CustomDataProvider
-					key={ attribute.id }
-					id="attribute"
-					data={ attribute }
+		<>
+			<InspectorControls>
+				<ToolsPanel
+					label={ __( 'Style', 'woocommerce' ) }
+					resetAll={ () => {
+						setAttributes( { displayStyle: INNER_CHIPS } );
+						resetDisplayStyleBlock( clientId, INNER_CHIPS );
+					} }
+				>
+					<ToolsPanelItem
+						hasValue={ () => displayStyle !== INNER_CHIPS }
+						label={ __( 'Style', 'woocommerce' ) }
+						onDeselect={ () => {
+							setAttributes( { displayStyle: INNER_CHIPS } );
+							resetDisplayStyleBlock( clientId, INNER_CHIPS );
+						} }
+						isShownByDefault
+					>
+						<div>
+							<span className="screen-reader-text">
+								{ __( 'Style', 'woocommerce' ) }
+							</span>
+							<DisplayStyleSwitcher
+								clientId={ clientId }
+								currentStyle={ displayStyle }
+								onChange={ ( value ) => {
+									setAttributes( {
+										displayStyle: value,
+									} );
+								} }
+							/>
+						</div>
+					</ToolsPanelItem>
+				</ToolsPanel>
+				<ToolsPanel
+					label={ __( 'Auto-select', 'woocommerce' ) }
+					resetAll={ () =>
+						setAttributes( {
+							autoselect: false,
+							disabledAttributesAction: 'disable',
+						} )
+					}
 				>
-					<AttributeItem
-						blocks={ blocks }
-						isSelected={
-							( selectedAttributeItem ||
-								productAttributes[ 0 ]?.id ) === attribute.id
+					<ToolsPanelItem
+						label={ __(
+							'Auto-select when only one option is available',
+							'woocommerce'
+						) }
+						hasValue={ () => autoselect }
+						onDeselect={ () =>
+							setAttributes( { autoselect: false } )
 						}
-						onSelect={ () =>
-							setSelectedAttributeItem( attribute.id )
+						isShownByDefault
+					>
+						<ToggleControl
+							label={ __(
+								'Auto-select when only one option is available',
+								'woocommerce'
+							) }
+							help={ __(
+								'Automatically select options on page load or after the shopper changes attributes, when only one valid choice is available.',
+								'woocommerce'
+							) }
+							checked={ autoselect }
+							onChange={ () =>
+								setAttributes( { autoselect: ! autoselect } )
+							}
+							__nextHasNoMarginBottom
+						/>
+					</ToolsPanelItem>
+					<ToolsPanelItem
+						label={ __( 'Invalid options', 'woocommerce' ) }
+						hasValue={ () =>
+							disabledAttributesAction !== 'disable'
 						}
-					/>
-				</CustomDataProvider>
-			) ) }
-		</div>
+						onDeselect={ () =>
+							setAttributes( {
+								disabledAttributesAction: 'disable',
+							} )
+						}
+						isShownByDefault
+					>
+						<ToggleGroupControl
+							label={ __( 'Invalid options', 'woocommerce' ) }
+							help={ __(
+								'Control the display of invalid options.',
+								'woocommerce'
+							) }
+							value={ disabledAttributesAction }
+							onChange={ ( value ) => {
+								if ( value === 'hide' || value === 'disable' ) {
+									setAttributes( {
+										disabledAttributesAction: value,
+									} );
+								}
+							} }
+							isBlock
+							size="__unstable-large"
+						>
+							<ToggleGroupControlOption
+								value="disable"
+								label={ __( 'Grayed-out', 'woocommerce' ) }
+							/>
+							<ToggleGroupControlOption
+								value="hide"
+								label={ __( 'Hidden', 'woocommerce' ) }
+							/>
+						</ToggleGroupControl>
+					</ToolsPanelItem>
+				</ToolsPanel>
+			</InspectorControls>
+
+			<div { ...blockProps } role="list">
+				{ productAttributes.map( ( attribute ) => (
+					<CustomDataProvider
+						key={ attribute.id }
+						id="attribute"
+						data={ attribute }
+					>
+						<AttributeItem
+							blocks={ blocks }
+							isSelected={
+								( selectedAttributeItem ||
+									productAttributes[ 0 ]?.id ) ===
+								attribute.id
+							}
+							onSelect={ () =>
+								setSelectedAttributeItem( attribute.id )
+							}
+						/>
+					</CustomDataProvider>
+				) ) }
+			</div>
+		</>
 	);
 }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/index.tsx
index 2c8fcad667a..0da5edf1b28 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/index.tsx
@@ -10,6 +10,8 @@ import { Icon, button } from '@wordpress/icons';
 import metadata from './block.json';
 import AttributeItemTemplateEdit from './edit';
 import AttributeItemTemplateSave from './save';
+import deprecated from './deprecated';
+import './style.scss';

 registerBlockType( metadata, {
 	edit: AttributeItemTemplateEdit,
@@ -22,4 +24,5 @@ registerBlockType( metadata, {
 		),
 	},
 	save: AttributeItemTemplateSave,
+	deprecated,
 } );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/style.scss
new file mode 100644
index 00000000000..bc3b19492fd
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/style.scss
@@ -0,0 +1,4 @@
+:where(.wp-block-woocommerce-add-to-cart-with-options-variation-selector-attribute .wc-block-dropdown__select) {
+	height: 3em;
+	padding: 0.25em 1em;
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/test/deprecated.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/test/deprecated.ts
new file mode 100644
index 00000000000..4143c273e34
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/test/deprecated.ts
@@ -0,0 +1,227 @@
+/**
+ * External dependencies
+ */
+import type { BlockInstance } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import metadata from '../block.json';
+import deprecated from '../deprecated';
+
+function createTestBlock( block: {
+	clientId: string;
+	name: string;
+	attributes?: Record< string, unknown >;
+	innerBlocks?: BlockInstance[];
+	originalContent?: string;
+} ): BlockInstance {
+	return {
+		isValid: true,
+		attributes: {},
+		innerBlocks: [],
+		...block,
+	};
+}
+
+jest.mock( '@wordpress/blocks', () => {
+	const actual = jest.requireActual( '@wordpress/blocks' );
+
+	return {
+		...actual,
+		createBlock: jest.fn(
+			(
+				name: string,
+				attributes = {},
+				innerBlocks: BlockInstance[] = []
+			) => ( {
+				clientId: `mock-${ name }`,
+				name,
+				attributes,
+				innerBlocks,
+				isValid: true,
+			} )
+		),
+	};
+} );
+
+const LEGACY_ATTRIBUTE_OPTIONS_BLOCK =
+	'woocommerce/add-to-cart-with-options-variation-selector-attribute-options';
+const INNER_CHIPS = 'woocommerce/product-filter-chips';
+const INNER_DROPDOWN = 'woocommerce/dropdown';
+
+const { isEligible, migrate } = deprecated[ 0 ];
+
+type MigrateResult = [ Record< string, unknown >, BlockInstance[] ];
+
+function runMigrate(
+	attributes: Record< string, unknown >,
+	innerBlocks: BlockInstance[]
+): MigrateResult {
+	return migrate( attributes, innerBlocks ) as MigrateResult;
+}
+
+function createLegacyMissingBlock( originalContent = '' ): BlockInstance {
+	return createTestBlock( {
+		clientId: 'legacy-options',
+		name: 'core/missing',
+		attributes: {
+			originalName: LEGACY_ATTRIBUTE_OPTIONS_BLOCK,
+		},
+		originalContent,
+	} );
+}
+
+function createAttributeNameBlock(): BlockInstance {
+	return createTestBlock( {
+		clientId: 'attribute-name',
+		name: 'woocommerce/add-to-cart-with-options-variation-selector-attribute-name',
+	} );
+}
+
+function createGroupWithInnerBlocks(
+	innerBlocks: BlockInstance[]
+): BlockInstance {
+	return createTestBlock( {
+		clientId: 'group',
+		name: 'core/group',
+		innerBlocks,
+	} );
+}
+
+describe( 'Variation Selector Attribute block deprecation', () => {
+	describe( 'isEligible', () => {
+		it( 'returns false when there is no legacy attribute options block', () => {
+			const innerBlocks = [ createAttributeNameBlock() ];
+
+			expect( isEligible( {}, innerBlocks ) ).toBe( false );
+		} );
+
+		it( 'returns true when a legacy attribute options block is a direct inner block', () => {
+			const innerBlocks = [
+				createAttributeNameBlock(),
+				createLegacyMissingBlock(),
+			];
+
+			expect( isEligible( {}, innerBlocks ) ).toBe( true );
+		} );
+
+		it( 'returns true when a legacy attribute options block is nested in inner blocks', () => {
+			const innerBlocks = [
+				createGroupWithInnerBlocks( [
+					createAttributeNameBlock(),
+					createLegacyMissingBlock(),
+				] ),
+			];
+
+			expect( isEligible( {}, innerBlocks ) ).toBe( true );
+		} );
+	} );
+
+	describe( 'migrate', () => {
+		it( 'replaces a legacy dropdown options block and sets displayStyle to dropdown', () => {
+			const attributes = {
+				displayStyle: INNER_CHIPS,
+				autoselect: true,
+				disabledAttributesAction: 'hide' as const,
+				customAttribute: 'preserved',
+			};
+			const innerBlocks = [
+				createAttributeNameBlock(),
+				createLegacyMissingBlock(
+					'<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-options {"optionStyle":"dropdown"} /-->'
+				),
+			];
+
+			const [ migratedAttributes, migratedInnerBlocks ] = runMigrate(
+				attributes,
+				innerBlocks
+			);
+
+			expect( migratedAttributes ).toEqual( {
+				...attributes,
+				displayStyle: INNER_DROPDOWN,
+				autoselect: metadata.attributes.autoselect.default,
+				disabledAttributesAction:
+					metadata.attributes.disabledAttributesAction.default,
+			} );
+			expect( migratedInnerBlocks ).toHaveLength( 2 );
+			expect( migratedInnerBlocks[ 0 ].name ).toBe(
+				'woocommerce/add-to-cart-with-options-variation-selector-attribute-name'
+			);
+			expect( migratedInnerBlocks[ 1 ].name ).toBe( INNER_DROPDOWN );
+		} );
+
+		it( 'replaces a legacy chips options block and sets displayStyle to chips', () => {
+			const attributes = {
+				displayStyle: INNER_DROPDOWN,
+			};
+			const innerBlocks = [
+				createLegacyMissingBlock(
+					'<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-options {"optionStyle":"pills"} /-->'
+				),
+			];
+
+			const [ migratedAttributes, migratedInnerBlocks ] = runMigrate(
+				attributes,
+				innerBlocks
+			);
+
+			expect( migratedAttributes.displayStyle ).toBe( INNER_CHIPS );
+			expect( migratedInnerBlocks ).toHaveLength( 1 );
+			expect( migratedInnerBlocks[ 0 ].name ).toBe( INNER_CHIPS );
+		} );
+
+		it( 'defaults to chips when legacy block content does not indicate dropdown', () => {
+			const innerBlocks = [ createLegacyMissingBlock() ];
+
+			const [ migratedAttributes, migratedInnerBlocks ] = runMigrate(
+				{},
+				innerBlocks
+			);
+
+			expect( migratedAttributes.displayStyle ).toBe( INNER_CHIPS );
+			expect( migratedInnerBlocks[ 0 ].name ).toBe( INNER_CHIPS );
+		} );
+
+		it( 'uses block.json defaults for autoselect and disabledAttributesAction', () => {
+			const innerBlocks = [ createLegacyMissingBlock() ];
+
+			const [ migratedAttributes ] = runMigrate( {}, innerBlocks );
+
+			expect( migratedAttributes.autoselect ).toBe(
+				metadata.attributes.autoselect.default
+			);
+			expect( migratedAttributes.disabledAttributesAction ).toBe(
+				metadata.attributes.disabledAttributesAction.default
+			);
+		} );
+
+		it( 'replaces a nested legacy attribute options block', () => {
+			const innerBlocks = [
+				createGroupWithInnerBlocks( [
+					createAttributeNameBlock(),
+					createLegacyMissingBlock(
+						'<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-options {"optionStyle":"dropdown"} /-->'
+					),
+				] ),
+			];
+
+			const [ migratedAttributes, migratedInnerBlocks ] = runMigrate(
+				{},
+				innerBlocks
+			);
+
+			expect( migratedAttributes.displayStyle ).toBe( INNER_DROPDOWN );
+			expect( migratedInnerBlocks ).toHaveLength( 1 );
+			expect( migratedInnerBlocks[ 0 ].name ).toBe( 'core/group' );
+			expect( migratedInnerBlocks[ 0 ].innerBlocks ).toHaveLength( 2 );
+			expect( migratedInnerBlocks[ 0 ].innerBlocks[ 0 ].name ).toBe(
+				'woocommerce/add-to-cart-with-options-variation-selector-attribute-name'
+			);
+			expect( migratedInnerBlocks[ 0 ].innerBlocks[ 1 ].name ).toBe(
+				INNER_DROPDOWN
+			);
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/edit.tsx
index ae64c819e48..57a662dece9 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/edit.tsx
@@ -23,7 +23,6 @@ export default function AddToCartWithOptionsVariationSelectorEdit(
 	} );
 	const innerBlocksProps = useInnerBlocksProps( blockProps, {
 		template: ATTRIBUTE_ITEM_TEMPLATE,
-		templateLock: 'all',
 	} );

 	return <div { ...innerBlocksProps } />;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
index 049329d1a9c..2037075c6f7 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
@@ -8,7 +8,6 @@ import {
 	getElement,
 } from '@wordpress/interactivity';
 import { SelectedAttributes } from '@woocommerce/stores/woocommerce/cart';
-import type { ChangeEvent } from 'react';
 import '@woocommerce/stores/woocommerce/products';
 import type { ProductsStore } from '@woocommerce/stores/woocommerce/products';
 import type { ProductResponseItem } from '@woocommerce/types';
@@ -16,35 +15,36 @@ import type { ProductResponseItem } from '@woocommerce/types';
 /**
  * Internal dependencies
  */
-import type {
-	AddToCartWithOptionsStore,
-	Context as AddToCartWithOptionsStoreContext,
-} from '../frontend';
 import {
 	normalizeAttributeName,
 	attributeNamesMatch,
 	getVariationAttributeValue,
 } from '../../../base/utils/variations/attribute-matching';
-import setStyles from './set-styles';
+import type {
+	AddToCartWithOptionsStore,
+	Context as AddToCartWithOptionsStoreContext,
+} from '../frontend';
+import type { SelectableItem } from '../../../types/type-defs/selectable-items';

-type Option = {
-	value: string;
+type VariationOptionItem = {
+	id: string;
 	label: string;
-	isSelected: boolean;
+	value: string;
+	ariaLabel?: string;
 };

 type Context = AddToCartWithOptionsStoreContext & {
 	name: string;
 	selectedValue: string | null;
-	option: Option;
-	options: Option[];
+	variationAttributeOptions: VariationOptionItem[];
 	autoselect: boolean;
+	disabledAttributesAction?: 'disable' | 'hide';
 };

-// Set selected pill styles for proper contrast.
-setStyles();
+type ToggleContext = Context & {
+	item?: SelectableItem;
+};

-// Stores are locked to prevent 3PD usage until the API is stable.
 const universalLock =
 	'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';

@@ -54,14 +54,6 @@ const { state: productsState } = store< ProductsStore >(
 	{ lock: universalLock }
 );

-/**
- * Check if the attribute value is valid given the other selected attributes and
- * the available variations.
- *
- * To know if an attribute value is valid given the other selected attributes,
- * we make sure there is at least one available variation matching the current
- * selected attributes and the attribute value being checked.
- */
 const isAttributeValueValid = ( {
 	attributeName,
 	attributeValue,
@@ -189,16 +181,12 @@ export type VariableProductAddToCartWithOptionsStore =
 	AddToCartWithOptionsStore & {
 		state: {
 			selectedAttributes: SelectedAttributes[];
-			isOptionSelected: boolean;
-			isOptionDisabled: boolean;
+			selectableItems: readonly SelectableItem[];
 		};
 		actions: {
 			setAttribute: ( attribute: string, value: string ) => void;
 			removeAttribute: ( attribute: string ) => void;
-			handlePillClick: () => void;
-			handleDropdownChange: (
-				event: ChangeEvent< HTMLSelectElement >
-			) => void;
+			toggle: ( item?: SelectableItem ) => void;
 			autoselectAttributes: ( args: {
 				includedAttributes?: string[];
 				excludedAttributes?: string[];
@@ -221,31 +209,46 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
 				if ( ! context ) {
 					return [];
 				}
-				return context.selectedAttributes;
-			},
-			get isOptionSelected() {
-				const { selectedAttributes, option, name } =
-					getContext< Context >();
-
-				return selectedAttributes.some( ( attrObject ) => {
-					return (
-						attributeNamesMatch( attrObject.attribute, name ) &&
-						attrObject.value === option.value
-					);
-				} );
+				return context.selectedAttributes || [];
 			},
-			get isOptionDisabled() {
-				const { name, option, selectedAttributes } =
-					getContext< Context >();
-
-				if ( option.value === '' ) {
-					return false;
+			get selectableItems(): readonly SelectableItem[] {
+				const context = getContext< Context >();
+				if ( ! context ) {
+					return [];
+				}
+				const {
+					name,
+					disabledAttributesAction,
+					variationAttributeOptions,
+				} = context;
+				const { selectedAttributes } = state;
+				const hideInvalid = disabledAttributesAction === 'hide';
+
+				if ( ! Array.isArray( variationAttributeOptions ) ) {
+					return [];
 				}

-				return ! isAttributeValueValid( {
-					attributeName: name,
-					attributeValue: option.value,
-					selectedAttributes,
+				return variationAttributeOptions.map( ( row, index ) => {
+					const disabled = ! isAttributeValueValid( {
+						attributeName: name,
+						attributeValue: row.value,
+						selectedAttributes,
+					} );
+					const selected = selectedAttributes.some(
+						( attrObject ) =>
+							attributeNamesMatch( attrObject.attribute, name ) &&
+							attrObject.value === row.value
+					);
+					return {
+						id: row.id,
+						label: row.label,
+						value: row.value,
+						ariaLabel: row.ariaLabel || row.label,
+						index,
+						selected,
+						disabled,
+						hidden: hideInvalid && disabled,
+					};
 				} );
 			},
 		},
@@ -292,28 +295,32 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
 					selectedAttributes.splice( index, 1 );
 				}
 			},
-			handlePillClick() {
-				const context = getContext< Context >();
+			toggle( itemArg?: SelectableItem | Event ) {
+				const context = getContext< ToggleContext >();
+				const item =
+					itemArg && ! ( itemArg instanceof Event )
+						? itemArg
+						: context.item;
+				if ( ! item || item.hidden || item.disabled ) {
+					return;
+				}
+
+				const { name } = context;
+				const { selectedAttributes } = state;
+				const isCurrentlySelected = selectedAttributes.some(
+					( attrObject ) =>
+						attributeNamesMatch( attrObject.attribute, name ) &&
+						attrObject.value === item.value
+				);

-				if ( state.isOptionSelected ) {
+				if ( isCurrentlySelected ) {
 					context.selectedValue = '';
+					actions.setAttribute( name, '' );
 				} else {
-					context.selectedValue = context.option.value;
-				}
-				actions.setAttribute( context.name, context.selectedValue );
-				if ( context.selectedValue !== '' ) {
+					context.selectedValue = item.value;
+					actions.setAttribute( name, item.value );
 					actions.autoselectAttributes( {
-						excludedAttributes: [ context.name ],
-					} );
-				}
-			},
-			handleDropdownChange( event: ChangeEvent< HTMLSelectElement > ) {
-				const context = getContext< Context >();
-				context.selectedValue = event.currentTarget.value;
-				actions.setAttribute( context.name, context.selectedValue );
-				if ( context.selectedValue !== '' ) {
-					actions.autoselectAttributes( {
-						excludedAttributes: [ context.name ],
+						excludedAttributes: [ name ],
 					} );
 				}
 			},
@@ -324,13 +331,13 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
 				includedAttributes?: Array< string >;
 				excludedAttributes?: Array< string >;
 			} = {} ) {
-				const { autoselect, selectedAttributes } =
-					getContext< Context >();
-
-				if ( ! autoselect ) {
+				const context = getContext< Context >();
+				if ( ! context || ! context.autoselect ) {
 					return;
 				}

+				const { selectedAttributes } = state;
+
 				const { mainProductInContext: product } = productsState;
 				if ( ! product ) {
 					return;
@@ -389,10 +396,14 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
 		callbacks: {
 			setDefaultSelectedAttribute() {
 				const context = getContext< Context >();
+				if ( ! context.name ) {
+					return;
+				}

 				if ( context.selectedValue ) {
 					actions.setAttribute( context.name, context.selectedValue );
 				}
+
 				actions.autoselectAttributes( {
 					includedAttributes: [ context.name ],
 				} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/set-styles.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/set-styles.ts
deleted file mode 100644
index 44685e0ce41..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/set-styles.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Recursively searches up the DOM tree to find the first non-transparent color of the specified type.
- *
- * @param element   - The DOM element to check for color.
- * @param colorType - Whether to check for color or background color.
- *
- * @return The computed RGB color string or null if not found.
- */
-function getClosestColor(
-	element: Element | null,
-	colorType: 'color' | 'backgroundColor'
-): string | null {
-	if ( ! element ) {
-		return null;
-	}
-	const computedColor = window.getComputedStyle( element )[ colorType ];
-
-	// Skip transparent or default "empty" colors.
-	if (
-		computedColor !== 'rgba(0, 0, 0, 0)' &&
-		computedColor !== 'transparent'
-	) {
-		// Extract RGB values from the color string.
-		const rgbValues = computedColor.match( /\d+/g );
-
-		if ( ! rgbValues || rgbValues.length < 3 ) {
-			return null;
-		}
-
-		const [ red, green, blue ] = rgbValues.slice( 0, 3 );
-		return `rgb(${ red }, ${ green }, ${ blue })`;
-	}
-
-	// If current element has transparent color, check parent element.
-	return getClosestColor( element.parentElement, colorType );
-}
-
-/**
- * Sets the appropriate styles for variation selector pills to ensure
- * visibility in both light and dark themes.
- *
- * This function swaps the text and background colors for selected pills
- * to create better contrast in all theme environments.
- */
-function setStyles(): void {
-	/**
-	 * Get the background color of the body then set it as the background color
-	 * of the Variation Selector selected pills.
-	 *
-	 * We only set the background color, instead of the whole background. As
-	 * we only provide the option to customize the background color.
-	 */
-
-	// For simplicity, we only consider the background color of the first Variation Selector pills.
-	const pillsContainer = document.querySelector(
-		'.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pills'
-	);
-
-	if ( ! pillsContainer ) {
-		return;
-	}
-
-	const style = document.createElement( 'style' );
-
-	const selectedPillColor =
-		getClosestColor( pillsContainer, 'backgroundColor' ) || '#fff';
-	const selectedPillBackgroundColor =
-		getClosestColor( pillsContainer, 'color' ) || '#000';
-
-	// We use :where here to reduce specificity so customized colors and theme CSS take priority.
-	style.appendChild(
-		document.createTextNode(
-			`:where(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill:has(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input:checked)) {
-				--pill-color: ${ selectedPillColor };
-				--pill-background-color: ${ selectedPillBackgroundColor };
-			}`
-		)
-	);
-
-	document.head.appendChild( style );
-}
-
-export default setStyles;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/block.json
new file mode 100644
index 00000000000..37a2be40114
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/block.json
@@ -0,0 +1,21 @@
+{
+	"$schema": "https://schemas.wp.org/trunk/block.json",
+	"name": "woocommerce/dropdown",
+	"title": "Dropdown",
+	"description": "Display options in a dropdown.",
+	"category": "woocommerce",
+	"keywords": [ "WooCommerce" ],
+	"textdomain": "woocommerce",
+	"apiVersion": 3,
+	"ancestor": [
+		"woocommerce/add-to-cart-with-options-variation-selector-attribute"
+	],
+	"supports": {
+		"interactivity": true
+	},
+	"usesContext": [ "woocommerce/selectableItems", "woocommerce/attributeId" ],
+	"attributes": {},
+	"viewScriptModule": "woocommerce/dropdown",
+	"style": "file:../woocommerce/dropdown-style.css",
+	"editorStyle": "file:../woocommerce/dropdown-editor.css"
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/edit.tsx
new file mode 100644
index 00000000000..ab202a0f39a
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/edit.tsx
@@ -0,0 +1,99 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import clsx from 'clsx';
+import { decodeHtmlEntities } from '@woocommerce/utils';
+import { Disabled } from '@wordpress/components';
+import { useBlockProps } from '@wordpress/block-editor';
+import type { BlockEditProps } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import type { SelectableItemsBlockContext } from '../../types/type-defs/selectable-items';
+import './editor.scss';
+import './style.scss';
+
+export type BlockAttributes = Record< string, never >;
+
+export type EditProps = BlockEditProps< BlockAttributes > & {
+	context: SelectableItemsBlockContext< {
+		label?: string;
+		ariaLabel?: string;
+	} >;
+};
+
+function getOptionLabel( item: {
+	label: string | unknown;
+	ariaLabel?: string;
+} ): string {
+	if ( typeof item.label === 'string' && item.label.trim().length > 0 ) {
+		return decodeHtmlEntities( item.label.trim() );
+	}
+	if (
+		typeof item.ariaLabel === 'string' &&
+		item.ariaLabel.trim().length > 0
+	) {
+		return item.ariaLabel.trim();
+	}
+	return '';
+}
+
+const Edit = ( props: EditProps ): JSX.Element => {
+	const { context } = props;
+	const selectableItems = context?.[ 'woocommerce/selectableItems' ] ?? {};
+	const isLoading = selectableItems.isLoading ?? false;
+	const items = Array.isArray( selectableItems.items )
+		? selectableItems.items
+		: [];
+
+	const blockProps = useBlockProps( {
+		className: clsx( 'wc-block-dropdown', {
+			'is-loading': isLoading,
+		} ),
+	} );
+
+	return (
+		<div { ...blockProps }>
+			<Disabled>
+				<fieldset className="wc-block-dropdown__fieldset">
+					<legend className="screen-reader-text">
+						{ __( 'Choose an option', 'woocommerce' ) }
+					</legend>
+					{ isLoading ? (
+						<div className="wc-block-dropdown__skeleton">
+							<div className="wc-block-dropdown__skeleton-option" />
+						</div>
+					) : (
+						<select
+							className="wc-block-dropdown__select"
+							aria-label={ selectableItems.groupLabel }
+						>
+							<option value="">
+								{ __( 'Choose an option', 'woocommerce' ) }
+							</option>
+							{ items.map( ( item, index ) => {
+								const optionLabel = getOptionLabel( item );
+								if ( ! optionLabel ) {
+									return null;
+								}
+								return (
+									<option
+										key={ index }
+										value={ item.value }
+										disabled={ !! item.disabled }
+									>
+										{ optionLabel }
+									</option>
+								);
+							} ) }
+						</select>
+					) }
+				</fieldset>
+			</Disabled>
+		</div>
+	);
+};
+
+export default Edit;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/editor.scss
new file mode 100644
index 00000000000..a98a362ce1e
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/editor.scss
@@ -0,0 +1,16 @@
+.wc-block-dropdown__skeleton-option {
+	height: 1.75em;
+	border-radius: 2px;
+	background: color-mix(in srgb, currentColor 8%, transparent);
+	animation: wc-block-dropdown-pulse 1.2s ease-in-out infinite
+		alternate;
+}
+
+@keyframes wc-block-dropdown-pulse {
+	from {
+		opacity: 0.55;
+	}
+	to {
+		opacity: 1;
+	}
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/frontend.ts
new file mode 100644
index 00000000000..ead8dce0af0
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/frontend.ts
@@ -0,0 +1,95 @@
+/**
+ * External dependencies
+ */
+import { store, getContext } from '@wordpress/interactivity';
+
+/**
+ * Internal dependencies
+ */
+import type { SelectableItem } from '../../types/type-defs/selectable-items';
+
+type DropdownContext = {
+	storeNamespace: string;
+};
+
+type SelectableParent = {
+	state: {
+		selectableItems: readonly SelectableItem[];
+	};
+	actions: {
+		toggle: ( item?: SelectableItem ) => void;
+		navigate?: () => void;
+	};
+};
+
+function isToggleableItem(
+	item: SelectableItem | undefined
+): item is SelectableItem & { value: string } {
+	return (
+		!! item &&
+		typeof item.value === 'string' &&
+		item.value.length > 0 &&
+		! item.disabled &&
+		! item.hidden
+	);
+}
+
+store(
+	'woocommerce/dropdown',
+	{
+		state: {
+			get selectValue(): string {
+				const { storeNamespace } = getContext< DropdownContext >();
+				const parent = store< SelectableParent >( storeNamespace );
+				const items = Array.isArray( parent.state.selectableItems )
+					? parent.state.selectableItems
+					: [];
+				const selected = items.find( ( row ) => row.selected );
+				return selected?.value ?? '';
+			},
+		},
+		actions: {
+			onDropdownChange( event: Event ) {
+				const target = event.currentTarget;
+				if ( ! ( target instanceof HTMLSelectElement ) ) {
+					return;
+				}
+				const { storeNamespace } = getContext< DropdownContext >();
+				const parent = store< SelectableParent >( storeNamespace );
+				const value = target.value;
+
+				// Choosing the empty <option> unselects the selected option.
+				if ( value === '' ) {
+					const items = Array.isArray( parent.state.selectableItems )
+						? parent.state.selectableItems
+						: [];
+					const selected = items.find( ( row ) => row.selected );
+					if (
+						selected &&
+						typeof parent.actions.toggle === 'function'
+					) {
+						parent.actions.toggle( selected );
+					}
+					return;
+				}
+
+				const items = Array.isArray( parent.state.selectableItems )
+					? parent.state.selectableItems
+					: [];
+				const row = items.find( ( item ) => item.value === value );
+
+				if ( ! isToggleableItem( row ) ) {
+					if ( typeof parent.actions.navigate === 'function' ) {
+						parent.actions.navigate();
+					}
+					return;
+				}
+
+				parent.actions.toggle( row );
+			},
+		},
+	},
+	{ lock: true }
+);
+
+export type { DropdownContext };
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/index.tsx
new file mode 100644
index 00000000000..0987f575343
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/index.tsx
@@ -0,0 +1,18 @@
+/**
+ * External dependencies
+ */
+import { productFilterOptions } from '@woocommerce/icons';
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import Edit from './edit';
+import Save from './save';
+
+registerBlockType( metadata, {
+	edit: Edit,
+	icon: productFilterOptions,
+	save: Save,
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/save.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/save.tsx
new file mode 100644
index 00000000000..51dec4dfa0a
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/save.tsx
@@ -0,0 +1,14 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+const Save = () => {
+	const blockProps = useBlockProps.save( {
+		className: 'wc-block-dropdown',
+	} );
+
+	return <div { ...blockProps } />;
+};
+
+export default Save;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/style.scss
new file mode 100644
index 00000000000..b959a272bed
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/dropdown/style.scss
@@ -0,0 +1,3 @@
+:where(.wc-block-dropdown__fieldset) {
+	display: contents;
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/components/display-style-switcher/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/components/display-style-switcher/index.tsx
index c32522b9e95..9bbbaa61368 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/components/display-style-switcher/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/components/display-style-switcher/index.tsx
@@ -14,6 +14,27 @@ import {
 	__experimentalToggleGroupControlOption as ToggleGroupControlOption,
 } from '@wordpress/components';

+/**
+ * Internal dependencies
+ */
+import { DISPLAY_STYLE_SWITCHER_EXCLUDED_BLOCK_NAMES } from '../../constants';
+
+function isDisplayStyleCandidate(
+	blockTypeName: string,
+	parentBlockName: string | undefined,
+	blockAncestor: readonly string[] | undefined
+): boolean {
+	if ( ! parentBlockName ) {
+		return false;
+	}
+	if (
+		DISPLAY_STYLE_SWITCHER_EXCLUDED_BLOCK_NAMES.includes( blockTypeName )
+	) {
+		return false;
+	}
+	return blockAncestor?.includes( parentBlockName ) ?? false;
+}
+
 export const DisplayStyleSwitcher = ( {
 	clientId,
 	currentStyle,
@@ -26,12 +47,13 @@ export const DisplayStyleSwitcher = ( {
 	const filterBlock = select( 'core/block-editor' ).getBlock( clientId );
 	const parentBlockName = filterBlock?.name;

-	const displayStyleOptions = getBlockTypes().filter( ( blockType ) => {
-		if ( parentBlockName ) {
-			return blockType.ancestor?.includes( parentBlockName );
-		}
-		return [];
-	} );
+	const displayStyleOptions = getBlockTypes().filter( ( blockType ) =>
+		isDisplayStyleCandidate(
+			blockType.name,
+			parentBlockName,
+			blockType.ancestor
+		)
+	);

 	const { insertBlock, replaceBlock } = useDispatch( 'core/block-editor' );

@@ -100,7 +122,11 @@ export function resetDisplayStyleBlock(

 	const parentBlockName = filterBlock.name;
 	const displayStyleOptions = getBlockTypes().filter( ( blockType ) =>
-		blockType.ancestor?.includes( parentBlockName )
+		isDisplayStyleCandidate(
+			blockType.name,
+			parentBlockName,
+			blockType.ancestor
+		)
 	);

 	const currentStyle = displayStyleOptions.find( ( blockType ) =>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts
index cd4c5154d6e..d4a91c2ef7f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/constants.ts
@@ -5,5 +5,15 @@ export const EXCLUDED_BLOCKS = [
 	'woocommerce/product-filter-price',
 	'woocommerce/product-filter-status',
 	'woocommerce/product-collection',
+	'woocommerce/add-to-cart-form',
+	'woocommerce/add-to-cart-with-options',
 	'core/query',
 ];
+
+/**
+ * Block types that declare a product filter or variation-selector parent in `ancestor`
+ * but are not interchangeable display styles for the style toggle (chips vs dropdown).
+ */
+export const DISPLAY_STYLE_SWITCHER_EXCLUDED_BLOCK_NAMES: string[] = [
+	'woocommerce/add-to-cart-with-options-variation-selector-attribute-name',
+];
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/edit.tsx
index c643ffdb45e..ef58738d712 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/edit.tsx
@@ -33,7 +33,7 @@ const Edit = () => {
 			<InitialDisabled>
 				<BlockContextProvider
 					value={ {
-						woocommerceRemovableItems: {
+						'woocommerce/removableItems': {
 							items: filtersPreview,
 							storeNamespace: 'woocommerce/product-filters',
 						} satisfies RemovableItemsContext,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx
index a797ea532ba..8ffe4a6cf7d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx
@@ -211,7 +211,7 @@ const Edit = ( props: EditProps ) => {
 			<InitialDisabled>
 				<BlockContextProvider
 					value={ {
-						woocommerceSelectableItems: {
+						'woocommerce/selectableItems': {
 							items:
 								attributeOptions.length === 0 && isPreview
 									? attributeOptionsPreview
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json
index e9010e3dc26..d674971bd2f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json
@@ -16,7 +16,7 @@
 	"supports": {
 		"interactivity": true
 	},
-	"usesContext": [ "woocommerceSelectableItems" ],
+	"usesContext": [ "woocommerce/selectableItems" ],
 	"attributes": {
 		"optionElementBorder": {
 			"type": "string",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx
index 9d62a022f74..5f6415965e6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx
@@ -49,7 +49,7 @@ const CheckboxListEdit = ( props: EditProps ): JSX.Element => {
 		customOptionElement,
 		customLabelElement,
 	} = attributes;
-	const selectableItems = context?.woocommerceSelectableItems ?? {};
+	const selectableItems = context?.[ 'woocommerce/selectableItems' ] ?? {};
 	const isLoading = selectableItems.isLoading ?? false;
 	const items = Array.isArray( selectableItems.items )
 		? selectableItems.items
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/__tests__/frontend.test.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/__tests__/frontend.test.ts
index b031b620259..f5d1ae8117a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/__tests__/frontend.test.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/__tests__/frontend.test.ts
@@ -4,7 +4,9 @@
 import type { ChipsStore } from '../frontend';

 const mockGetContext = jest.fn();
+const mockGetElement = jest.fn();
 const mockParentToggle = jest.fn();
+const mockGetClosestColor = jest.fn();

 let mockRegisteredStore: ChipsStore | null = null;

@@ -34,6 +36,7 @@ jest.mock(
 	'@wordpress/interactivity',
 	() => ( {
 		getContext: mockGetContext,
+		getElement: mockGetElement,
 		store: jest.fn( ( _name, definition ) => {
 			if ( definition ) {
 				mockRegisteredStore = definition;
@@ -45,11 +48,17 @@ jest.mock(
 	{ virtual: true }
 );

+jest.mock( '../../../utils/get-closest-color', () => ( {
+	getClosestColor: ( ...args: unknown[] ) => mockGetClosestColor( ...args ),
+} ) );
+
 describe( 'product filter chips interactivity store', () => {
 	beforeEach( () => {
 		jest.resetModules();
 		mockGetContext.mockReset();
+		mockGetElement.mockReset();
 		mockParentToggle.mockReset();
+		mockGetClosestColor.mockReset();
 		mockRegisteredStore = null;

 		jest.isolateModules( () => {
@@ -146,4 +155,99 @@ describe( 'product filter chips interactivity store', () => {

 		expect( mockParentToggle ).not.toHaveBeenCalled();
 	} );
+
+	it( 'sets chip CSS variables when not already defined', () => {
+		if ( ! mockRegisteredStore ) {
+			throw new Error( 'Chips store was not registered.' );
+		}
+
+		const element = document.createElement( 'div' );
+
+		mockGetElement.mockReturnValue( { ref: element } );
+		mockGetClosestColor.mockImplementation(
+			(
+				_el: Element,
+				colorType: 'color' | 'backgroundColor'
+			): string | null => {
+				return colorType === 'backgroundColor'
+					? 'rgb(255, 255, 255)'
+					: 'rgb(0, 0, 0)';
+			}
+		);
+
+		mockRegisteredStore.callbacks.initColors();
+
+		expect(
+			element.style.getPropertyValue(
+				'--wc-product-filter-chips-background'
+			)
+		).toBe( 'rgb(255, 255, 255)' );
+		expect(
+			element.style.getPropertyValue( '--wc-product-filter-chips-text' )
+		).toBe( 'rgb(0, 0, 0)' );
+	} );
+
+	it( 'does not calculate chip colors when already defined', () => {
+		if ( ! mockRegisteredStore ) {
+			throw new Error( 'Chips store was not registered.' );
+		}
+
+		const element = document.createElement( 'div' );
+		element.style.setProperty(
+			'--wc-product-filter-chips-text',
+			'var(--wp--preset--color--contrast)'
+		);
+		element.style.setProperty(
+			'--wc-product-filter-chips-background',
+			'var(--wp--preset--color--base)'
+		);
+
+		mockGetElement.mockReturnValue( { ref: element } );
+
+		mockRegisteredStore.callbacks.initColors();
+
+		expect( mockGetClosestColor ).not.toHaveBeenCalled();
+		expect(
+			element.style.getPropertyValue( '--wc-product-filter-chips-text' )
+		).toBe( 'var(--wp--preset--color--contrast)' );
+		expect(
+			element.style.getPropertyValue(
+				'--wc-product-filter-chips-background'
+			)
+		).toBe( 'var(--wp--preset--color--base)' );
+	} );
+
+	it( 'does not override theme contrast CSS variables from stylesheets', () => {
+		if ( ! mockRegisteredStore ) {
+			throw new Error( 'Chips store was not registered.' );
+		}
+
+		const style = document.createElement( 'style' );
+		style.textContent = `.has-theme-vars {
+			--wc-product-filter-chips-background: rgb(1, 2, 3);
+			--wc-product-filter-chips-text: rgb(4, 5, 6);
+		}`;
+		document.head.appendChild( style );
+
+		const element = document.createElement( 'div' );
+		element.className = 'has-theme-vars';
+		document.body.appendChild( element );
+
+		mockGetElement.mockReturnValue( { ref: element } );
+
+		mockRegisteredStore.callbacks.initColors();
+
+		expect( mockGetClosestColor ).not.toHaveBeenCalled();
+		expect(
+			element.style.getPropertyValue(
+				'--wc-product-filter-chips-background'
+			)
+		).toBe( '' );
+		expect(
+			element.style.getPropertyValue( '--wc-product-filter-chips-text' )
+		).toBe( '' );
+
+		element.remove();
+		style.remove();
+	} );
 } );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json
index e1d50155a31..5f8d3aeda57 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json
@@ -10,12 +10,13 @@
 	"ancestor": [
 		"woocommerce/product-filter-attribute",
 		"woocommerce/product-filter-taxonomy",
-		"woocommerce/product-filter-status"
+		"woocommerce/product-filter-status",
+		"woocommerce/add-to-cart-with-options-variation-selector-attribute"
 	],
 	"supports": {
 		"interactivity": true
 	},
-	"usesContext": [ "woocommerceSelectableItems" ],
+	"usesContext": [ "woocommerce/selectableItems", "woocommerce/attributeId" ],
 	"attributes": {
 		"chipText": {
 			"type": "string"
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx
index f71877743f3..b09cf5dce7b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx
@@ -53,7 +53,7 @@ const Edit = ( props: EditProps ): JSX.Element => {
 		customSelectedChipBorder,
 	} = attributes;
 	const { isLoading = false, items = [] } =
-		context?.woocommerceSelectableItems ?? {};
+		context?.[ 'woocommerce/selectableItems' ] ?? {};

 	const hasColorSwatches = items.some( ( item ) => 'color' in item );

diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts
index 924bb9f0967..f3522a7b051 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { store, getContext } from '@wordpress/interactivity';
+import { store, getContext, getElement } from '@wordpress/interactivity';

 /**
  * Internal dependencies
@@ -10,6 +10,7 @@ import type {
 	SelectableItem,
 	SelectableItemsParentStore,
 } from '../../../../types/type-defs/selectable-items';
+import { getClosestColor } from '../../utils/get-closest-color';

 type ChipsItem = SelectableItem< {
 	color?: string;
@@ -17,6 +18,8 @@ type ChipsItem = SelectableItem< {
 } >;

 const DEFAULT_DISPLAY_LIMIT = 15;
+const CHIP_BACKGROUND_VAR = '--wc-product-filter-chips-background';
+const CHIP_TEXT_VAR = '--wc-product-filter-chips-text';

 type ChipsContext = {
 	storeNamespace: string;
@@ -34,6 +37,9 @@ type ChipsStore = {
 		toggle: () => void;
 		showAll: () => void;
 	};
+	callbacks: {
+		initColors: () => void;
+	};
 };

 function getParentStore( storeNamespace?: string ) {
@@ -56,6 +62,31 @@ function getCurrentItem(): ChipsItem | undefined {
 	return context.item;
 }

+function getCssVariable( element: HTMLElement, property: string ): string {
+	return (
+		element.style.getPropertyValue( property ) ||
+		window.getComputedStyle( element ).getPropertyValue( property )
+	).trim();
+}
+
+function initChipColors( element: HTMLElement ): void {
+	const style = element.style;
+
+	if ( ! getCssVariable( element, CHIP_BACKGROUND_VAR ) ) {
+		const backgroundColor = getClosestColor( element, 'backgroundColor' );
+		if ( backgroundColor ) {
+			style.setProperty( CHIP_BACKGROUND_VAR, backgroundColor );
+		}
+	}
+
+	if ( ! getCssVariable( element, CHIP_TEXT_VAR ) ) {
+		const textColor = getClosestColor( element, 'color' );
+		if ( textColor ) {
+			style.setProperty( CHIP_TEXT_VAR, textColor );
+		}
+	}
+}
+
 const { state }: ChipsStore = store< ChipsStore >(
 	'woocommerce/product-filter-chips',
 	{
@@ -100,6 +131,16 @@ const { state }: ChipsStore = store< ChipsStore >(
 				context.isExpanded = true;
 			},
 		},
+		callbacks: {
+			initColors: () => {
+				const el = getElement();
+				if ( ! el.ref ) {
+					return;
+				}
+
+				initChipColors( el.ref );
+			},
+		},
 	},
 	{ lock: true }
 );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
index e08e1623879..dec85799c21 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
@@ -1,4 +1,4 @@
-.wc-block-product-filter-chips fieldset {
+:where(.wc-block-product-filter-chips__fieldset) {
 	display: contents;
 }

@@ -36,9 +36,18 @@
 }

 .wc-block-product-filter-chips__item[aria-checked="true"] {
-	background: var(--wc-product-filters-text-color, CanvasText);
-	color: var(--wc-product-filters-background-color, Canvas);
-	border-color: var(--wc-product-filters-text-color, CanvasText);
+	background: var(
+		--wc-product-filter-chips-selected-background,
+		var(--wc-product-filter-chips-text, CanvasText)
+	);
+	color: var(
+		--wc-product-filter-chips-selected-text,
+		var(--wc-product-filter-chips-background, Canvas)
+	);
+	border-color: var(
+		--wc-product-filter-chips-selected-border,
+		var(--wc-product-filter-chips-text, CanvasText)
+	);

 	.has-selected-chip-text-color & {
 		color: var(--wc-product-filter-chips-selected-text);
@@ -128,7 +137,10 @@
 		}

 		&[aria-checked="true"] {
-			border-color: var(--wc-product-filters-text-color, CanvasText);
+			border-color: var(
+				--wc-product-filter-chips-selected-border,
+				var(--wc-product-filter-chips-text, CanvasText)
+			);
 		}
 	}

diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/block.json
index ac8666d5a4a..73bbb50fd1d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/block.json
@@ -8,7 +8,7 @@
 	"textdomain": "woocommerce",
 	"apiVersion": 3,
 	"ancestor": [ "woocommerce/product-filter-active" ],
-	"usesContext": [ "woocommerceRemovableItems" ],
+	"usesContext": [ "woocommerce/removableItems" ],
 	"supports": {
 		"interactivity": true,
 		"inserter": true
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/edit.tsx
index 517e6115238..2079d081bc1 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/edit.tsx
@@ -30,7 +30,7 @@ const Edit = () => {
 			<InitialDisabled>
 				<BlockContextProvider
 					value={ {
-						woocommerceRangeInput: {
+						'woocommerce/rangeInput': {
 							...getPriceFilterData( data ),
 							isLoading,
 						},
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/block.json
index 05078a1f068..0a7fce0ef1b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/block.json
@@ -49,7 +49,7 @@
 		}
 	},
 	"ancestor": [ "woocommerce/product-filter-price" ],
-	"usesContext": [ "woocommerceRangeInput" ],
+	"usesContext": [ "woocommerce/rangeInput" ],
 	"textdomain": "woocommerce",
 	"apiVersion": 3,
 	"$schema": "https://schemas.wp.org/trunk/block.json",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/edit.tsx
index aff4ae198b7..fd03932214d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-slider/edit.tsx
@@ -56,7 +56,7 @@ const PriceSliderEdit = ( {
 		customSlider,
 	} = attributes;

-	const rangeInput = context.woocommerceRangeInput;
+	const rangeInput = context[ 'woocommerce/rangeInput' ];
 	const { isLoading } = rangeInput ?? {};

 	const blockProps = useBlockProps( {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/edit.tsx
index a981f4cc72a..7c8988dc62e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/edit.tsx
@@ -187,7 +187,7 @@ const RatingFilterEdit = ( props: BlockEditProps< Attributes > ) => {
 					>
 						<BlockContextProvider
 							value={ {
-								woocommerceSelectableItems: {
+								'woocommerce/selectableItems': {
 									items: displayedOptions,
 									selectionMode: 'multiple' as const,
 									storeNamespace:
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/block.json
index 686ff058131..44f8b14b2b4 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/block.json
@@ -19,7 +19,7 @@
 		},
 		"interactivity": true
 	},
-	"usesContext": [ "queryId", "woocommerceRemovableItems" ],
+	"usesContext": [ "queryId", "woocommerce/removableItems" ],
 	"attributes": {
 		"chipText": {
 			"type": "string"
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/edit.tsx
index 15fcd60c1e2..060cbb1c9fe 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/removable-chips/edit.tsx
@@ -44,7 +44,7 @@ const Edit = ( props: EditProps ): JSX.Element => {
 	} = props;
 	const { customChipText, customChipBackground, customChipBorder, layout } =
 		attributes;
-	const removableItemsContext = context.woocommerceRemovableItems;
+	const removableItemsContext = context[ 'woocommerce/removableItems' ];
 	const { items } = removableItemsContext ?? {};

 	// Extract attributes from block layout
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/edit.tsx
index 115438a43b3..92c8d6ce2db 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/edit.tsx
@@ -90,7 +90,7 @@ const Edit = ( props: EditProps ) => {
 			<InitialDisabled>
 				<BlockContextProvider
 					value={ {
-						woocommerceSelectableItems: {
+						'woocommerce/selectableItems': {
 							items,
 							selectionMode: 'multiple' as const,
 							storeNamespace: 'woocommerce/product-filters',
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/edit.tsx
index 8ffe6927d2c..8e65383d3c4 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/edit.tsx
@@ -310,7 +310,7 @@ const Edit = ( props: EditProps ) => {
 			<InitialDisabled>
 				<BlockContextProvider
 					value={ {
-						woocommerceSelectableItems: {
+						'woocommerce/selectableItems': {
 							items:
 								termOptions.length === 0 && isPreview
 									? termOptionsPreview
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/range-input.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/range-input.ts
index 6880691a1be..923f7e3b40c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/range-input.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/range-input.ts
@@ -14,7 +14,8 @@ export interface RangeInputContext {
 }

 export type RangeInputBlockContext = {
-	woocommerceRangeInput: RangeInputContext;
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	'woocommerce/rangeInput': RangeInputContext;
 };

 /**
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/removable-items.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/removable-items.ts
index 680b093c1d7..4779bb1259a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/removable-items.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/removable-items.ts
@@ -11,7 +11,8 @@ export interface RemovableItemsContext {
 }

 export type RemovableItemsBlockContext = {
-	woocommerceRemovableItems: RemovableItemsContext;
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	'woocommerce/removableItems': RemovableItemsContext;
 };

 /**
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/selectable-items.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/selectable-items.ts
index c98758a1612..c41c5bd6fcd 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/selectable-items.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/selectable-items.ts
@@ -30,7 +30,8 @@ export interface SelectableItemsContext< T = unknown > {
 }

 export type SelectableItemsBlockContext< T = unknown > = {
-	woocommerceSelectableItems: SelectableItemsContext< T >;
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	'woocommerce/selectableItems': SelectableItemsContext< T >;
 };

 /**
diff --git a/plugins/woocommerce/client/blocks/bin/webpack-entries.js b/plugins/woocommerce/client/blocks/bin/webpack-entries.js
index 23a0069f0a6..e9941ab10a4 100644
--- a/plugins/woocommerce/client/blocks/bin/webpack-entries.js
+++ b/plugins/woocommerce/client/blocks/bin/webpack-entries.js
@@ -29,10 +29,6 @@ const blocks = {
 	'add-to-cart-with-options-variation-selector-attribute-name': {
 		customDir: 'add-to-cart-with-options/variation-selector/attribute-name',
 	},
-	'add-to-cart-with-options-variation-selector-attribute-options': {
-		customDir:
-			'add-to-cart-with-options/variation-selector/attribute-options',
-	},
 	'add-to-cart-with-options-grouped-product-selector': {
 		customDir: 'add-to-cart-with-options/grouped-product-selector',
 	},
@@ -62,6 +58,7 @@ const blocks = {
 	'coming-soon': {},
 	'coupon-code': {},
 	'customer-account': {},
+	dropdown: {},
 	'email-content': {},
 	'featured-category': {
 		customDir: 'featured-items/featured-category',
diff --git a/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/inner-block-protocols.md b/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/inner-block-protocols.md
index 0c9deb05f35..1e4c99051dc 100644
--- a/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/inner-block-protocols.md
+++ b/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/inner-block-protocols.md
@@ -6,9 +6,9 @@ WooCommerce reusable inner blocks use small context protocols so they can render

 | Context key | Purpose | Inner blocks |
 | --- | --- | --- |
-| `woocommerceSelectableItems` | Select/deselect items | checkbox-list, chips |
-| `woocommerceRemovableItems` | Remove active items | removable-chips |
-| `woocommerceRangeInput` | Numeric range input | price-slider |
+| `woocommerce/selectableItems` | Select/deselect items (filters, variation attributes) | checkbox-list, chips, dropdown |
+| `woocommerce/removableItems` | Remove individual items (active filters) | removable-chips |
+| `woocommerce/rangeInput` | Numeric range input (price, slider) | price-slider |

 ## Shared pattern

@@ -30,7 +30,7 @@ Parents pass the context directly when rendering inner blocks because items are

 ```php
 ( new \WP_Block( $parsed_block, array(
-    'woocommerceSelectableItems' => $context,
+    'woocommerce/selectableItems' => $context,
 ) ) )->render();
 ```

@@ -117,7 +117,7 @@ Checkbox-list and chips mirror parent items into child `state.items`, adding loc

 ## Removable Items

-Context key: `woocommerceRemovableItems`
+Context key: `woocommerce/removableItems`

 Used by active-filter chips and similar removable item lists.

@@ -160,7 +160,7 @@ Reference implementations: `ProductFilterRemovableChips.php`, `ProductFilterClea

 ## Range Input

-Context key: `woocommerceRangeInput`
+Context key: `woocommerce/rangeInput`

 Used by two-ended numeric controls such as price sliders.

diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
index cad256f025c..57f2c992338 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
@@ -238,12 +238,21 @@ test.describe( 'Add to Cart + Options Block', () => {
 			await expect( quantityInput ).toHaveValue( '1' );
 		} );

-		// The radio input is visually hidden and, thus, not clickable. That's
-		// why we need to select the <label> instead.
-		const logoNoOption = page.locator( 'label:has-text("No")' );
-		const colorBlueOption = page.locator( 'label:has-text("Blue")' );
-		const colorGreenOption = page.locator( 'label:has-text("Green")' );
-		const colorRedOption = page.locator( 'label:has-text("Red")' );
+		const addToCartBlock = page.locator(
+			'.wp-block-add-to-cart-with-options'
+		);
+		const logoNoOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Logo' } )
+			.getByRole( 'radio', { name: 'No', exact: true } );
+		const colorBlueOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Blue', exact: true } );
+		const colorGreenOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Green', exact: true } );
+		const colorRedOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Red', exact: true } );
 		// We use the Add to Cart + Options class to make sure we don't select
 		// the Add to Cart button from the Related Products block.
 		const addToCartButton = page
@@ -372,11 +381,18 @@ test.describe( 'Add to Cart + Options Block', () => {

 		await page.goto( '/product/v-neck-t-shirt/' );

-		// The radio input is visually hidden and, thus, not clickable. That's
-		// why we need to select the <label> instead.
-		const colorBlueOption = page.locator( 'label:has-text("Blue")' );
-		const colorRedOption = page.locator( 'label:has-text("Red")' );
-		const sizeLargeOption = page.locator( 'label:has-text("Large")' );
+		const addToCartBlock = page.locator(
+			'.wp-block-add-to-cart-with-options'
+		);
+		const colorBlueOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Blue', exact: true } );
+		const colorRedOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Red', exact: true } );
+		const sizeLargeOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Size' } )
+			.getByRole( 'radio', { name: 'Large', exact: true } );

 		await colorBlueOption.click();
 		await sizeLargeOption.click();
@@ -453,12 +469,19 @@ test.describe( 'Add to Cart + Options Block', () => {
 			isOnlyCurrentEntityDirty: true,
 		} );

-		await test.step( 'when in pills mode', async () => {
+		await test.step( 'when in chips mode', async () => {
 			await page.goto( '/product/custom-slug-variable/' );

-			// Verify the pills show term names (not slugs).
-			const petitOption = page.locator( 'label:has-text("Petit")' );
-			const grandOption = page.locator( 'label:has-text("Grand")' );
+			// Verify the chips show term names (not slugs).
+			const addToCartBlock = page.locator(
+				'.wp-block-add-to-cart-with-options'
+			);
+			const petitOption = addToCartBlock
+				.getByRole( 'radiogroup', { name: 'Taille' } )
+				.getByRole( 'radio', { name: 'Petit', exact: true } );
+			const grandOption = addToCartBlock
+				.getByRole( 'radiogroup', { name: 'Taille' } )
+				.getByRole( 'radio', { name: 'Grand', exact: true } );
 			const addToCartButton = page.getByRole( 'button', {
 				name: 'Add to cart',
 				exact: true,
@@ -790,7 +813,7 @@ test.describe( 'Add to Cart + Options Block', () => {
 		} );
 	} );

-	test( "doesn't allow selecting invalid variations in pills mode", async ( {
+	test( "doesn't allow selecting invalid variations in chips mode", async ( {
 		page,
 		pageObject,
 		editor,
@@ -803,10 +826,15 @@ test.describe( 'Add to Cart + Options Block', () => {

 		await page.goto( '/product/hoodie/' );

-		// The radio input is visually hidden and, thus, not clickable. That's
-		// why we need to select the <label> instead.
-		const logoYesOption = page.locator( 'label:has-text("Yes")' );
-		const colorGreenOption = page.locator( 'label:has-text("Green")' );
+		const addToCartBlock = page.locator(
+			'.wp-block-add-to-cart-with-options'
+		);
+		const logoYesOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Logo' } )
+			.getByRole( 'radio', { name: 'Yes', exact: true } );
+		const colorGreenOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Green', exact: true } );

 		await expect( colorGreenOption ).toBeEnabled();

@@ -957,15 +985,24 @@ test.describe( 'Add to Cart + Options Block', () => {

 			await expect( quantityInput ).toHaveValue( '1' );

-			const colorBlueOption = page.locator( 'label:has-text("Blue")' );
-			const logoNoOption = page.locator( 'label:has-text("No")' );
+			const addToCartBlock = page.locator(
+				'.wp-block-add-to-cart-with-options'
+			);
+			const colorBlueOption = addToCartBlock
+				.getByRole( 'radiogroup', { name: 'Color' } )
+				.getByRole( 'radio', { name: 'Blue', exact: true } );
+			const logoNoOption = addToCartBlock
+				.getByRole( 'radiogroup', { name: 'Logo' } )
+				.getByRole( 'radio', { name: 'No', exact: true } );

 			await colorBlueOption.click();
 			await logoNoOption.click();

 			await expect( quantityInput ).toHaveValue( '4' );

-			const logoYesOption = page.locator( 'label:has-text("Yes")' );
+			const logoYesOption = addToCartBlock
+				.getByRole( 'radiogroup', { name: 'Logo' } )
+				.getByRole( 'radio', { name: 'Yes', exact: true } );
 			await logoYesOption.click();

 			await expect( quantityInput ).toHaveValue( '4' );
@@ -1025,9 +1062,9 @@ test.describe( 'Add to Cart + Options Block', () => {
 			await test.step( 'hides Product Quantity input when the product is sold individually', async () => {
 				await expect( quantityInput ).toBeVisible();

-				const colorGreenOption = page.locator(
-					'label:has-text("Green")'
-				);
+				const colorGreenOption = addToCartBlock
+					.getByRole( 'radiogroup', { name: 'Color' } )
+					.getByRole( 'radio', { name: 'Green', exact: true } );
 				await colorGreenOption.click();

 				await expect( quantityInput ).toBeHidden();
@@ -1191,8 +1228,15 @@ test.describe( 'Add to Cart + Options Block', () => {

 		await page.goto( '/product/hoodie' );

-		const colorBlueOption = page.locator( 'label:has-text("Blue")' );
-		const logoYesOption = page.locator( 'label:has-text("Yes")' );
+		const addToCartBlock = page.locator(
+			'.wp-block-add-to-cart-with-options'
+		);
+		const colorBlueOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Blue', exact: true } );
+		const logoYesOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Logo' } )
+			.getByRole( 'radio', { name: 'Yes', exact: true } );

 		await colorBlueOption.click();
 		await logoYesOption.click();
@@ -1260,8 +1304,15 @@ test.describe( 'Add to Cart + Options Block', () => {
 	} ) => {
 		await pageObject.createPostWithProductBlock( 'hoodie' );

-		const colorBlueOption = page.locator( 'label:has-text("Blue")' );
-		const logoYesOption = page.locator( 'label:has-text("Yes")' );
+		const addToCartBlock = page.locator(
+			'.wp-block-add-to-cart-with-options'
+		);
+		const colorBlueOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Color' } )
+			.getByRole( 'radio', { name: 'Blue', exact: true } );
+		const logoYesOption = addToCartBlock
+			.getByRole( 'radiogroup', { name: 'Logo' } )
+			.getByRole( 'radio', { name: 'Yes', exact: true } );

 		await colorBlueOption.click();
 		await logoYesOption.click();
@@ -1479,8 +1530,8 @@ test.describe( 'Add to Cart + Options Block', () => {
 			}
 		} );

-		for ( const optionStyle of [ 'pills', 'dropdown' ] as (
-			| 'pills'
+		for ( const optionStyle of [ 'chips', 'dropdown' ] as (
+			| 'chips'
 			| 'dropdown'
 		 )[] ) {
 			// eslint-disable-next-line playwright/expect-expect
@@ -1491,7 +1542,7 @@ test.describe( 'Add to Cart + Options Block', () => {
 			} ) => {
 				await pageObject.updateSingleProductTemplate();

-				if ( optionStyle === 'pills' ) {
+				if ( optionStyle === 'chips' ) {
 					await editor.saveSiteEditorEntities( {
 						isOnlyCurrentEntityDirty: true,
 					} );
@@ -1566,6 +1617,7 @@ test.describe( 'Add to Cart + Options Block', () => {
 					);
 				} );
 			} );
+
 			test( `${ optionStyle }: Test the disabledAttributesAction block attribute`, async ( {
 				page,
 				pageObject,
@@ -1574,7 +1626,7 @@ test.describe( 'Add to Cart + Options Block', () => {
 				await test.step( `${ optionStyle }: Set the disabledAttributesAction block attribute to "disable"`, async () => {
 					await pageObject.updateSingleProductTemplate();

-					if ( optionStyle === 'pills' ) {
+					if ( optionStyle === 'chips' ) {
 						await editor.saveSiteEditorEntities( {
 							isOnlyCurrentEntityDirty: true,
 						} );
@@ -1595,16 +1647,30 @@ test.describe( 'Add to Cart + Options Block', () => {
 						optionStyle
 					);

-					await expect(
-						page
-							.getByLabel( 'Size' )
-							.getByText( 'L', { exact: true } )
-					).toBeDisabled();
-					await expect(
-						page
-							.getByLabel( 'Size' )
-							.getByText( 'L', { exact: true } )
-					).not.toHaveAttribute( 'hidden' );
+					if ( optionStyle === 'chips' ) {
+						const sizeLChip = page
+							.getByRole( 'radiogroup', { name: 'Size' } )
+							.getByRole( 'radio', {
+								name: 'L',
+								exact: true,
+							} );
+						await expect( sizeLChip ).toBeDisabled();
+						await expect( sizeLChip ).not.toHaveAttribute(
+							'hidden'
+						);
+					} else {
+						const sizeSelect = page
+							.locator( '.wp-block-add-to-cart-with-options' )
+							.getByLabel( 'Size', { exact: true } );
+						const sizeLOption = sizeSelect.getByRole( 'option', {
+							name: 'L',
+							exact: true,
+						} );
+						await expect( sizeLOption ).toBeDisabled();
+						await expect( sizeLOption ).not.toHaveAttribute(
+							'hidden'
+						);
+					}
 				} );

 				await test.step( `${ optionStyle }: Set the disabledAttributesAction block attribute to "hide"`, async () => {
@@ -1625,18 +1691,27 @@ test.describe( 'Add to Cart + Options Block', () => {
 						optionStyle
 					);

-					await expect(
-						page
-							.getByLabel( 'Size' )
-							.getByText( 'L', { exact: true } )
-					).toBeDisabled();
-					await expect(
-						page
-							.getByLabel( 'Size' )
-							.getByText( 'L', { exact: true } )
-					).toBeHidden();
+					if ( optionStyle === 'chips' ) {
+						const sizeLChip = page
+							.getByRole( 'radiogroup', { name: 'Size' } )
+							.getByRole( 'radio', {
+								name: 'L',
+								exact: true,
+							} );
+						await expect( sizeLChip ).toBeHidden();
+					} else {
+						const sizeSelect = page
+							.locator( '.wp-block-add-to-cart-with-options' )
+							.getByLabel( 'Size', { exact: true } );
+						const sizeLOption = sizeSelect.getByRole( 'option', {
+							name: 'L',
+							exact: true,
+						} );
+						await expect( sizeLOption ).toBeHidden();
+					}
 				} );
 			} );
+
 			// eslint-disable-next-line playwright/expect-expect
 			test( `${ optionStyle }: Combining autoselect and disabledAttributesAction block attributes should work`, async ( {
 				page,
@@ -1698,14 +1773,14 @@ test.describe( 'Add to Cart + Options Block', () => {
 			} );
 		}

-		test( `Pills: "X in cart" text displays correctly after auto-selection`, async ( {
+		test( `chips: "X in cart" text displays correctly after auto-selection`, async ( {
 			page,
 			pageObject,
 			editor,
 		} ) => {
 			await pageObject.updateSingleProductTemplate();
 			await pageObject.setVariationSelectorAttributes( {
-				optionStyle: 'pills',
+				optionStyle: 'chips',
 				autoselect: true,
 			} );
 			await editor.saveSiteEditorEntities();
@@ -1717,14 +1792,14 @@ test.describe( 'Add to Cart + Options Block', () => {
 				await pageObject.selectVariationSelectorOptions(
 					'Color',
 					'Blue',
-					'pills'
+					'chips'
 				);

 				// Type and Size should auto-select to T-shirt and XL
 				await pageObject.expectVariationSelectorOptions(
 					productAttributes,
 					{ Type: 'T-shirt', Color: 'Blue', Size: 'XL' },
-					'pills'
+					'chips'
 				);

 				// Add to cart
@@ -1751,7 +1826,7 @@ test.describe( 'Add to Cart + Options Block', () => {
 				await pageObject.selectVariationSelectorOptions(
 					'Color',
 					'Blue',
-					'pills'
+					'chips'
 				);

 				// After auto-selection completes, the button should show "1 in cart"
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts
index 175c43752ce..c20d80dc158 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts
@@ -114,7 +114,7 @@ class AddToCartWithOptionsPage {
 		autoselect,
 		disabledAttributesAction,
 	}: {
-		optionStyle?: 'pills' | 'dropdown';
+		optionStyle?: 'chips' | 'dropdown';
 		autoselect?: boolean;
 		disabledAttributesAction?: 'disable' | 'hide';
 	} = {} ) {
@@ -126,21 +126,21 @@ class AddToCartWithOptionsPage {
 		// Verify inner blocks have loaded.
 		await expect(
 			this.editor.canvas
-				.getByLabel(
-					'Block: Variation Selector: Attribute Options (Beta)'
-				)
+				.getByLabel( 'Block: Variation Selector: Template (Beta)' )
 				.first()
 		).toBeVisible();

-		const attributeOptionsBlock = await this.editor.getBlockByName(
-			'woocommerce/add-to-cart-with-options-variation-selector-attribute-options'
+		const attributeTemplateBlock = await this.editor.getBlockByName(
+			'woocommerce/add-to-cart-with-options-variation-selector-attribute'
 		);
-		await this.editor.selectBlocks( attributeOptionsBlock.first() );
+		await this.editor.selectBlocks( attributeTemplateBlock.first() );

-		// Option style attribute.
+		// Option style attribute (inner block titles: Chips, Dropdown).
 		if ( optionStyle ) {
+			const styleLabel =
+				optionStyle === 'dropdown' ? 'Dropdown' : 'Chips';
 			const optionStyleInput = page.getByRole( 'radio', {
-				name: optionStyle,
+				name: styleLabel,
 			} );
 			await optionStyleInput.click();
 		}
@@ -195,22 +195,33 @@ class AddToCartWithOptionsPage {
 	async selectVariationSelectorOptions(
 		attributeName: string,
 		attributeValue: string,
-		optionStyle: 'pills' | 'dropdown'
+		optionStyle: 'chips' | 'dropdown'
 	) {
 		if ( optionStyle === 'dropdown' ) {
-			await this.page
-				.getByLabel( attributeName )
-				.selectOption( attributeValue );
-		} else if ( attributeValue !== '' ) {
-			await this.page
-				.getByLabel( attributeName )
-				.getByText( attributeValue )
+			const select = this.page.getByLabel( attributeName, {
+				exact: true,
+			} );
+			if ( attributeValue !== '' ) {
+				await expect(
+					select.getByRole( 'option', {
+						name: attributeValue,
+						exact: true,
+					} )
+				).toBeAttached();
+			}
+			await select.selectOption( attributeValue );
+			return;
+		}
+		const group = this.page.getByRole( 'radiogroup', {
+			name: attributeName,
+		} );
+		if ( attributeValue !== '' ) {
+			await group
+				.getByRole( 'radio', { name: attributeValue, exact: true } )
 				.click();
 		} else {
-			await this.page
-				.getByLabel( attributeName )
-				.locator( 'label:has(:checked)' )
-				.click();
+			const selected = group.getByRole( 'radio', { checked: true } );
+			await selected.click();
 		}
 	}

@@ -221,18 +232,21 @@ class AddToCartWithOptionsPage {
 			variation: boolean;
 			visible: boolean;
 		}[],
-		expectedValues: Record< string, string | RegExp > = {},
-		optionStyle: 'pills' | 'dropdown'
+		expectedValues: Record< string, string > = {},
+		optionStyle: 'chips' | 'dropdown'
 	) {
 		for ( let {
 			name: attributeName,
 			options: attributeValues,
 		} of productAttributes ) {
-			const attributeNameLocator = this.page.getByLabel( attributeName, {
-				exact: true,
-			} );
 			if ( optionStyle === 'dropdown' ) {
-				let expectedValue: string | RegExp;
+				const attributeNameLocator = this.page.getByLabel(
+					attributeName,
+					{
+						exact: true,
+					}
+				);
+				let expectedValue: string;
 				if (
 					attributeName in expectedValues &&
 					expectedValues[ attributeName ] !== ''
@@ -246,27 +260,32 @@ class AddToCartWithOptionsPage {
 				);
 				continue;
 			}
+			const group = this.page.getByRole( 'radiogroup', {
+				name: attributeName,
+			} );
 			if (
 				attributeName in expectedValues &&
 				expectedValues[ attributeName ] !== ''
 			) {
 				attributeValues = attributeValues.filter(
 					( item ) => item !== expectedValues[ attributeName ]
-				); // Omit attributeName
+				);
 				await expect(
-					attributeNameLocator.getByLabel(
-						expectedValues[ attributeName ],
-						{ exact: true }
-					)
+					group.getByRole( 'radio', {
+						name: String( expectedValues[ attributeName ] ),
+						exact: true,
+					} )
 				).toBeChecked();
 			}
 			if ( attributeValues.length ) {
 				for ( const attributeValue of attributeValues ) {
-					await expect(
-						attributeNameLocator.getByLabel( attributeValue, {
-							exact: true,
-						} )
-					).not.toBeChecked();
+					const radio = group.getByRole( 'radio', {
+						name: attributeValue,
+						exact: true,
+					} );
+					if ( ( await radio.count() ) > 0 ) {
+						await expect( radio ).not.toBeChecked();
+					}
 				}
 			}
 		}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 0daca2f8e17..b005bd27fe0 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -53529,12 +53529,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/ProductFilterCheckboxList.php

-		-
-			message: '#^Access to property \$context on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 2
-			path: src/Blocks/BlockTypes/ProductFilterChips.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductFilterChips\:\:render\(\) should return string but returns string\|false\.$#'
 			identifier: return.type
@@ -53553,12 +53547,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/ProductFilterChips.php

-		-
-			message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductFilterChips\:\:render\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Blocks/BlockTypes/ProductFilterChips.php
-
 		-
 			message: '#^Access to property \$context on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
 			identifier: class.notFound
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationDescription.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationDescription.php
index 1709a50aac8..6b743797752 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationDescription.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationDescription.php
@@ -19,7 +19,6 @@ class VariationDescription extends AbstractBlock {
 	 */
 	protected $block_name = 'add-to-cart-with-options-variation-description';

-
 	/**
 	 * Render the block.
 	 *
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php
index c4af4f47c7d..819dce6a4a9 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php
@@ -5,12 +5,10 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;

 use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
 use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
-use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils as AddToCartWithOptionsUtils;
 use WP_Block;

 /**
- * Block type for variation selector item in add to cart with options.
- * It's responsible to render each child attribute in a form of a list item.
+ * Block type for Variation Selector in the Add to Cart + Options block.
  */
 class VariationSelectorAttribute extends AbstractBlock {

@@ -31,7 +29,7 @@ class VariationSelectorAttribute extends AbstractBlock {
 	 * @param WP_Block $block Block instance.
 	 * @return string Rendered block output.
 	 */
-	protected function render( $attributes, $content, $block ): string {
+	protected function render( $attributes, $content, $block ) {
 		global $product;

 		if ( ! $product instanceof \WC_Product_Variable ) {
@@ -40,138 +38,284 @@ class VariationSelectorAttribute extends AbstractBlock {

 		$content = '';

-		$product_attributes = $product->get_variation_attributes();
+		$product_attributes            = $product->get_variation_attributes();
+		$available_values_by_attribute = $this->get_available_variation_values_by_attribute_slug();

 		foreach ( $product_attributes as $product_attribute_name => $product_attribute_terms ) {
-			$content .= $this->get_product_row( $product_attribute_name, $product_attribute_terms, $block );
+			$content .= $this->render_attribute_row( $product_attribute_name, $product_attribute_terms, $block, $attributes, $available_values_by_attribute );
 		}

 		return $content;
 	}

 	/**
-	 * Get product row HTML.
+	 * Get attribute row HTML.
 	 *
 	 * @param string   $attribute_name Product Attribute Name.
 	 * @param array    $product_attribute_terms Product Attribute Terms.
 	 * @param WP_Block $block The Block.
+	 * @param array    $attributes Template block attributes (displayStyle, autoselect, etc.).
+	 * @param array    $available_values_by_attribute Variation values keyed by attribute slug.
 	 * @return string Row HTML
 	 */
-	private function get_product_row( $attribute_name, $product_attribute_terms, $block ): string {
-		global $product;
+	private function render_attribute_row( string $attribute_name, array $product_attribute_terms, WP_Block $block, array $attributes, array $available_values_by_attribute ): string {
+		$inner_blocks = $block->parsed_block['innerBlocks'] ?? array();

-		$attribute_terms    = $this->get_terms( $attribute_name, $product_attribute_terms );
-		$product_variations = $product->get_available_variations( 'objects' );
+		if ( empty( $inner_blocks ) ) {
+			return '';
+		}

-		// Filter out terms which are not available in any product variation.
-		$attribute_terms = array_filter(
-			$attribute_terms,
-			function ( $term ) use ( $product_variations, $attribute_name ) {
-				foreach ( $product_variations as $variation ) {
-					$attributes = $variation->get_variation_attributes();
-					if (
-						$term['value'] === $attributes[ wc_variation_attribute_name( $attribute_name ) ] ||
-						'' === $attributes[ wc_variation_attribute_name( $attribute_name ) ]
-					) {
-						return true;
-					}
-				}
-			}
+		$attribute_slug  = wc_variation_attribute_name( $attribute_name );
+		$attribute_terms = $this->get_filtered_attribute_terms(
+			$attribute_name,
+			$product_attribute_terms,
+			$available_values_by_attribute[ $attribute_slug ] ?? array()
 		);

 		if ( empty( $attribute_terms ) ) {
 			return '';
 		}

-		$block_content = AddToCartWithOptionsUtils::render_block_with_context(
-			$block,
-			array(
-				'woocommerce/attributeId'    => 'wc_product_attribute_' . uniqid(),
-				'woocommerce/attributeName'  => $attribute_name,
-				'woocommerce/attributeTerms' => $attribute_terms,
+		$default_selected = $this->get_default_selected_attribute( $attribute_slug, $attribute_terms );
+		$variation_items  = $this->build_variation_selectable_items( $attribute_slug, $attribute_terms, $default_selected );
+		$attribute_label  = wc_attribute_label( $attribute_name );
+		$attribute_id     = 'wc_product_attribute_' . uniqid();
+		$context          = array(
+			'woocommerce/attributeId'     => $attribute_id,
+			'woocommerce/attributeName'   => $attribute_name,
+			'woocommerce/attributeTerms'  => $attribute_terms,
+			'woocommerce/selectableItems' => array(
+				'items'          => $variation_items,
+				'selectionMode'  => 'single',
+				'storeNamespace' => 'woocommerce/add-to-cart-with-options',
+				'groupLabel'     => $attribute_label,
 			),
 		);

-		// Render the inner blocks of the Variation Selector Item Template block with `dynamic` set to `false`
-		// to prevent calling `render_callback` and ensure that no wrapper markup is included.
-		return $block_content;
+		$inner_html = '';
+
+		foreach ( $inner_blocks as $inner_block ) {
+			$inner_block = $this->replace_legacy_attribute_options_block( $inner_block, $attributes );
+			$inner_html .= ( new WP_Block( $inner_block, $context ) )->render();
+		}
+
+		$interactive_context = array(
+			'name'                      => $attribute_label,
+			'variationAttributeOptions' => $variation_items,
+			'selectedValue'             => $default_selected,
+			'autoselect'                => $attributes['autoselect'] ?? false,
+			'disabledAttributesAction'  => $attributes['disabledAttributesAction'] ?? 'disable',
+		);
+
+		$interactive_attributes = array(
+			'data-wp-interactive' => 'woocommerce/add-to-cart-with-options',
+			'data-wp-init'        => 'callbacks.setDefaultSelectedAttribute',
+		);
+
+		return sprintf(
+			'<div %s %s>%s</div>',
+			get_block_wrapper_attributes( $interactive_attributes ),
+			wp_interactivity_data_wp_context( $interactive_context ),
+			$inner_html
+		);
 	}

 	/**
-	 * Get product attributes terms.
+	 * Replace legacy Attribute Options block and apply its settings to the parent attributes.
 	 *
-	 * @param string $attribute_name Product Attribute Name.
-	 * @param array  $attribute_terms Product Attribute Terms.
-	 * @return array[] Array of term data with structure:
-	 *                 [
-	 *                     'label'      => (string) Display label for the term.
-	 *                     'value'      => (string) Internal value/slug for the term.
-	 *                     'isSelected' => (bool)   Whether this term is the default selection.
-	 *                 ]
+	 * @param array $inner_block  The inner block to replace.
+	 * @param array $attributes   Parent block attributes, updated when attributes in the legacy Attribute Options block are found.
+	 * @return array The replaced inner block.
 	 */
-	protected function get_terms( $attribute_name, $attribute_terms ) {
-		global $product;
+	private function replace_legacy_attribute_options_block( array $inner_block, array &$attributes ): array {
+		if ( 'woocommerce/add-to-cart-with-options-variation-selector-attribute-options' === $inner_block['blockName'] ) {
+			$legacy_attrs = $inner_block['attrs'] ?? array();

-		$is_taxonomy = taxonomy_exists( $attribute_name );
+			if ( array_key_exists( 'autoselect', $legacy_attrs ) && true === $legacy_attrs['autoselect'] ) {
+				$attributes['autoselect'] = true;
+			}

-		$selected_attribute = $product->get_variation_default_attribute( $attribute_name );
+			if ( array_key_exists( 'disabledAttributesAction', $legacy_attrs ) && 'hide' === $legacy_attrs['disabledAttributesAction'] ) {
+				$attributes['disabledAttributesAction'] = 'hide';
+			}
+
+			if ( array_key_exists( 'optionStyle', $legacy_attrs ) && 'dropdown' === $legacy_attrs['optionStyle'] ) {
+				$attributes['displayStyle'] = 'woocommerce/dropdown';
+			}

-		if ( $is_taxonomy ) {
-			$items = array_map(
-				function ( $term ) use ( $attribute_name, $product, $selected_attribute ) {
-					return array(
-						'value'      => $term->slug,
-						/**
-						 * Filter the variation option name.
-						 *
-						 * @since 9.7.0
-						 *
-						 * @param string     $option_label    The option label.
-						 * @param WP_Term|string|null $item   Term object for taxonomies, option string for custom attributes.
-						 * @param string     $attribute_name  Name of the attribute.
-						 * @param WC_Product $product         Product object.
-						 */
-						'label'      => apply_filters(
-							'woocommerce_variation_option_name',
-							$term->name,
-							$term,
-							$attribute_name,
-							$product
-						),
-						'isSelected' => $selected_attribute === $term->slug,
-					);
-				},
-				wc_get_product_terms( $product->get_id(), $attribute_name, array( 'fields' => 'all' ) ),
+			return array(
+				'blockName'    => 'woocommerce/dropdown' === $attributes['displayStyle'] ? 'woocommerce/dropdown' : 'woocommerce/product-filter-chips',
+				'attrs'        => array(),
+				'innerBlocks'  => array(),
+				'innerHTML'    => '',
+				'innerContent' => array(),
 			);
+		} elseif ( isset( $inner_block['innerBlocks'] ) && is_array( $inner_block['innerBlocks'] ) && ! empty( $inner_block['innerBlocks'] ) ) {
+			foreach ( $inner_block['innerBlocks'] as $key => $child_inner_block ) {
+				$inner_block['innerBlocks'][ $key ] = $this->replace_legacy_attribute_options_block( $child_inner_block, $attributes );
+			}
+		}
+
+		return $inner_block;
+	}
+
+	/**
+	 * Build filtered attribute term options for the variation selector.
+	 *
+	 * @param string $attribute_name Product attribute name.
+	 * @param array  $product_attribute_terms Custom attribute terms when not a taxonomy.
+	 * @param array  $available_values Available variation values for this attribute (value => true).
+	 * @return array Attribute terms, or empty string when none match.
+	 */
+	private function get_filtered_attribute_terms( string $attribute_name, array $product_attribute_terms, array $available_values ) {
+		global $product;
+
+		$selected_attribute = $product->get_variation_default_attribute( $attribute_name );
+		$terms              = taxonomy_exists( $attribute_name )
+			? wc_get_product_terms( $product->get_id(), $attribute_name, array( 'fields' => 'all' ) )
+			: $product_attribute_terms;
+
+		$attribute_terms = array();
+		if ( ! empty( $available_values ) ) {
+			$allows_any_value = isset( $available_values[''] );
+
+			foreach ( $terms as $term ) {
+				$option = $this->map_term_to_option( $term, $attribute_name, $product, $selected_attribute );
+
+				if ( ! isset( $option['value'] ) ) {
+					continue;
+				}
+
+				if ( $allows_any_value || isset( $available_values[ $option['value'] ] ) ) {
+					$attribute_terms[] = $option;
+				}
+			}
+		}
+
+		return $attribute_terms;
+	}
+
+	/**
+	 * Build a lookup of attribute values used by available variations.
+	 *
+	 * @return array Map of attribute slug to set of values (keys are values).
+	 */
+	private function get_available_variation_values_by_attribute_slug(): array {
+		global $product;
+
+		$product_variations = $product->get_available_variations( 'objects' );
+		$available_by_slug  = array();
+
+		foreach ( $product_variations as $variation ) {
+			foreach ( $variation->get_variation_attributes() as $attribute_slug => $value ) {
+				$available_by_slug[ $attribute_slug ][ $value ] = true;
+			}
+		}
+
+		return $available_by_slug;
+	}
+
+	/**
+	 * Get the default selected attribute.
+	 *
+	 * @param string $attribute_slug The attribute's slug.
+	 * @param array  $attribute_terms The attribute's terms.
+	 * @return string|null The default selected attribute.
+	 */
+	private function get_default_selected_attribute( string $attribute_slug, array $attribute_terms ): ?string {
+		if ( isset( $_GET[ $attribute_slug ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			$raw = wp_unslash( $_GET[ $attribute_slug ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			if ( is_string( $raw ) ) {
+				$attribute_slug_from_request = sanitize_title( $raw );
+				foreach ( $attribute_terms as $attribute_term ) {
+					if ( sanitize_title( $attribute_term['value'] ) === $attribute_slug_from_request ) {
+						return $attribute_term['value'];
+					}
+				}
+			}
 		} else {
-			$items = array_map(
-				function ( $term ) use ( $attribute_name, $product, $selected_attribute ) {
-					return array(
-						'value'      => $term,
-						/**
-						 * Filter the variation option name.
-						 *
-						 * @since 9.7.0
-						 *
-						 * @param string     $option_label    The option label.
-						 * @param WP_Term|string|null $item   Term object for taxonomies, option string for custom attributes.
-						 * @param string     $attribute_name  Name of the attribute.
-						 * @param WC_Product $product         Product object.
-						 */
-						'label'      => apply_filters(
-							'woocommerce_variation_option_name',
-							$term,
-							null,
-							$attribute_name,
-							$product
-						),
-						'isSelected' => $selected_attribute === $term,
-					);
-				},
-				$attribute_terms,
+			foreach ( $attribute_terms as $attribute_term ) {
+				if ( $attribute_term['isSelected'] ) {
+					return $attribute_term['value'];
+				}
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Build selectable items for the inner block protocol and client context.
+	 *
+	 * @param string      $attribute_slug Attribute slug.
+	 * @param array       $attribute_terms Terms from context.
+	 * @param string|null $default_selected Default selected attribute value.
+	 * @return array
+	 */
+	private function build_variation_selectable_items( string $attribute_slug, array $attribute_terms, ?string $default_selected ): array {
+		$id_prefix = sanitize_title( $attribute_slug );
+		$items     = array();
+
+		foreach ( $attribute_terms as $attribute_term ) {
+			if ( ! is_array( $attribute_term ) || ! isset( $attribute_term['value'], $attribute_term['label'] ) ) {
+				continue;
+			}
+			$value   = (string) $attribute_term['value'];
+			$slug    = sanitize_title( $value );
+			$items[] = array(
+				'id'        => $id_prefix . '-' . $slug,
+				'label'     => (string) $attribute_term['label'],
+				'value'     => $value,
+				'ariaLabel' => (string) $attribute_term['label'],
+				'selected'  => $default_selected === $value,
 			);
 		}

 		return $items;
 	}
+
+	/**
+	 * Map a taxonomy term or custom attribute option to the variation row option shape.
+	 *
+	 * @param \WP_Term|string $term Term object for taxonomies, option string for custom attributes.
+	 * @param string          $attribute_name Name of the attribute.
+	 * @param \WC_Product     $product Product object.
+	 * @param string          $selected_attribute Default selected attribute value.
+	 * @return array
+	 */
+	private function map_term_to_option( $term, string $attribute_name, \WC_Product $product, string $selected_attribute ): array {
+		if ( $term instanceof \WP_Term ) {
+			$value       = $term->slug;
+			$label       = $term->name;
+			$filter_item = $term;
+		} elseif ( is_string( $term ) ) {
+			$value       = $term;
+			$label       = $term;
+			$filter_item = null;
+		} else {
+			return array();
+		}
+
+		return array(
+			'value'      => $value,
+			/**
+			 * Filter the variation option name.
+			 *
+			 * @since 9.7.0
+			 *
+			 * @param string                $option_label   The option label.
+			 * @param \WP_Term|string|null $item            Term object for taxonomies, option string for custom attributes.
+			 * @param string                $attribute_name Name of the attribute.
+			 * @param \WC_Product           $product        Product object.
+			 */
+			'label'      => apply_filters(
+				'woocommerce_variation_option_name',
+				$label,
+				$filter_item,
+				$attribute_name,
+				$product
+			),
+			'isSelected' => $selected_attribute === $value,
+		);
+	}
 }
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeName.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeName.php
index 2f482d36b88..27056a9e420 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeName.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeName.php
@@ -48,7 +48,6 @@ class VariationSelectorAttributeName extends AbstractBlock {
 		$wrapper_attributes = get_block_wrapper_attributes(
 			array(
 				'class' => esc_attr( $classes_and_styles['classes'] ),
-				'for'   => esc_attr( $attribute_id ),
 				'id'    => esc_attr( $attribute_id . '_label' ),
 				'style' => esc_attr( $classes_and_styles['styles'] ),
 			)
@@ -57,7 +56,7 @@ class VariationSelectorAttributeName extends AbstractBlock {
 		$label_text = esc_html( wc_attribute_label( $attribute_name ) );

 		return sprintf(
-			'<label %s>%s</label>',
+			'<span %s>%s</span>',
 			$wrapper_attributes,
 			$label_text
 		);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
deleted file mode 100644
index ba1122f3976..00000000000
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
+++ /dev/null
@@ -1,278 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
-
-use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
-use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
-use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
-
-/**
- * Block type for variation selector attribute options in add to cart with options.
- * It's responsible to render the attribute options.
- */
-class VariationSelectorAttributeOptions extends AbstractBlock {
-
-	use EnableBlockJsonAssetsTrait;
-
-	/**
-	 * Block name.
-	 *
-	 * @var string
-	 */
-	protected $block_name = 'add-to-cart-with-options-variation-selector-attribute-options';
-
-	/**
-	 * Render the block.
-	 *
-	 * @param array     $attributes Block attributes.
-	 * @param string    $content Block content.
-	 * @param \WP_Block $block Block instance.
-	 * @return string Rendered block output.
-	 */
-	protected function render( $attributes, $content, $block ): string {
-		if (
-			! isset(
-				$block->context['woocommerce/attributeName'],
-				$block->context['woocommerce/attributeId'],
-				$block->context['woocommerce/attributeTerms']
-			)
-		) {
-			return '';
-		}
-
-		$attribute_slug = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
-
-		$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
-
-		$option_style = array_key_exists( 'optionStyle', $attributes ) ? $attributes['optionStyle'] : null;
-
-		// During the beta period, `optionStyle` was called `style`, so we check
-		// `style` for backwards compatibility.
-		if ( ! $option_style && array_key_exists( 'style', $attributes ) && 'dropdown' === $attributes['style'] ) {
-			$option_style = 'dropdown';
-		}
-
-		$wrapper_attributes = get_block_wrapper_attributes(
-			array(
-				'class' => $classes_and_styles['classes'],
-				'style' => $classes_and_styles['styles'],
-			)
-		);
-
-		if ( 'dropdown' === $option_style ) {
-			$content = $this->render_dropdown( $attributes, $content, $block );
-		} else {
-			$content = $this->render_pills( $attributes, $content, $block );
-		}
-
-		return sprintf(
-			'<div %s>%s</div>',
-			$wrapper_attributes,
-			$content
-		);
-	}
-
-	/**
-	 * Get the normalized version of the attributes.
-	 *
-	 * @param array $attributes         The element's attributes.
-	 * @param array $default_attributes The element's default attributes.
-	 * @return string The HTML element's attributes.
-	 */
-	public static function get_normalized_attributes( $attributes, $default_attributes = array() ) {
-		$normalized_attributes = array();
-
-		$merged_attributes = array_merge( $default_attributes, $attributes );
-
-		foreach ( $merged_attributes as $key => $value ) {
-			if ( is_null( $value ) ) {
-				continue;
-			}
-			if ( is_array( $value ) || is_object( $value ) ) {
-				$value = wp_json_encode(
-					$value,
-					JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
-				);
-			}
-			$normalized_attributes[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) );
-		}
-
-		return implode( ' ', $normalized_attributes );
-	}
-
-	/**
-	 * Get the default selected attribute.
-	 *
-	 * @param string $attribute_slug The attribute's slug.
-	 * @param array  $attribute_terms The attribute's terms.
-	 * @return string|null The default selected attribute.
-	 */
-	protected function get_default_selected_attribute( $attribute_slug, $attribute_terms ) {
-		if ( isset( $_GET[ $attribute_slug ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
-			$raw = wp_unslash( $_GET[ $attribute_slug ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-			if ( is_string( $raw ) ) {
-				$attribute_slug_from_request = sanitize_title( $raw );
-				foreach ( $attribute_terms as $attribute_term ) {
-					if ( sanitize_title( $attribute_term['value'] ) === $attribute_slug_from_request ) {
-						return $attribute_term['value'];
-					}
-				}
-			}
-		} else {
-			foreach ( $attribute_terms as $attribute_term ) {
-				if ( $attribute_term['isSelected'] ) {
-					return $attribute_term['value'];
-				}
-			}
-		}
-
-		return null;
-	}
-
-	/**
-	 * Render the attribute options as pills.
-	 *
-	 * @param array     $attributes Block attributes.
-	 * @param string    $content Block content.
-	 * @param \WP_Block $block Block instance.
-	 * @return string The pills.
-	 */
-	protected function render_pills( $attributes, $content, $block ) {
-		$attribute_id               = $block->context['woocommerce/attributeId'];
-		$attribute_slug             = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
-		$attribute_terms            = $block->context['woocommerce/attributeTerms'];
-		$autoselect                 = $attributes['autoselect'] ?? false;
-		$disabled_attributes_action = $attributes['disabledAttributesAction'] ?? 'disable';
-
-		wp_interactivity_state(
-			'woocommerce/add-to-cart-with-options',
-			array(
-				'isOptionSelected' =>
-				function () {
-					$context = wp_interactivity_get_context();
-
-					return $context['option']['value'] === $context['selectedValue'];
-				},
-			)
-		);
-
-		$pills = '';
-		foreach ( $attribute_terms as $attribute_term ) {
-			$input = sprintf(
-				'<input type="radio" %s/>',
-				$this->get_normalized_attributes(
-					array(
-						'class'                  => 'wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input',
-						'name'                   => $attribute_slug,
-						'value'                  => $attribute_term['value'],
-						'data-wp-bind--checked'  => 'state.isOptionSelected',
-						'data-wp-bind--disabled' => 'state.isOptionDisabled',
-						'data-wp-bind--hidden'   => 'hide' === $disabled_attributes_action ? 'state.isOptionDisabled' : null,
-						'data-wp-on--click'      => 'actions.handlePillClick',
-						'data-wp-on--keydown'    => 'actions.handleKeyDown',
-						'data-wp-context'        => array(
-							'option' => $attribute_term,
-						),
-					),
-				)
-			);
-
-			$pills .= '<label class="wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill">' . $input . esc_html( $attribute_term['label'] ) . '</label>';
-		}
-
-		return sprintf(
-			'<div %s>%s</div>',
-			$this->get_normalized_attributes(
-				array(
-					'class'           => 'wc-block-add-to-cart-with-options-variation-selector-attribute-options__pills',
-					'role'            => 'radiogroup',
-					'id'              => $attribute_id,
-					'aria-labelledby' => $attribute_id . '_label',
-					'data-wp-context' => array(
-						'name'          => wc_attribute_label( $block->context['woocommerce/attributeName'] ),
-						'options'       => $attribute_terms,
-						'selectedValue' => $this->get_default_selected_attribute( $attribute_slug, $attribute_terms ),
-						'focused'       => '',
-						'autoselect'    => $autoselect,
-					),
-					'data-wp-init'    => 'callbacks.setDefaultSelectedAttribute',
-				),
-			),
-			$pills,
-		);
-	}
-
-	/**
-	 * Render the attribute options as a dropdown.
-	 *
-	 * @param array     $attributes Block attributes.
-	 * @param string    $content Block content.
-	 * @param \WP_Block $block Block instance.
-	 * @return string The dropdown.
-	 */
-	protected function render_dropdown( $attributes, $content, $block ) {
-		$attribute_id    = $block->context['woocommerce/attributeId'];
-		$attribute_slug  = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
-		$attribute_terms = $block->context['woocommerce/attributeTerms'];
-		$default_option  = array(
-			'label'      => esc_html__( 'Choose an option', 'woocommerce' ),
-			'value'      => '',
-			'isSelected' => false,
-		);
-
-		$attribute_terms = array_merge(
-			array( $default_option ),
-			$attribute_terms
-		);
-
-		$selected_attribute         = $this->get_default_selected_attribute( $attribute_slug, $attribute_terms );
-		$autoselect                 = $attributes['autoselect'] ?? false;
-		$disabled_attributes_action = $attributes['disabledAttributesAction'] ?? 'disable';
-
-		$options = '';
-		foreach ( $attribute_terms as $attribute_term ) {
-			$option_attributes = array(
-				'value'                  => $attribute_term['value'],
-				'data-wp-bind--selected' => 'state.isOptionSelected',
-				'data-wp-bind--disabled' => 'state.isOptionDisabled',
-				'data-wp-bind--hidden'   => 'hide' === $disabled_attributes_action ? 'state.isOptionDisabled' : null,
-				'data-wp-context'        => array(
-					'option' => $attribute_term,
-				),
-			);
-
-			if ( $attribute_term['value'] === $selected_attribute ) {
-				$option_attributes['selected'] = 'selected';
-			}
-
-			$options .= sprintf(
-				'<option %s>%s</option>',
-				$this->get_normalized_attributes(
-					$option_attributes
-				),
-				esc_html( $attribute_term['label'] )
-			);
-		}
-
-		return sprintf(
-			'<select %s>%s</select>',
-			$this->get_normalized_attributes(
-				array(
-					'class'              => 'wc-block-add-to-cart-with-options-variation-selector-attribute-options__dropdown',
-					'id'                 => $attribute_id,
-					'data-wp-context'    => array(
-						'name'          => wc_attribute_label( $block->context['woocommerce/attributeName'] ),
-						'options'       => $attribute_terms,
-						'selectedValue' => $selected_attribute,
-						'autoselect'    => $autoselect,
-					),
-					'data-wp-init'       => 'callbacks.setDefaultSelectedAttribute',
-					'data-wp-on--change' => 'actions.handleDropdownChange',
-					'name'               => $attribute_slug,
-				),
-			),
-			$options,
-		);
-	}
-}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Dropdown.php b/plugins/woocommerce/src/Blocks/BlockTypes/Dropdown.php
new file mode 100644
index 00000000000..e0021e8e15d
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Dropdown.php
@@ -0,0 +1,148 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Blocks\BlockTypes;
+
+/**
+ * Dropdown block (native select).
+ */
+final class Dropdown extends AbstractBlock {
+
+	use EnableBlockJsonAssetsTrait;
+
+	/**
+	 * Block name.
+	 *
+	 * @var string
+	 */
+	protected $block_name = 'dropdown';
+
+	/**
+	 * Plain-text label for a select option (no HTML in `<option>`).
+	 *
+	 * @param array $item Selectable item from context.
+	 * @return string
+	 */
+	private function get_option_text( array $item ): string {
+		if ( isset( $item['label'] ) && is_string( $item['label'] ) ) {
+			return wp_strip_all_tags( $item['label'] );
+		}
+		if ( ! empty( $item['ariaLabel'] ) && is_string( $item['ariaLabel'] ) ) {
+			return $item['ariaLabel'];
+		}
+		return '';
+	}
+
+	/**
+	 * Render the block.
+	 *
+	 * @param array     $attributes Block attributes.
+	 * @param string    $content    Block content.
+	 * @param \WP_Block $block      Block instance.
+	 * @return string Rendered block type output.
+	 */
+	protected function render( $attributes, $content, $block ) {
+		if ( empty( $block->context['woocommerce/selectableItems'] ) ) {
+			return '';
+		}
+
+		$selectable_items = $block->context['woocommerce/selectableItems'];
+		$items            = is_array( $selectable_items['items'] ?? null ) ? $selectable_items['items'] : array();
+		$store_namespace  = is_string( $selectable_items['storeNamespace'] ?? null ) ? $selectable_items['storeNamespace'] : 'woocommerce/add-to-cart-with-options';
+
+		if ( empty( $items ) ) {
+			return '';
+		}
+
+		$attribute_id       = $block->context['woocommerce/attributeId'] ?? '';
+		$has_external_label = is_string( $attribute_id ) && '' !== $attribute_id;
+		$select_id          = $has_external_label
+			? $attribute_id
+			: wp_unique_id( 'wc-block-dropdown-' );
+
+		$wrapper_attributes = array(
+			'data-wp-interactive' => 'woocommerce/dropdown',
+			'data-wp-context'     => (string) wp_json_encode(
+				array(
+					'storeNamespace' => $store_namespace,
+				),
+				JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
+			),
+		);
+
+		$aria_label = isset( $selectable_items['groupLabel'] ) && is_string( $selectable_items['groupLabel'] ) ? $selectable_items['groupLabel'] : __( 'Choose an option', 'woocommerce' );
+
+		ob_start();
+		?>
+		<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
+			<fieldset class="wc-block-dropdown__fieldset">
+				<?php if ( ! $has_external_label ) : ?>
+					<legend class="screen-reader-text"><?php echo esc_html( __( 'Choose an option', 'woocommerce' ) ); ?></legend>
+				<?php endif; ?>
+				<select
+					class="wc-block-dropdown__select"
+					id="<?php echo esc_attr( $select_id ); ?>"
+					<?php if ( $has_external_label ) : ?>
+						aria-labelledby="<?php echo esc_attr( $attribute_id . '_label' ); ?>"
+					<?php else : ?>
+						aria-label="<?php echo esc_attr( $aria_label ); ?>"
+					<?php endif; ?>
+					data-wp-bind--value="state.selectValue"
+					data-wp-on--change="actions.onDropdownChange"
+				>
+					<option value="">
+						<?php echo esc_html( __( 'Choose an option', 'woocommerce' ) ); ?>
+					</option>
+					<?php foreach ( $items as $item ) : ?>
+						<?php
+						if ( ! is_array( $item ) ) {
+							continue;
+						}
+						$option_label = $this->get_option_text( $item );
+						if ( empty( $option_label ) ) {
+							continue;
+						}
+						$item_id    = isset( $item['id'] ) && is_string( $item['id'] ) ? $item['id'] : '';
+						$item_value = isset( $item['value'] ) && is_string( $item['value'] ) ? $item['value'] : '';
+						?>
+						<option
+							id="<?php echo esc_attr( $item_id ); ?>"
+							value="<?php echo esc_attr( $item_value ); ?>"
+							<?php disabled( ! empty( $item['disabled'] ) ); ?>
+							<?php
+							if ( ! empty( $item['hidden'] ) ) :
+								?>
+							hidden
+								<?php
+							endif;
+							if ( ! empty( $item['selected'] ) ) :
+								?>
+								selected
+								<?php
+							endif;
+							?>
+							data-wp-each-child
+						>
+							<?php echo esc_html( $option_label ); ?>
+						</option>
+					<?php endforeach; ?>
+					<template
+						data-wp-interactive="<?php echo esc_attr( $store_namespace ); ?>"
+						data-wp-each--item="state.selectableItems"
+						data-wp-each-key="context.item.id"
+					>
+						<option
+							data-wp-bind--value="context.item.value"
+							data-wp-bind--disabled="context.item.disabled"
+							data-wp-bind--hidden="context.item.hidden"
+							data-wp-text="context.item.label"
+						></option>
+					</template>
+				</select>
+			</fieldset>
+		</div>
+		<?php
+		$output = ob_get_clean();
+		return is_string( $output ) ? $output : '';
+	}
+}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php
index bccb3fc60d0..de18788a701 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php
@@ -100,7 +100,7 @@ final class ProductFilterActive extends AbstractBlock {
 			array_reduce(
 				$block->parsed_block['innerBlocks'],
 				function ( $carry, $parsed_block ) use ( $filter_context ) {
-					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerceRemovableItems' => $filter_context ) ) )->render();
+					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerce/removableItems' => $filter_context ) ) )->render();
 					return $carry;
 				},
 				''
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
index abc09e8c2a5..4613b625bb9 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
@@ -298,7 +298,7 @@ final class ProductFilterAttribute extends AbstractBlock {
 			array_reduce(
 				$block->parsed_block['innerBlocks'],
 				function ( $carry, $parsed_block ) use ( $filter_context ) {
-					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerceSelectableItems' => $filter_context ) ) )->render();
+					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerce/selectableItems' => $filter_context ) ) )->render();
 					return $carry;
 				},
 				''
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php
index 624bc998a54..d739edc81e2 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php
@@ -33,11 +33,11 @@ final class ProductFilterCheckboxList extends AbstractBlock {
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		if ( empty( $block->context['woocommerceSelectableItems'] ) ) {
+		if ( empty( $block->context['woocommerce/selectableItems'] ) ) {
 			return '';
 		}

-		$block_context   = $block->context['woocommerceSelectableItems'];
+		$block_context   = $block->context['woocommerce/selectableItems'];
 		$items           = is_array( $block_context['items'] ?? null ) ? $block_context['items'] : array();
 		$store_namespace = $block_context['storeNamespace'] ?? 'woocommerce/product-filters';
 		$filter_type     = $block_context['filterType'] ?? '';
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php
index a15ac2d1fd8..81b7b8b5eca 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php
@@ -17,30 +17,23 @@ final class ProductFilterChips extends AbstractBlock {
 	 */
 	protected $block_name = 'product-filter-chips';

-	/**
-	 * Default number of items to show before "Show more" button.
-	 *
-	 * @var int
-	 */
-	const DISPLAY_LIMIT = 15;
-
 	/**
 	 * Render the block.
 	 *
-	 * @param array    $attributes Block attributes.
-	 * @param string   $content    Block content.
-	 * @param WP_Block $block      Block instance.
+	 * @param array     $attributes Block attributes.
+	 * @param string    $content    Block content.
+	 * @param \WP_Block $block      Block instance.
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		if ( empty( $block->context['woocommerceSelectableItems'] ) ) {
+		if ( empty( $block->context['woocommerce/selectableItems'] ) ) {
 			return '';
 		}

-		$block_context   = $block->context['woocommerceSelectableItems'];
+		$block_context   = $block->context['woocommerce/selectableItems'];
 		$items           = is_array( $block_context['items'] ?? null ) ? $block_context['items'] : array();
 		$store_namespace = $block_context['storeNamespace'] ?? 'woocommerce/product-filters';
-		$display_limit   = self::DISPLAY_LIMIT;
+		$display_limit   = 'woocommerce/product-filters' === $store_namespace ? 15 : 30;
 		$classes         = '';
 		$style           = '';

@@ -51,8 +44,9 @@ final class ProductFilterChips extends AbstractBlock {
 		}

 		$wrapper_attributes = array(
-			'data-wp-interactive' => 'woocommerce/product-filter-chips',
-			'data-wp-context'     => (string) wp_json_encode(
+			'data-wp-interactive'  => 'woocommerce/product-filter-chips',
+			'data-wp-init--colors' => 'callbacks.initColors',
+			'data-wp-context'      => (string) wp_json_encode(
 				array(
 					'storeNamespace' => $store_namespace,
 					'displayLimit'   => $display_limit,
@@ -60,7 +54,7 @@ final class ProductFilterChips extends AbstractBlock {
 				),
 				JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
 			),
-			'class'               => esc_attr( $classes ),
+			'class'                => esc_attr( $classes ),
 		);

 		if ( ! empty( $style ) ) {
@@ -68,6 +62,14 @@ final class ProductFilterChips extends AbstractBlock {
 			$wrapper_attributes['style'] = esc_attr( $style ) . ';';
 		}

+		$attribute_id       = $block->context['woocommerce/attributeId'] ?? '';
+		$has_external_label = is_string( $attribute_id ) && '' !== $attribute_id;
+
+		if ( $has_external_label ) {
+			$wrapper_attributes['role']            = 'single' === $block_context['selectionMode'] ? 'radiogroup' : 'group';
+			$wrapper_attributes['aria-labelledby'] = esc_attr( $attribute_id . '_label' );
+		}
+
 		$first_items             = array_slice( $items, 0, $display_limit, true );
 		$overflow_items          = array_slice( $items, $display_limit );
 		$overflow_selected_items = array_filter( $overflow_items, fn( $item ) => is_array( $item ) && ! empty( $item['selected'] ) );
@@ -77,6 +79,7 @@ final class ProductFilterChips extends AbstractBlock {
 		$first_item         = reset( $items );
 		$show_counts        = is_array( $first_item ) && array_key_exists( 'count', $first_item );
 		$has_color_swatches = is_array( $first_item ) && array_key_exists( 'color', $first_item );
+		$button_role        = 'single' === $block_context['selectionMode'] ? 'radio' : 'checkbox';

 		if ( $has_color_swatches && is_string( $classes ) && ! str_contains( $classes, 'is-style-swatch' ) ) {
 			$classes                    .= ' is-style-swatch';
@@ -86,8 +89,8 @@ final class ProductFilterChips extends AbstractBlock {
 		ob_start();
 		?>
 		<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
-			<fieldset>
-				<?php if ( ! empty( $block_context['groupLabel'] ) ) : ?>
+			<fieldset class="wc-block-product-filter-chips__fieldset">
+				<?php if ( ! empty( $block_context['groupLabel'] ) && ! $has_external_label ) : ?>
 					<legend class="screen-reader-text"><?php echo esc_html( $block_context['groupLabel'] ); ?></legend>
 				<?php endif; ?>
 				<div class="wc-block-product-filter-chips__items">
@@ -97,7 +100,7 @@ final class ProductFilterChips extends AbstractBlock {
 						<button
 							class="wc-block-product-filter-chips__item"
 							type="button"
-							role="checkbox"
+							role="<?php echo esc_attr( $button_role ); ?>"
 							id="<?php echo esc_attr( $item['id'] ); ?>"
 							<?php if ( ! empty( $item['ariaLabel'] ) ) : ?>
 								aria-label="<?php echo esc_attr( $item['ariaLabel'] ); ?>"
@@ -143,7 +146,7 @@ final class ProductFilterChips extends AbstractBlock {
 						<button
 							class="wc-block-product-filter-chips__item"
 							type="button"
-							role="checkbox"
+							role="<?php echo esc_attr( $button_role ); ?>"
 							data-wp-bind--id="context.item.id"
 							data-wp-bind--aria-label="context.item.ariaLabel"
 							<?php if ( $has_color_swatches ) : ?>
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterClearButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterClearButton.php
index d79c7171468..b0a53943269 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterClearButton.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterClearButton.php
@@ -37,7 +37,7 @@ final class ProductFilterClearButton extends AbstractBlock {
 	 */
 	protected function render( $attributes, $content, $block ) {
 		// don't render if its admin, or ajax in progress.
-		$removable_context = $block->context['woocommerceRemovableItems'] ?? null;
+		$removable_context = $block->context['woocommerce/removableItems'] ?? null;
 		if (
 			is_admin() ||
 			wp_doing_ajax() ||
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php
index 5136b0b993b..7b31811cef7 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php
@@ -178,7 +178,7 @@ final class ProductFilterPrice extends AbstractBlock {
 			array_reduce(
 				$block->parsed_block['innerBlocks'],
 				function ( $carry, $parsed_block ) use ( $filter_context ) {
-					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerceRangeInput' => $filter_context ) ) )->render();
+					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerce/rangeInput' => $filter_context ) ) )->render();
 					return $carry;
 				},
 				''
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPriceSlider.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPriceSlider.php
index f0a2c944f7b..e200cf39d01 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPriceSlider.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPriceSlider.php
@@ -32,11 +32,11 @@ class ProductFilterPriceSlider extends AbstractBlock {
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		if ( is_admin() || wp_doing_ajax() || empty( $block->context['woocommerceRangeInput'] ) ) {
+		if ( is_admin() || wp_doing_ajax() || empty( $block->context['woocommerce/rangeInput'] ) ) {
 			return '';
 		}

-		$range_data = $block->context['woocommerceRangeInput'];
+		$range_data = $block->context['woocommerce/rangeInput'];
 		$min_range  = $range_data['min'] ?? 0;
 		$max_range  = $range_data['max'] ?? 0;
 		$min_price  = $range_data['currentMin'] ?? $min_range;
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php
index c28a20ac1b7..aeebec806dc 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php
@@ -162,7 +162,7 @@ final class ProductFilterRating extends AbstractBlock {
 			array_reduce(
 				$block->parsed_block['innerBlocks'],
 				function ( $carry, $parsed_block ) use ( $filter_context ) {
-					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerceSelectableItems' => $filter_context ) ) )->render();
+					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerce/selectableItems' => $filter_context ) ) )->render();
 					return $carry;
 				},
 				''
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRemovableChips.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRemovableChips.php
index d27174ddf9c..1750c3d7263 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRemovableChips.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRemovableChips.php
@@ -27,12 +27,12 @@ final class ProductFilterRemovableChips extends AbstractBlock {
 	 */
 	protected function render( $attributes, $content, $block ) {
 		if (
-			empty( $block->context['woocommerceRemovableItems'] )
+			empty( $block->context['woocommerce/removableItems'] )
 		) {
 			return '';
 		}

-		$filter_items = $block->context['woocommerceRemovableItems']['items'] ?? array();
+		$filter_items = $block->context['woocommerce/removableItems']['items'] ?? array();

 		$style = '';

@@ -72,8 +72,13 @@ final class ProductFilterRemovableChips extends AbstractBlock {
 					</li>
 				</template>
 				<?php foreach ( $filter_items as $item ) : ?>
-					<?php // translators: %s: item label. ?>
-					<?php $remove_label = sprintf( __( 'Remove filter: %s', 'woocommerce' ), $item['label'] ); ?>
+					<?php
+					$remove_label = sprintf(
+						/* translators: %s: item label. */
+						__( 'Remove filter: %s', 'woocommerce' ),
+						$item['label']
+					);
+					?>
 					<li class="wc-block-product-filter-removable-chips__item" data-wp-each-child
 						<?php echo wp_interactivity_data_wp_context( array( 'item' => $item ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
 					>
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStatus.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStatus.php
index 56e2488c49c..12bbc4edde4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStatus.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStatus.php
@@ -164,7 +164,7 @@ final class ProductFilterStatus extends AbstractBlock {
 			array_reduce(
 				$block->parsed_block['innerBlocks'],
 				function ( $carry, $parsed_block ) use ( $filter_context ) {
-					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerceSelectableItems' => $filter_context ) ) )->render();
+					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerce/selectableItems' => $filter_context ) ) )->render();
 					return $carry;
 				},
 				''
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
index d569892218c..859beba25d3 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
@@ -301,7 +301,7 @@ final class ProductFilterTaxonomy extends AbstractBlock {
 			array_reduce(
 				$block->parsed_block['innerBlocks'],
 				function ( $carry, $parsed_block ) use ( $filter_context ) {
-					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerceSelectableItems' => $filter_context ) ) )->render();
+					$carry .= ( new \WP_Block( $parsed_block, array( 'woocommerce/selectableItems' => $filter_context ) ) )->render();
 					return $carry;
 				},
 				''
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index afd97ad90a5..046a9111098 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -381,6 +381,7 @@ final class BlockTypesController {
 			'CatalogSorting',
 			'ClassicShortcode',
 			'CustomerAccount',
+			'Dropdown',
 			'FeaturedCategory',
 			'FeaturedProduct',
 			'MiniCart',
@@ -449,6 +450,7 @@ final class BlockTypesController {
 			'ComingSoon',
 			'CouponCode',
 			'CustomerAccount',
+			'Dropdown',
 			'EmailContent',
 			'FeaturedCategory',
 			'FeaturedProduct',
@@ -560,7 +562,6 @@ final class BlockTypesController {
 			$block_types[] = 'AddToCartWithOptions\VariationSelector';
 			$block_types[] = 'AddToCartWithOptions\VariationSelectorAttribute';
 			$block_types[] = 'AddToCartWithOptions\VariationSelectorAttributeName';
-			$block_types[] = 'AddToCartWithOptions\VariationSelectorAttributeOptions';
 			$block_types[] = 'AddToCartWithOptions\GroupedProductSelector';
 			$block_types[] = 'AddToCartWithOptions\GroupedProductItem';
 			$block_types[] = 'AddToCartWithOptions\GroupedProductItemSelector';
diff --git a/plugins/woocommerce/templates/parts/variable-product-add-to-cart-with-options.html b/plugins/woocommerce/templates/parts/variable-product-add-to-cart-with-options.html
index df818a2d28b..f9eb81d8eb0 100644
--- a/plugins/woocommerce/templates/parts/variable-product-add-to-cart-with-options.html
+++ b/plugins/woocommerce/templates/parts/variable-product-add-to-cart-with-options.html
@@ -15,7 +15,9 @@
 		>
 			<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-name /-->

-			<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-options /-->
+			<!-- wp:woocommerce/product-filter-chips -->
+			<div class="wp-block-woocommerce-product-filter-chips wc-block-product-filter-chips"></div>
+			<!-- /wp:woocommerce/product-filter-chips -->
 		</div>
 		<!-- /wp:group -->
 	</div>
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
index 79fcd36a01e..2ffa2160dcb 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
@@ -15,7 +15,6 @@ use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsGroupedProduct
 use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorMock;
 use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeMock;
 use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeNameMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeOptionsMock;
 use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils;

 /**
@@ -47,7 +46,6 @@ class AddToCartWithOptions extends \WP_UnitTestCase {
 			new AddToCartWithOptionsVariationSelectorMock();
 			new AddToCartWithOptionsVariationSelectorAttributeMock();
 			new AddToCartWithOptionsVariationSelectorAttributeNameMock();
-			new AddToCartWithOptionsVariationSelectorAttributeOptionsMock();

 			self::$are_blocks_registered = true;
 		}
@@ -471,54 +469,6 @@ class AddToCartWithOptions extends \WP_UnitTestCase {
 		);
 	}

-	/**
-	 * Tests that the VariationSelectorAttribute block returns empty string
-	 * for non-variable products (simple, grouped, external) to prevent
-	 * a fatal error from calling get_variation_attributes() on unsupported
-	 * product types.
-	 *
-	 * @covers VariationSelectorAttribute::render
-	 */
-	public function test_variation_selector_attribute_returns_empty_for_non_variable_products() {
-		global $product;
-		$original_product = $product;
-
-		$block_markup = '<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute /-->';
-
-		try {
-			// Test with a missing/invalid product context.
-			$product = null;
-			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string when the global product is null.' );
-
-			$product = false;
-			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string when the global product is false.' );
-
-			// Test with a simple product.
-			$simple_product = new \WC_Product_Simple();
-			$simple_product->set_regular_price( 10 );
-			$simple_product->save();
-
-			$product = $simple_product;
-			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string for simple products.' );
-
-			// Test with a grouped product.
-			$grouped_product = new \WC_Product_Grouped();
-			$grouped_product->save();
-
-			$product = $grouped_product;
-			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string for grouped products.' );
-
-			// Test with an external product.
-			$external_product = new \WC_Product_External();
-			$external_product->save();
-
-			$product = $external_product;
-			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string for external products.' );
-		} finally {
-			$product = $original_product;
-		}//end try
-	}
-
 	/**
 	 * Tests that the quantity selector and its steppers are hidden when
 	 * a filter sets min and max quantity to the same value for a product.
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
new file mode 100644
index 00000000000..c1a068206ca
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
@@ -0,0 +1,268 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use Automattic\WooCommerce\Enums\ProductStockStatus;
+use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeMock;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeNameMock;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the VariationSelectorAttribute block type.
+ */
+class VariationSelectorAttribute extends WC_Unit_Test_Case {
+
+	/**
+	 * Tracks whether blocks have been registered.
+	 *
+	 * @var bool
+	 */
+	protected static $are_blocks_registered = false;
+
+	/**
+	 * Register blocks required for do_blocks tests.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		if ( ! self::$are_blocks_registered ) {
+			new AddToCartWithOptionsVariationSelectorAttributeMock();
+			new AddToCartWithOptionsVariationSelectorAttributeNameMock();
+
+			self::$are_blocks_registered = true;
+		}
+	}
+
+	/**
+	 * Data provider for legacy Attribute Options block replacement styles.
+	 *
+	 * @return array<string, array{0: array<string, string>, 1: string, 2: string}>
+	 */
+	public function legacy_attribute_options_block_styles_provider(): array {
+		return array(
+			'dropdown' => array(
+				array( 'optionStyle' => 'dropdown' ),
+				'wc-block-dropdown',
+				'wc-block-product-filter-chips',
+			),
+			'pills'    => array(
+				array( 'optionStyle' => 'pills' ),
+				'wc-block-product-filter-chips',
+				'wc-block-dropdown',
+			),
+			'default'  => array(
+				array(),
+				'wc-block-product-filter-chips',
+				'wc-block-dropdown',
+			),
+		);
+	}
+
+	/**
+	 * Tests that the block returns empty string for non-variable products.
+	 *
+	 * @testdox VariationSelectorAttribute returns an empty string for non-variable products.
+	 * @covers \Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\VariationSelectorAttribute::render
+	 */
+	public function test_returns_empty_for_non_variable_products(): void {
+		global $product;
+		$original_product = $product;
+
+		$block_markup = '<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute /-->';
+
+		try {
+			$product = null;
+			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string when the global product is null.' );
+
+			$product = false;
+			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string when the global product is false.' );
+
+			$simple_product = new \WC_Product_Simple();
+			$simple_product->set_regular_price( 10 );
+			$simple_product->save();
+
+			$product = $simple_product;
+			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string for simple products.' );
+
+			$grouped_product = new \WC_Product_Grouped();
+			$grouped_product->save();
+
+			$product = $grouped_product;
+			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string for grouped products.' );
+
+			$external_product = new \WC_Product_External();
+			$external_product->save();
+
+			$product = $external_product;
+			$this->assertSame( '', do_blocks( $block_markup ), 'VariationSelectorAttribute should return empty string for external products.' );
+		} finally {
+			$product = $original_product;
+		}
+	}
+
+	/**
+	 * Tests that legacy Attribute Options blocks are replaced with dropdown or chips blocks when rendered.
+	 *
+	 * @testdox Legacy Attribute Options block renders with %2$s and not %3$s.
+	 * @dataProvider legacy_attribute_options_block_styles_provider
+	 * @covers \Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\VariationSelectorAttribute::replace_legacy_attribute_options_block
+	 *
+	 * @param array  $options_attrs          Legacy options block attributes.
+	 * @param string $expected_output_class  CSS class expected in rendered output.
+	 * @param string $unexpected_output_class CSS class that should not appear in rendered output.
+	 */
+	public function test_replaces_legacy_attribute_options_block_when_rendered( array $options_attrs, string $expected_output_class, string $unexpected_output_class ): void {
+		$variable_product = $this->create_variable_product_with_variations();
+		$inner_blocks     = $this->get_attribute_name_block_markup() . $this->get_legacy_options_block_markup( $options_attrs );
+
+		$markup = $this->render_variation_selector_attribute( $variable_product, $inner_blocks );
+
+		$this->assertStringContainsString( 'variation-selector-attribute-name', $markup, 'Attribute name block should render.' );
+		$this->assertStringContainsString( $expected_output_class, $markup, 'Legacy Attribute Options block should render as the replacement block.' );
+		$this->assertStringNotContainsString( $unexpected_output_class, $markup, 'Legacy Attribute Options block should not render as the other option style.' );
+	}
+
+	/**
+	 * Tests that autoselect and disabledAttributesAction are migrated from legacy Attribute Options blocks.
+	 *
+	 * @testdox autoselect and disabledAttributesAction are migrated from legacy Attribute Options block.
+	 * @covers \Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\VariationSelectorAttribute::replace_legacy_attribute_options_block
+	 */
+	public function test_migrates_legacy_attribute_options_settings_when_rendered(): void {
+		$variable_product = $this->create_variable_product_with_variations();
+		$inner_blocks     = $this->get_attribute_name_block_markup() . $this->get_legacy_options_block_markup(
+			array(
+				'autoselect'               => true,
+				'disabledAttributesAction' => 'hide',
+			)
+		);
+
+		$markup = $this->render_variation_selector_attribute( $variable_product, $inner_blocks );
+
+		$this->assertStringContainsString( '"autoselect":true', $markup, 'Legacy autoselect should be applied to the interactivity context.' );
+		$this->assertStringContainsString( '"disabledAttributesAction":"hide"', $markup, 'Legacy disabledAttributesAction should be applied to the interactivity context.' );
+	}
+
+	/**
+	 * Tests that legacy Attribute Options blocks nested in a group are replaced when rendered.
+	 *
+	 * @testdox Legacy Attribute Options block nested in a group is replaced with a dropdown when rendered.
+	 * @covers \Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\VariationSelectorAttribute::replace_legacy_attribute_options_block
+	 */
+	public function test_replaces_nested_legacy_attribute_options_block_when_rendered(): void {
+		$variable_product = $this->create_variable_product_with_variations();
+		$inner_blocks     = sprintf(
+			'<!-- wp:group -->%s%s<!-- /wp:group -->',
+			$this->get_attribute_name_block_markup(),
+			$this->get_legacy_options_block_markup( array( 'optionStyle' => 'dropdown' ) )
+		);
+
+		$markup = $this->render_variation_selector_attribute( $variable_product, $inner_blocks );
+
+		$this->assertStringContainsString( 'variation-selector-attribute-name', $markup, 'Attribute name block should render.' );
+		$this->assertStringContainsString( 'wc-block-dropdown', $markup, 'Nested legacy Attribute Options block should render as a dropdown.' );
+		$this->assertStringNotContainsString( 'wc-block-product-filter-chips', $markup, 'Nested legacy Attribute Options block should not render as chips.' );
+	}
+
+	/**
+	 * Create a variable product with color variations for rendering tests.
+	 *
+	 * @return \WC_Product_Variable
+	 */
+	private function create_variable_product_with_variations(): \WC_Product_Variable {
+		$fixtures = new FixtureData();
+
+		$product = $fixtures->get_variable_product(
+			array(),
+			array(
+				$fixtures->get_product_attribute( 'color', array( 'red', 'blue' ) ),
+			)
+		);
+
+		$product_id = $product->get_id();
+
+		$fixtures->get_variation_product(
+			$product_id,
+			array(
+				'pa_color' => 'red-slug',
+			),
+			array(
+				'regular_price' => 10,
+				'stock_status'  => ProductStockStatus::IN_STOCK,
+			)
+		);
+
+		$fixtures->get_variation_product(
+			$product_id,
+			array(
+				'pa_color' => 'blue-slug',
+			),
+			array(
+				'regular_price' => 10,
+				'stock_status'  => ProductStockStatus::IN_STOCK,
+			)
+		);
+
+		\WC_Product_Variable::sync( $product_id );
+
+		$variable_product = wc_get_product( $product_id );
+
+		$this->assertInstanceOf( \WC_Product_Variable::class, $variable_product );
+
+		return $variable_product;
+	}
+
+	/**
+	 * Render the variation selector attribute block with the given inner block markup.
+	 *
+	 * @param \WC_Product_Variable $variable_product  Variable product to use as context.
+	 * @param string               $inner_blocks_markup Inner blocks markup.
+	 * @return string Rendered block output.
+	 */
+	private function render_variation_selector_attribute( \WC_Product_Variable $variable_product, string $inner_blocks_markup ): string {
+		global $product;
+
+		$original_product = $product;
+		$product          = $variable_product;
+
+		$block_markup = sprintf(
+			'<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute -->%s<!-- /wp:woocommerce/add-to-cart-with-options-variation-selector-attribute -->',
+			$inner_blocks_markup
+		);
+
+		try {
+			return do_blocks( $block_markup );
+		} finally {
+			$product = $original_product;
+		}
+	}
+
+	/**
+	 * Get block markup for the attribute name inner block.
+	 *
+	 * @return string
+	 */
+	private function get_attribute_name_block_markup(): string {
+		return '<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-name /-->';
+	}
+
+	/**
+	 * Get block markup for the legacy Attribute Options inner block.
+	 *
+	 * @param array $attrs Legacy options block attributes.
+	 * @return string
+	 */
+	private function get_legacy_options_block_markup( array $attrs ): string {
+		if ( empty( $attrs ) ) {
+			return '<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-options /-->';
+		}
+
+		return sprintf(
+			'<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-options %s /-->',
+			wp_json_encode( $attrs )
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Mocks/AddToCartWithOptionsVariationSelectorAttributeOptionsMock.php b/plugins/woocommerce/tests/php/src/Blocks/Mocks/AddToCartWithOptionsVariationSelectorAttributeOptionsMock.php
deleted file mode 100644
index 24e55bafdb6..00000000000
--- a/plugins/woocommerce/tests/php/src/Blocks/Mocks/AddToCartWithOptionsVariationSelectorAttributeOptionsMock.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Blocks\Mocks;
-
-use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\VariationSelectorAttributeOptions;
-use Automattic\WooCommerce\Blocks\Package;
-use Automattic\WooCommerce\Blocks\Assets\Api;
-use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
-use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
-
-/**
- * AddToCartWithOptionsVariationSelectorAttributeOptionsMock used to test VariationSelectorAttributeOptions block functions.
- */
-class AddToCartWithOptionsVariationSelectorAttributeOptionsMock extends VariationSelectorAttributeOptions {
-	/**
-	 * Initialize our mock class.
-	 */
-	public function __construct() {
-		parent::__construct(
-			Package::container()->get( Api::class ),
-			Package::container()->get( AssetDataRegistry::class ),
-			new IntegrationRegistry(),
-		);
-	}
-}