agentsclimarketplace

Wc product attribute swatches

Skill Lonsdale201/wp-agent-skills/woocommerce/wc-product-attribute-swatches

Build or audit WooCommerce 10.9+ product attribute swatch integrations around the experimental `wc-visual` attribute type. Covers feature-flag gating, global `pa_*` attribute taxonomy type, color/image term meta, Store API `__experimental_visual` / `__experimentalVisual`, classic variation dropdown fallbacks, and safe plugin/theme rendering. Use when code or requests mention variation swatches, visual attributes, `wc-visual`, `wc_visual_attribute_type`, `term_color`, `term_image`, `__experimental_visual`, `ProductAttributeTerms`, or custom swatch UI.From its SKILL.md

Install
npx -y skills add Lonsdale201/wp-agent-skills --skill wc-product-attribute-swatches

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

13.5 KB, ~3.0k tokens by cl100k_base, as published. Nobody here has run it

WooCommerce product attribute swatches

Use this skill when a plugin or theme needs to read, write, render, or audit WooCommerce visual product attributes. In WooCommerce 10.9.4 this is not a mature "classic variation swatches" template API. It is an experimental wc-visual product attribute type with color/image term metadata, consumed by selected block UI and optionally exposed by Store API.

Source-verified status in 10.9.4

  • Feature ID: wc-visual-attribute.
  • Feature option: woocommerce_feature_wc_visual_attribute_enabled.
  • Default: experimental and disabled by default.
  • Admin UI gating: the feature setting UI is disabled on non-block themes; wc_get_attribute_types() only exposes wc-visual when the site is a block theme with the feature enabled, or when the store already has an existing wc-visual attribute.
  • Attribute type slug: wc-visual.
  • Admin label: Color / image.
  • Visual term value types: color, image, none.
  • Supported core term meta: color hex string and image attachment ID. Image wins over color when both exist; core save logic deletes the other key.
  • Classic single-product variable template still renders wc_dropdown_variation_attribute_options() selects. It does not output swatch buttons by itself.
  • Store API visual data is opt-in and experimental: request __experimental_visual=true; response property is __experimentalVisual.

Data model

Only global product attributes can be visual attributes. A visual attribute is still a WooCommerce attribute taxonomy:

woocommerce_attribute_taxonomies.attribute_name = color
woocommerce_attribute_taxonomies.attribute_type = wc-visual
taxonomy slug                                  = pa_color
term meta color                               = #2271b1
term meta image                               = attachment ID

Do not treat custom per-product text attributes as swatch sources. They have no term IDs, no color or image term meta, and no Store API visual payload.

Safe read helper

Avoid importing Automattic\WooCommerce\Internal\ProductAttributes\VisualAttributeTermMeta in plugin code unless there is no alternative; it is marked @internal. Mirror the storage contract through public WP/Woo APIs instead.

function myplugin_is_wc_visual_attribute_taxonomy( string $taxonomy ): bool {
    if ( ! function_exists( 'wc_get_attribute_taxonomies' ) || ! function_exists( 'wc_attribute_taxonomy_name' ) ) {
        return false;
    }

    foreach ( wc_get_attribute_taxonomies() as $attribute ) {
        if (
            isset( $attribute->attribute_type, $attribute->attribute_name ) &&
            'wc-visual' === $attribute->attribute_type &&
            wc_attribute_taxonomy_name( $attribute->attribute_name ) === $taxonomy
        ) {
            return true;
        }
    }

    return false;
}

function myplugin_get_wc_term_visual( int $term_id, string $image_size = 'thumbnail' ): array {
    $image_id = absint( get_term_meta( $term_id, 'image', true ) );

    if ( $image_id && wp_attachment_is_image( $image_id ) ) {
        $image_url = wp_get_attachment_image_url( $image_id, $image_size );

        if ( $image_url ) {
            return array(
                'type'  => 'image',
                'value' => $image_url,
            );
        }
    }

    $color = sanitize_hex_color( get_term_meta( $term_id, 'color', true ) );

    if ( $color ) {
        return array(
            'type'  => 'color',
            'value' => $color,
        );
    }

    return array(
        'type'  => 'none',
        'value' => '',
    );
}

For lists, call update_meta_cache( 'term', $term_ids ) 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 wp_get_attachment_image_url().

Safe write helper

Write mutually exclusive term meta. Validate capability and nonce in the caller; this helper only normalizes storage.

function myplugin_set_wc_term_visual( int $term_id, string $color = '', int $image_id = 0 ): void {
    if ( $image_id && wp_attachment_is_image( $image_id ) ) {
        update_term_meta( $term_id, 'image', absint( $image_id ) );
        delete_term_meta( $term_id, 'color' );
        return;
    }

    $color = sanitize_hex_color( $color );

    if ( $color ) {
        update_term_meta( $term_id, 'color', $color );
        delete_term_meta( $term_id, 'image' );
        return;
    }

    delete_term_meta( $term_id, 'color' );
    delete_term_meta( $term_id, 'image' );
}

When creating an attribute programmatically, verify that wc_get_attribute_types() currently contains wc-visual. wc_create_attribute() validates the type against that function and silently falls back to select when wc-visual is not available.

if ( array_key_exists( 'wc-visual', wc_get_attribute_types() ) ) {
    $attribute_id = wc_create_attribute( array(
        'name'     => 'Color',
        'slug'     => 'color',
        'type'     => 'wc-visual',
        'order_by' => 'menu_order',
    ) );
}

Do not force-create visual attributes on classic-theme stores just to get swatches. In 10.9.4 WooCommerce intentionally hides the feature setting UI outside block themes unless a visual attribute already exists.

Store API

Use Store API only for shopper-facing reads. Fetch attribute IDs first, then opt into experimental visual data for terms:

GET /wp-json/wc/store/v1/products/attributes
GET /wp-json/wc/store/v1/products/attributes/12/terms?__experimental_visual=true

Returned term objects may include:

{
  "id": 34,
  "name": "Blue",
  "slug": "blue",
  "__experimentalVisual": {
    "type": "color",
    "value": "#2271b1"
  }
}

Rules:

  • The property appears only when __experimental_visual is true and the term belongs to a wc-visual taxonomy.
  • type=image returns an image URL, not an attachment object.
  • type=color returns a sanitized hex color.
  • type=none means no valid visual value.
  • Do not rely on this field as a stable non-experimental API until WooCommerce removes the experimental prefix.
  • WC REST /wc/v3/products/attributes exposes the attribute type, but do not assume the classic REST attribute-term endpoints expose the visual payload.

Classic theme rendering

For classic templates, keep the core select in place. The variation form JS reads .variations select, the attribute_pa_* field names, and change events. Swatch buttons should drive the select, not replace the submission contract.

Append swatches through woocommerce_dropdown_variation_attribute_options_html:

add_filter(
    'woocommerce_dropdown_variation_attribute_options_html',
    function ( string $html, array $args ): string {
        $product   = $args['product'] ?? null;
        $taxonomy  = isset( $args['attribute'] ) ? (string) $args['attribute'] : '';
        $options   = isset( $args['options'] ) && is_array( $args['options'] ) ? $args['options'] : array();
        $selected  = isset( $args['selected'] ) ? (string) $args['selected'] : '';

        if ( ! $product instanceof WC_Product || ! taxonomy_exists( $taxonomy ) ) {
            return $html;
        }

        if ( ! myplugin_is_wc_visual_attribute_taxonomy( $taxonomy ) ) {
            return $html;
        }

        $terms = wc_get_product_terms( $product->get_id(), $taxonomy, array( 'fields' => 'all' ) );
        if ( empty( $terms ) || is_wp_error( $terms ) ) {
            return $html;
        }

        $out = '<div class="myplugin-wc-swatches" role="group" aria-label="' . esc_attr( wc_attribute_label( $taxonomy ) ) . '">';

        foreach ( $terms as $term ) {
            if ( ! in_array( $term->slug, $options, true ) ) {
                continue;
            }

            $visual = myplugin_get_wc_term_visual( (int) $term->term_id );
            $style  = '';

            if ( 'color' === $visual['type'] ) {
                $style = 'background-color:' . esc_attr( $visual['value'] );
            } elseif ( 'image' === $visual['type'] ) {
                $style = "background-image:url('" . esc_url( $visual['value'] ) . "')";
            }

            $out .= sprintf(
                '<button type="button" class="myplugin-wc-swatch" data-value="%1$s" aria-pressed="%2$s" aria-label="%3$s"><span class="myplugin-wc-swatch__visual" style="%4$s" aria-hidden="true"></span><span class="screen-reader-text">%5$s</span></button>',
                esc_attr( $term->slug ),
                $selected === $term->slug ? 'true' : 'false',
                esc_attr( sprintf( '%s: %s', wc_attribute_label( $taxonomy ), $term->name ) ),
                esc_attr( $style ),
                esc_html( $term->name )
            );
        }

        $out .= '</div>';

        return $html . $out;
    },
    20,
    2
);

Then sync button clicks to the native select:

jQuery( function ( $ ) {
    $( document ).on( 'click', '.myplugin-wc-swatch', function () {
        var $button = $( this );
        var $wrap = $button.closest( '.value' );
        var $select = $wrap.find( 'select' );

        $select.val( $button.data( 'value' ) ).trigger( 'change' );
        $wrap.find( '.myplugin-wc-swatch' ).attr( 'aria-pressed', 'false' );
        $button.attr( 'aria-pressed', 'true' );
    } );

    $( '.variations_form' ).on( 'woocommerce_update_variation_values reset_data', function () {
        $( this ).find( '.value' ).each( function () {
            var $wrap = $( this );
            var $select = $wrap.find( 'select' );

            $wrap.find( '.myplugin-wc-swatch' ).each( function () {
                var value = String( $( this ).data( 'value' ) );
                var enabled = $select.find( 'option' ).filter( function () {
                    return this.value === value && ! this.disabled;
                } ).length > 0;

                $( this ).prop( 'disabled', ! enabled );
            } );

            $wrap.find( '.myplugin-wc-swatch' ).attr( 'aria-pressed', 'false' )
                .filter( function () {
                    return String( $( this ).data( 'value' ) ) === String( $select.val() || '' );
                } )
                .attr( 'aria-pressed', 'true' );
        } );
    } );

    $( document ).on( 'change', '.variations_form .variations select', function () {
        $( this ).closest( '.variations_form' ).trigger( 'woocommerce_update_variation_values' );
    } );
} );

Keep the native select available as the authoritative accessible control. Swatches are enhancement buttons: use type="button", an accessible name, synchronized aria-pressed, visible focus styles, and disabled states matching the select. If a design hides the native select, replace this shortcut pattern with a complete radio-group implementation including arrow-key behavior; visual buttons alone are not an accessible select replacement.

Common mistakes

  • Calling this "variation swatches" and storing data on product_variation posts. In core 10.9.4 the swatch data belongs to attribute terms, not variations.
  • Removing the select from classic variation forms. Core JS and POST handling expect attribute_pa_* select values.
  • Creating wc-visual attributes on classic-theme stores and assuming the feature is supported. The 10.9.4 UI gate is deliberate.
  • Importing internal Woo classes as if they were stable public APIs.
  • Assuming Store API visual data is returned by default. It requires __experimental_visual=true.
  • Treating image swatches as attachment arrays in Store API. The value is a URL string.
  • Do not hardcode Woo admin CSS classes as frontend contracts or confuse term swatches with per-variation galleries.

Use wc-variations-data for real variation CRUD/sync, wc-variation-gallery for per-variation image sets, wc-store-api for headless reads, and wc-variations-pricing-filters when selection affects price/availability display.

References

  • Official documentation: https://woocommerce.com/document/variable-product/
  • Official documentation: https://developer.woocommerce.com/docs/apis/store-api/
  • Verified source paths:
    • wp-content/plugins/woocommerce/includes/wc-attribute-functions.php
    • wp-content/plugins/woocommerce/src/Internal/Features/FeaturesController.php
    • wp-content/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermMeta.php
    • wp-content/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php
    • wp-content/plugins/woocommerce/src/StoreApi/Routes/V1/ProductAttributeTerms.php
    • wp-content/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductAttributeTermSchema.php
    • wp-content/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
    • wp-content/plugins/woocommerce/includes/wc-template-functions.php
    • wp-content/plugins/woocommerce/templates/single-product/add-to-cart/variable.php

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,834. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.