Классические текстовые полосы в шапке сайта, сообщающие о сумме до бесплатной доставки, отлично справляются со своей задачей, но часто выглядят громоздко и отнимают ценное пространство на экране. Современный 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-функции серверного обработчика.
