Создаем умный плавающий виджет бесплатной доставки для WooCommerce

Создаем умный плавающий виджет бесплатной доставки для WooCommerce

от Михаил | Мар 20, 2026 | Уроки

Классические текстовые полосы в шапке сайта, сообщающие о сумме до бесплатной доставки, отлично справляются со своей задачей, но часто выглядят громоздко и отнимают ценное пространство на экране. Современный UX-дизайн требует более элегантных решений.

В этом уроке мы создадим аккуратный плавающий виджет в виде круга, который фиксируется в нижнем углу экрана и ненавязчиво сопровождает пользователя в процессе покупок.

Главная особенность этого сниппета — полноценная цепочка микроинтеракций, как в качественных мобильных приложениях. Когда покупатель добавляет товар в корзину, SVG-кольцо плавно заполняется пропорционально сумме заказа. При наведении на виджет появляется аккуратная всплывающая подсказка с точным остатком до заветного порога.

Самое интересное происходит в момент, когда сумма корзины достигает цели. Вместо того чтобы просто исчезнуть, виджет отыгрывает красивую секвенцию: кольцо плавно доходит до полных 100%, иконка грузовика уезжает в сторону, уступая место галочке, после чего происходит аккуратный залп конфетти. Только после этой визуальной награды виджет красиво «схлопывается» и исчезает с экрана.

Помимо визуальной части, код написан с упором на максимальную совместимость. Благодаря глобальному перехватчику AJAX-запросов, виджет в реальном времени реагирует на любые изменения в корзине, будь то стандартная страница оформления заказа или сторонние плагины всплывающих корзин (например, WPC Fly Cart).

Код виджета

Для внедрения этого функционала достаточно скопировать приведенный ниже код и добавить его в файл functions.php вашей дочерней темы или использовать плагин для пользовательских сниппетов.

<?php
// 1. Выводим HTML структуру виджета в футер
function custom_floating_shipping_widget() {
    if (!class_exists('WooCommerce')) return;
    ?>
    <div id="floating-shipping-widget" style="display: none;">
        <!-- viewBox позволяет SVG-графике резиново масштабироваться под размер родителя -->
        <svg class="progress-ring" viewBox="0 0 54 54" width="100%" height="100%">
            <!-- Фоновый серый круг -->
            <circle class="progress-ring__background" stroke="#f0f0f0" stroke-width="4" fill="white" r="24" cx="27" cy="27"/>
            <!-- Красный круг прогресса -->
            <circle class="progress-ring__circle" stroke="#792423" stroke-width="4" fill="transparent" r="24" cx="27" cy="27"/>
        </svg>
        
        <!-- Иконки (Грузовик и Галочка) -->
        <div class="widget-icon">
            <svg class="icon-truck" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
                <rect x="1" y="3" width="15" height="13"></rect>
                <polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon>
                <circle cx="5.5" cy="18.5" r="2.5"></circle>
                <circle cx="18.5" cy="18.5" r="2.5"></circle>
            </svg>
            <svg class="icon-check" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="20 6 9 17 4 12"></polyline>
            </svg>
        </div>

        <!-- Всплывающая подсказка -->
        <div class="widget-tooltip">
            <span class="tooltip-text">До бесплатной доставки:</span>
            <strong class="tooltip-amount">...</strong>
        </div>
    </div>
    <?php
}
add_action('wp_footer', 'custom_floating_shipping_widget');

// 2. Подключаем CSS стили
function custom_floating_shipping_styles() {
    ?>
    <style>
        #floating-shipping-widget {
            /* Базовые размеры виджета */
            width: 54px;
            height: 54px;
            
            position: fixed;
            bottom: 10px;
            left: 10px;
            z-index: 999999;
            border-radius: 50%;
            box-shadow: 0 4px 15px rgba(0,0,0,0.15);
            cursor: default;
            transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            background: white; 
        }
        
        #floating-shipping-widget.hidden-with-effect {
            opacity: 0;
            transform: scale(0) rotate(-180deg);
            pointer-events: none;
        }

        #floating-shipping-widget:hover {
            transform: scale(1.05);
        }

        .progress-ring {
            position: absolute;
            top: 0;
            left: 0;
            display: block; 
        }

        .progress-ring__circle {
            transition: stroke-dashoffset 0.6s ease-in-out;
            transform: rotate(-90deg);
            transform-origin: 50% 50%;
            stroke-linecap: round; 
        }

        /* Адаптивный контейнер для иконок */
        .widget-icon {
            position: absolute;
            width: 45%;
            height: 45%;
            color: #792423; 
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .widget-icon svg {
            position: absolute;
            width: 100%;
            height: 100%;
            transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
        }

        .icon-check {
            opacity: 0;
            transform: scale(0) rotate(-45deg);
        }

        .widget-reached .icon-truck {
            opacity: 0;
            transform: scale(0) translate(20px, 0); 
        }

        .widget-reached .icon-check {
            opacity: 1;
            transform: scale(1) rotate(0);
        }

        /* Стилизация подсказки */
        .widget-tooltip {
            position: absolute;
            left: calc(100% + 15px);
            top: 50%;
            transform: translateY(-50%) translateX(10px); 
            background: #333;
            color: #fff;
            padding: 6px 12px;
            border-radius: 6px;
            font-size: 13px;
            font-family: sans-serif;
            white-space: nowrap;
            opacity: 0;
            visibility: hidden;
            transition: all 0.3s ease;
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
            pointer-events: none;
        }

        .widget-tooltip::before {
            content: '';
            position: absolute;
            right: 100%;
            top: 50%;
            transform: translateY(-50%);
            border-width: 5px;
            border-style: solid;
            border-color: transparent #333 transparent transparent;
        }

        #floating-shipping-widget:hover .widget-tooltip {
            opacity: 1;
            visibility: visible;
            transform: translateY(-50%) translateX(0);
        }
        
        @media (max-width: 768px) {
            .widget-tooltip { display: none; }
        }
    </style>
    <?php
}
add_action('wp_head', 'custom_floating_shipping_styles');

// 3. Серверная логика и расчет прогресса
function custom_free_shipping_widget_ajax() {
    if (isset($_POST['action']) && $_POST['action'] === 'get_shipping_progress') {
        $current_total = WC()->cart->get_cart_contents_total();
        // Укажите здесь свой порог бесплатной доставки
        $threshold = 5500; 
        
        $progress = min(($current_total / $threshold) * 100, 100);
        $remaining = max($threshold - $current_total, 0);
        
        wp_send_json([
            'show_bar' => $current_total > 0 && $current_total < $threshold,
            'progress' => $progress,
            'remaining' => wc_price($remaining),
            'reached' => $current_total >= $threshold
        ]);
    }
}
add_action('wp_ajax_get_shipping_progress', 'custom_free_shipping_widget_ajax');
add_action('wp_ajax_nopriv_get_shipping_progress', 'custom_free_shipping_widget_ajax');

// 4. Подключение JavaScript и управление анимациями
function custom_floating_shipping_scripts() {
    ?>
    <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
    
    <script type="text/javascript">
        jQuery(document).ready(function($) {
            const $widget = $('#floating-shipping-widget');
            if ($widget.length === 0) return;

            const circle = document.querySelector('.progress-ring__circle');
            const radius = circle.r.baseVal.value;
            const circumference = radius * 2 * Math.PI;

            circle.style.strokeDasharray = `${circumference} ${circumference}`;
            circle.style.strokeDashoffset = circumference;

            let updateTimeout;
            let isFirstLoad = true;
            let lastState = 'init';
            let isAnimatingCelebration = false;

            function setProgress(percent) {
                const offset = circumference - (percent / 100) * circumference;
                circle.style.strokeDashoffset = offset;
            }

            function triggerConfetti() {
                const rect = $widget[0].getBoundingClientRect();
                const originX = (rect.left + rect.width / 2) / window.innerWidth;
                const originY = (rect.top + rect.height / 2) / window.innerHeight;

                confetti({
                    particleCount: 20,
                    spread: 60,
                    startVelocity: 25,
                    origin: { x: originX, y: originY },
                    colors: ['#792423', '#ffffff', '#e0e0e0'],
                    scalar: 0.6,
                    ticks: 150,
                    zIndex: 999999
                });
            }

            function updateShippingWidget() {
                clearTimeout(updateTimeout);
                
                updateTimeout = setTimeout(() => {
                    $.post('<?php echo admin_url('admin-ajax.php'); ?>', {
                        action: 'get_shipping_progress'
                    }, function(response) {
                        
                        let currentState = response.reached ? 'reached' : (response.show_bar ? 'progress' : 'empty');

                        if (currentState === 'reached') {
                            if (!isFirstLoad && lastState !== 'reached') {
                                isAnimatingCelebration = true;
                                $widget.removeClass('hidden-with-effect widget-reached');
                                $widget.show(); 
                                
                                setTimeout(() => { setProgress(100); }, 50);
                                setTimeout(() => { $widget.addClass('widget-reached'); }, 600);
                                setTimeout(() => { triggerConfetti(); }, 1100);
                                setTimeout(() => {
                                    if (lastState === 'reached') { 
                                        $widget.addClass('hidden-with-effect');
                                        setTimeout(() => { 
                                            $widget.hide(); 
                                            $widget.removeClass('widget-reached'); 
                                            isAnimatingCelebration = false;
                                        }, 500);
                                    } else {
                                        isAnimatingCelebration = false;
                                    }
                                }, 2600);

                            } else if (!isAnimatingCelebration) {
                                $widget.hide();
                            }
                        } else if (currentState === 'progress') {
                            if (isAnimatingCelebration) isAnimatingCelebration = false;
                            
                            $widget.removeClass('hidden-with-effect widget-reached');
                            if (!$widget.is(':visible')) $widget.fadeIn(300);
                            
                            setTimeout(() => { setProgress(response.progress); }, 50);
                            $widget.find('.tooltip-amount').html(response.remaining);
                        } else {
                            if (!isAnimatingCelebration) {
                                $widget.fadeOut(300);
                                $widget.removeClass('widget-reached');
                            }
                        }
                        
                        lastState = currentState;
                        isFirstLoad = false;
                    });
                }, 500);
            }

            // Стандартные триггеры WooCommerce
            const events = [
                'updated_cart_totals', 'added_to_cart', 'removed_from_cart', 'updated_wc_div'
            ];
            events.forEach(event => { $(document.body).on(event, updateShippingWidget); });

            // Глобальный перехватчик для кастомных AJAX корзин
            $(document).ajaxComplete(function(event, xhr, settings) {
                if (settings.url && (settings.url.includes('wc-ajax') || settings.url.includes('admin-ajax.php'))) {
                    if (settings.data && typeof settings.data === 'string' && settings.data.includes('action=get_shipping_progress')) return;
                    updateShippingWidget();
                }
            });

            updateShippingWidget();
        });
    </script>
    <?php
}
add_action('wp_footer', 'custom_floating_shipping_scripts');

Как адаптировать код под свой проект

Вся логика отрисовки построена на использовании SVG с атрибутом viewBox, что избавляет вас от необходимости пересчитывать радиусы и координаты вручную. Если вы решите сделать виджет крупнее или меньше, достаточно изменить значения width и height в блоке стилей #floating-shipping-widget. Иконки и круговая шкала пропорционально смасштабируются сами.

Чтобы изменить фирменные цвета виджета, обратите внимание на параметр stroke="#792423" в HTML-разметке красного круга, а также на CSS-класс .widget-icon и массив colors в настройках библиотеки конфетти в JavaScript. Порог суммы для бесплатной доставки настраивается в переменной $threshold внутри PHP-функции серверного обработчика.

Было полезно?

Пожалуйста, расскажите об этом друзьям!

VK
Pinterest
OK
Telegram
Linkedin
WhatsApp
Viber
Reddit

Готовы получать больше клиентов?

Ваш сайт может быть не просто визиткой, а полноценным инструментом продаж — работать 24/7, привлекать заявки и усиливать доверие к вашему бизнесу. Оставьте заявку — и мы подскажем, какое решение подойдёт именно вам.

Вам может быть интересно

Telegram Почта Телефон