{"slug":"wc-product-attribute-swatches","title":"wc-product-attribute-swatches","summary":"Build or audit WooCommerce product attribute swatch integrations around the experimental `wc-visual` attribute type. Covers feature gating, global `pa_*` attribute taxonomies, the WooCommerce 11 slug byte limit, color/image term meta, visual admin search results, Store API `__exp","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:17.605754Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-26T23:03:36Z"},"bodyHtml":"<hr>\n<h2>name: wc-product-attribute-swatches\ndescription: Build or audit WooCommerce product attribute swatch integrations around the experimental <code>wc-visual</code> attribute type. Covers feature gating, global <code>pa_*</code> attribute taxonomies, the WooCommerce 11 slug byte limit, color/image term meta, visual admin search results, Store API <code>__experimental_visual</code> / <code>__experimentalVisual</code>, classic dropdown fallbacks, and safe rendering. Use for variation swatches, visual attributes, <code>wc-visual</code>, <code>term_color</code>, <code>term_image</code>, <code>woocommerce_json_search_found_product_attribute_terms</code>, or custom swatch UI.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"woocommerce\"\nwp-skills-plugin-version-tested: \"11.0.0\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-08-05\"</h2>\n<h1>WooCommerce product attribute swatches</h1>\n<p>Use this skill when a plugin or theme needs to read, write, render, or audit WooCommerce visual product attributes. In WooCommerce 11.0 this is not a mature \"classic variation swatches\" template API. It is an experimental <code>wc-visual</code> product attribute type with color/image term metadata, consumed by selected block UI and optionally exposed by Store API.</p>\n<h2>Source-verified status in 11.0.0</h2>\n<ul>\n<li>Feature ID: <code>wc-visual-attribute</code>.</li>\n<li>Feature option: <code>woocommerce_feature_wc_visual_attribute_enabled</code>.</li>\n<li>Default: experimental and disabled by default.</li>\n<li>Admin UI gating: the feature setting UI is disabled on non-block themes; <code>wc_get_attribute_types()</code> only exposes <code>wc-visual</code> when the site is a block theme with the feature enabled, or when the store already has an existing <code>wc-visual</code> attribute.</li>\n<li>Attribute type slug: <code>wc-visual</code>.</li>\n<li>Admin label: <code>Color / image</code>.</li>\n<li>Visual term value types: <code>color</code>, <code>image</code>, <code>none</code>.</li>\n<li>Supported core term meta: <code>color</code> hex string and <code>image</code> attachment ID. Image wins over color when both exist; core save logic deletes the other key.</li>\n<li>Classic single-product variable template still renders <code>wc_dropdown_variation_attribute_options()</code> selects. It does not output swatch buttons by itself.</li>\n<li>Store API visual data is opt-in and experimental: request <code>__experimental_visual=true</code>; response property is <code>__experimentalVisual</code>.</li>\n</ul>\n<h2>Data model</h2>\n<p>Only global product attributes can be visual attributes. A visual attribute is still a WooCommerce attribute taxonomy:</p>\n<pre><code>woocommerce_attribute_taxonomies.attribute_name = color\nwoocommerce_attribute_taxonomies.attribute_type = wc-visual\ntaxonomy slug                                  = pa_color\nterm meta color                               = #2271b1\nterm meta image                               = attachment ID\n</code></pre>\n<p>Do not treat custom per-product text attributes as swatch sources. They have no term IDs, no <code>color</code> or <code>image</code> term meta, and no Store API visual payload.</p>\n<h2>Safe read helper</h2>\n<p>Avoid importing <code>Automattic\\WooCommerce\\Internal\\ProductAttributes\\VisualAttributeTermMeta</code> in plugin code unless there is no alternative; it is marked <code>@internal</code>. Mirror the storage contract through public WP/Woo APIs instead.</p>\n<pre><code>function myplugin_is_wc_visual_attribute_taxonomy( string $taxonomy ): bool {\n    if ( ! function_exists( 'wc_get_attribute_taxonomies' ) || ! function_exists( 'wc_attribute_taxonomy_name' ) ) {\n        return false;\n    }\n\n    foreach ( wc_get_attribute_taxonomies() as $attribute ) {\n        if (\n            isset( $attribute-&gt;attribute_type, $attribute-&gt;attribute_name ) &amp;&amp;\n            'wc-visual' === $attribute-&gt;attribute_type &amp;&amp;\n            wc_attribute_taxonomy_name( $attribute-&gt;attribute_name ) === $taxonomy\n        ) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nfunction myplugin_get_wc_term_visual( int $term_id, string $image_size = 'thumbnail' ): array {\n    $image_id = absint( get_term_meta( $term_id, 'image', true ) );\n\n    if ( $image_id &amp;&amp; wp_attachment_is_image( $image_id ) ) {\n        $image_url = wp_get_attachment_image_url( $image_id, $image_size );\n\n        if ( $image_url ) {\n            return array(\n                'type'  =&gt; 'image',\n                'value' =&gt; $image_url,\n            );\n        }\n    }\n\n    $color = sanitize_hex_color( get_term_meta( $term_id, 'color', true ) );\n\n    if ( $color ) {\n        return array(\n            'type'  =&gt; 'color',\n            'value' =&gt; $color,\n        );\n    }\n\n    return array(\n        'type'  =&gt; 'none',\n        'value' =&gt; '',\n    );\n}\n</code></pre>\n<p>For lists, call <code>update_meta_cache( 'term', $term_ids )</code> before looping terms. If image swatches are common and the page renders many terms, collect attachment IDs from term meta and prime post caches before calling <code>wp_get_attachment_image_url()</code>.</p>\n<h2>Safe write helper</h2>\n<p>Write mutually exclusive term meta. Validate capability and nonce in the caller; this helper only normalizes storage.</p>\n<pre><code>function myplugin_set_wc_term_visual( int $term_id, string $color = '', int $image_id = 0 ): void {\n    if ( $image_id &amp;&amp; wp_attachment_is_image( $image_id ) ) {\n        update_term_meta( $term_id, 'image', absint( $image_id ) );\n        delete_term_meta( $term_id, 'color' );\n        return;\n    }\n\n    $color = sanitize_hex_color( $color );\n\n    if ( $color ) {\n        update_term_meta( $term_id, 'color', $color );\n        delete_term_meta( $term_id, 'image' );\n        return;\n    }\n\n    delete_term_meta( $term_id, 'color' );\n    delete_term_meta( $term_id, 'image' );\n}\n</code></pre>\n<p>When creating an attribute programmatically, verify that <code>wc_get_attribute_types()</code> currently contains <code>wc-visual</code>. <code>wc_create_attribute()</code> validates the type against that function and silently falls back to <code>select</code> when <code>wc-visual</code> is not available.</p>\n<pre><code>if ( array_key_exists( 'wc-visual', wc_get_attribute_types() ) ) {\n    $attribute_id = wc_create_attribute( array(\n        'name'     =&gt; 'Color',\n        'slug'     =&gt; 'color',\n        'type'     =&gt; 'wc-visual',\n        'order_by' =&gt; 'menu_order',\n    ) );\n}\n</code></pre>\n<p>Do not force-create visual attributes on classic-theme stores just to get swatches. In 11.0 WooCommerce intentionally hides the feature setting UI outside block themes unless a visual attribute already exists.</p>\n<h3>Attribute slug byte limit in WooCommerce 11.0</h3>\n<p>WordPress limits taxonomy names to 32 bytes and Woo prefixes global product attributes with <code>pa_</code>. WooCommerce 11.0 exposes <code>wc_get_attribute_slug_max_byte_length()</code>, currently 29 bytes, and validates with <code>strlen()</code> rather than a character count.</p>\n<p>Use the helper when validating/importing attribute slugs and return the <code>WP_Error</code> from <code>wc_create_attribute()</code> instead of truncating blindly. A 29-character multibyte slug can exceed 29 bytes; truncating by characters can still create an invalid taxonomy or collide with another normalized slug.</p>\n<h2>Store API</h2>\n<p>Use Store API only for shopper-facing reads. Fetch attribute IDs first, then opt into experimental visual data for terms:</p>\n<pre><code>GET /wp-json/wc/store/v1/products/attributes\nGET /wp-json/wc/store/v1/products/attributes/12/terms?__experimental_visual=true\n</code></pre>\n<p>Returned term objects may include:</p>\n<pre><code>{\n  \"id\": 34,\n  \"name\": \"Blue\",\n  \"slug\": \"blue\",\n  \"__experimentalVisual\": {\n    \"type\": \"color\",\n    \"value\": \"#2271b1\"\n  }\n}\n</code></pre>\n<p>Rules:</p>\n<ul>\n<li>The property appears only when <code>__experimental_visual</code> is true and the term belongs to a <code>wc-visual</code> taxonomy.</li>\n<li><code>type=image</code> returns an image URL, not an attachment object.</li>\n<li><code>type=color</code> returns a sanitized hex color.</li>\n<li><code>type=none</code> means no valid visual value.</li>\n<li>Do not rely on this field as a stable non-experimental API until WooCommerce removes the experimental prefix.</li>\n<li>WC REST <code>/wc/v3/products/attributes</code> exposes the attribute <code>type</code>, but do not assume the classic REST attribute-term endpoints expose the visual payload.</li>\n</ul>\n<h2>Admin AJAX term-search shape in WooCommerce 11.0</h2>\n<p>For a visual taxonomy, WooCommerce enriches valid term objects returned through <code>woocommerce_json_search_found_product_attribute_terms</code> with a dynamic <code>visual</code> property shaped as <code>{ type, value }</code>. Types are <code>color</code>, <code>image</code>, or <code>none</code>; image values are thumbnail URLs.</p>\n<p>Treat the property defensively: the core enrichment callback is internal, non-visual taxonomies do not receive it, earlier Woo versions omit it, and another filter may return an error or nonstandard entries. Do not mutate the public filter's results into a different base shape that breaks Woo's admin selector.</p>\n<h2>Classic theme rendering</h2>\n<p>Keep the native <code>.variations select</code> and <code>attribute_pa_*</code> field as the authoritative submission/accessibility contract. Swatch buttons are progressive enhancement: drive the select, trigger <code>change</code>, and synchronize selection and disabled states after Woo's <code>woocommerce_update_variation_values</code> and <code>reset_data</code> events.</p>\n<p>Use <code>type=\"button\"</code>, accessible names, synchronized <code>aria-pressed</code>, visible focus, and disabled states matching the select. If the design removes the select, implement a complete accessible radio-group including keyboard behavior. See <a href=\"references/classic-rendering.md\">references/classic-rendering.md</a> for the PHP filter and JavaScript synchronization pattern.</p>\n<h2>Common mistakes</h2>\n<ul>\n<li>Calling this \"variation swatches\" and storing data on <code>product_variation</code> posts. In core 11.0 the swatch data belongs to attribute terms, not variations.</li>\n<li>Removing the select from classic variation forms. Core JS and POST handling expect <code>attribute_pa_*</code> select values.</li>\n<li>Creating <code>wc-visual</code> attributes on classic-theme stores and assuming the feature is supported. The 11.0 UI gate is deliberate.</li>\n<li>Counting attribute slug characters instead of UTF-8 bytes or hardcoding a limit instead of calling <code>wc_get_attribute_slug_max_byte_length()</code>.</li>\n<li>Importing internal Woo classes as if they were stable public APIs.</li>\n<li>Assuming Store API visual data is returned by default. It requires <code>__experimental_visual=true</code>.</li>\n<li>Treating image swatches as attachment arrays in Store API. The value is a URL string.</li>\n<li>Do not hardcode Woo admin CSS classes as frontend contracts or confuse term swatches with per-variation galleries.</li>\n</ul>\n<p>Use <code>wc-variations-data</code> for real variation CRUD/sync, <code>wc-variation-gallery</code> for per-variation image sets, and <code>wc-variations-pricing-filters</code> when selection affects price/availability display.</p>\n<h2>References</h2>\n<ul>\n<li>Official documentation: <a href=\"https://woocommerce.com/document/variable-product/\">https://woocommerce.com/document/variable-product/</a></li>\n<li>Official documentation: <a href=\"https://developer.woocommerce.com/docs/apis/store-api/\">https://developer.woocommerce.com/docs/apis/store-api/</a></li>\n<li>Verified source paths:\n<ul>\n<li><code>wp-content/plugins/woocommerce/includes/wc-attribute-functions.php</code></li>\n<li><code>wp-content/plugins/woocommerce/src/Internal/Features/FeaturesController.php</code></li>\n<li><code>wp-content/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermMeta.php</code></li>\n<li><code>wp-content/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php</code></li>\n<li><code>wp-content/plugins/woocommerce/src/StoreApi/Routes/V1/ProductAttributeTerms.php</code></li>\n<li><code>wp-content/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductAttributeTermSchema.php</code></li>\n<li><code>wp-content/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php</code></li>\n<li><code>wp-content/plugins/woocommerce/includes/wc-template-functions.php</code></li>\n<li><code>wp-content/plugins/woocommerce/templates/single-product/add-to-cart/variable.php</code></li>\n</ul>\n</li>\n</ul>\n","files":[{"path":"references/classic-rendering.md","sizeBytes":4101,"isText":true},{"path":"SKILL.md","sizeBytes":11224,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:58:18.19227Z","sha256":"2321F0F451148B55B95F0A62597264589A6DFAADDBE31DCD7F60A098EF222EB0","sizeBytes":5685},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"woocommerce/wc-product-attribute-swatches","license":"MIT","commit":"c51b571a259f0c4b5f5c0a3bc50ed580c6851f98","subtreeSha":"CF86F824BAE82032FEB6FA97AFC788D20A75F8A6A8DDC59171EF0524372DEE32","lastSyncedAt":"2026-09-29T23:33:03.303675Z"},"reviewedAt":"2026-09-16T15:18:08.681175Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-product-attribute-swatches"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}