Barra de anuncios animada para WordPress (topbar)

gratuita, liviana y personalizable
Desarrollo web

En muchos sitios WordPress que administro, necesitaba una forma simple y visible de mostrar promociones, mensajes importantes o anuncios destacados. Algo que no recargue el sitio, que sea fácil de configurar y que se vea bien en cualquier diseño.

Así nació este desarrollo: una barra superior de anuncios animada para WordPress, con modo marquee y slider, totalmente gratuita y pensada para que cualquiera pueda instalarla sin complicaciones.

¿Qué es y para qué sirve?

Este topbar para WordPress te permite mostrar mensajes dinámicos en la parte superior del sitio. Ideal para:

  • Promociones activas
  • Avisos importantes
  • Novedades del blog o tienda
  • Mensajes personalizados para tus visitantes

Funciona como un carrusel de mensajes o un ticker de texto animado, según el modo que elijas.

Características principales

Desde el panel de administración de WordPress podés:

  • Agregar mensajes: uno por línea o en formato JSON para mayor control.
  • Elegir ubicación: mostrar en todo el sitio, solo en ciertas páginas o excluir otras.
  • Seleccionar animación:
    • Marquee: texto deslizante continuo, estilo cartel digital.
    • Slider: los mensajes rotan cada cierto tiempo.
  • Ajustar velocidad y espaciado: definís cuántos píxeles por segundo se mueve el texto y cuánto espacio hay entre mensajes.
  • Aplicar estilos personalizados: colores, tamaños, tipografía… todo editable con CSS.
barra animada anuncios wordpress woocommerce backend

Instalación rápida

Podés instalarlo como:

Descargar twm-topbar
Descargar plugin

Personalización libre

El código es abierto y editable. Podés modificar estilos, agregar funciones o adaptarlo a tu diseño sin restricciones.

Si necesitás ayuda para ajustarlo, escribime y lo vemos juntos.

				
					/**
 * Plugin Name: Tu Web Master - TopBar Slider
 * Plugin URI:  https://www.tuwebmaster.com.ar
 * Description: Muestra un topbar deslizante o marquee con anuncios, mensajes o promociones en todo el sitio o páginas seleccionadas.
 * Version:     1.0.0
 * Author:      Tu Web Master
 * Author URI:  https://www.tuwebmaster.com.ar
 * License:     GPLv2 or later
 * Text Domain: twm-topbar
 */

if ( ! defined('ABSPATH') ) exit;

/** ---------------------------
 *  Settings: register options
 *  --------------------------- */
add_action('admin_menu', 'twm_topbar_add_menu');
function twm_topbar_add_menu() {
    add_menu_page(
        'TopBar Slider',
        'TopBar Slider',
        'manage_options',
        'topbar-slider',
        'twm_topbar_settings_page',
        'dashicons-megaphone',
        65
    );
    add_action('admin_init', 'twm_topbar_register_settings');
}

function twm_topbar_register_settings() {
    // Slides: newline or JSON array
    register_setting('twm_topbar_group', 'twm_topbar_slides', [
        'type' => 'string',
        'sanitize_callback' => 'twm_topbar_sanitize_slides',
    ]);

    // Display mode (where): all | only | exclude
    register_setting('twm_topbar_group', 'twm_topbar_display_mode', [
        'type' => 'string',
        'default' => 'all',
        'sanitize_callback' => function($v){ $v = sanitize_key($v); return in_array($v, ['all','only','exclude'], true) ? $v : 'all'; }
    ]);
    register_setting('twm_topbar_group', 'twm_topbar_pages_only', [
        'type' => 'string',
        'sanitize_callback' => 'twm_topbar_sanitize_csv_tokens'
    ]);
    register_setting('twm_topbar_group', 'twm_topbar_pages_exclude', [
        'type' => 'string',
        'sanitize_callback' => 'twm_topbar_sanitize_csv_tokens'
    ]);

    // Behavior mode (how): slides | marquee
    register_setting('twm_topbar_group', 'twm_topbar_mode', [
        'type' => 'string',
        'default' => 'slides',
        'sanitize_callback' => function($v){ $v = sanitize_key($v); return in_array($v, ['slides','marquee'], true) ? $v : 'slides'; }
    ]);

    // Slides mode options
    register_setting('twm_topbar_group', 'twm_topbar_interval_ms', [
        'type' => 'integer',
        'default' => 4000,
        'sanitize_callback' => function($v){ $v = intval($v); return $v > 200 ? $v : 4000; }
    ]);

    // Marquee mode options
    register_setting('twm_topbar_group', 'twm_topbar_speed_px_s', [
        'type' => 'integer',
        'default' => 80, // pixels per second
        'sanitize_callback' => function($v){ $v = intval($v); return $v > 10 ? $v : 80; }
    ]);
    register_setting('twm_topbar_group', 'twm_topbar_gap_px', [
        'type' => 'integer',
        'default' => 40,
        'sanitize_callback' => function($v){ $v = intval($v); return $v >= 0 ? $v : 40; }
    ]);

    // Custom CSS (admins only)
    register_setting('twm_topbar_group', 'twm_topbar_custom_css', [
        'type' => 'string',
        'sanitize_callback' => 'twm_topbar_sanitize_css'
    ]);
}

function twm_topbar_sanitize_slides($raw) {
    $raw = is_string($raw) ? trim(wp_unslash($raw)) : '';
    if ($raw === '') return '';
    if (str_starts_with($raw, '[')) {
        $arr = json_decode($raw, true);
        if (is_array($arr)) {
            $arr = array_map(function($s){ return trim(wp_strip_all_tags((string)$s)); }, $arr);
            $arr = array_filter($arr, fn($s)=> $s !== '');
            return implode("\n", $arr);
        }
    }
    $lines = preg_split('/\r\n|\r|\n/', $raw);
    $lines = array_map(function($s){ return trim(wp_strip_all_tags((string)$s)); }, $lines);
    $lines = array_filter($lines, fn($s)=> $s !== '');
    return implode("\n", $lines);
}

function twm_topbar_sanitize_csv_tokens($raw) {
    $raw = is_string($raw) ? $raw : '';
    $parts = array_map('trim', explode(',', $raw));
    $parts = array_filter($parts, fn($t)=> $t !== '');
    $clean = array_map(function($t){
        if (is_numeric($t)) return (string)intval($t);
        $t = strtolower($t);
        $t = preg_replace('~[^a-z0-9/_-]+~', '', $t);
        return trim($t, '/');
    }, $parts);
    $clean = array_unique(array_filter($clean, fn($t)=> $t !== ''));
    return implode(',', $clean);
}

function twm_topbar_sanitize_css($raw) {
    $raw = is_string($raw) ? $raw : '';
    return str_replace(["\r\n","\r"], "\n", $raw);
}

/** -----------------------------
 *  Admin page (simple settings)
 *  ----------------------------- */
function twm_topbar_settings_page() {
    if ( ! current_user_can('manage_options') ) return;

    $slides         = get_option('twm_topbar_slides', "");
    $display_mode   = get_option('twm_topbar_display_mode', "all");
    $pages_only     = get_option('twm_topbar_pages_only', "");
    $pages_exclude  = get_option('twm_topbar_pages_exclude', "");
    $mode           = get_option('twm_topbar_mode', 'slides');
    $interval_ms    = intval(get_option('twm_topbar_interval_ms', 4000));
    $speed_px_s     = intval(get_option('twm_topbar_speed_px_s', 80));
    $gap_px         = intval(get_option('twm_topbar_gap_px', 40));
    $custom_css     = get_option('twm_topbar_custom_css', "");
    ?>
    <div class="wrap">
        <h1>TopBar Slider — Ajustes</h1>
        <form method="post" action="options.php">
            <?php settings_fields('twm_topbar_group'); do_settings_sections('twm_topbar_group'); ?>

            <table class="form-table" role="presentation">
                <tr>
                    <th scope="row"><label for="twm_topbar_slides">Mensajes (uno por línea o JSON)</label></th>
                    <td>
                        <textarea id="twm_topbar_slides" name="twm_topbar_slides" rows="8" cols="80" style="width: 600px;"><?php echo esc_textarea($slides); ?></textarea>
                        <p class="description">Ejemplo por línea: <code>¡Envíos a todo el país!</code><br>Ejemplo JSON: <code>["Promo 2x1","Envíos 24h","3 cuotas sin interés"]</code></p>
                    </td>
                </tr>

                <tr>
                    <th scope="row">Dónde mostrar</th>
                    <td>
                        <fieldset>
                            <label><input type="radio" name="twm_topbar_display_mode" value="all" <?php checked($display_mode,'all'); ?>> Todo el sitio</label><br>
                            <label><input type="radio" name="twm_topbar_display_mode" value="only" <?php checked($display_mode,'only'); ?>> Solo en estas páginas</label><br>
                            <label><input type="radio" name="twm_topbar_display_mode" value="exclude" <?php checked($display_mode,'exclude'); ?>> En todas menos estas</label>
                        </fieldset>
                        <p class="description">Combina con los campos de abajo para apuntar por ID o slug.</p>
                    </td>
                </tr>

                <tr>
                    <th scope="row"><label for="twm_topbar_pages_only">Solo en (IDs o slugs)</label></th>
                    <td>
                        <input type="text" id="twm_topbar_pages_only" name="twm_topbar_pages_only" value="<?php echo esc_attr($pages_only); ?>" style="width: 600px;">
                        <p class="description">Separado por comas. Ej: <code>12, 45, contacto, tienda</code></p>
                    </td>
                </tr>

                <tr>
                    <th scope="row"><label for="twm_topbar_pages_exclude">Excluir (IDs o slugs)</label></th>
                    <td>
                        <input type="text" id="twm_topbar_pages_exclude" name="twm_topbar_pages_exclude" value="<?php echo esc_attr($pages_exclude); ?>" style="width: 600px;">
                        <p class="description">Separado por comas. Ej: <code>home, carrito, 102</code></p>
                    </td>
                </tr>

                <tr>
                    <th scope="row">Modo de comportamiento</th>
                    <td>
                        <label><input type="radio" name="twm_topbar_mode" value="slides" <?php checked($mode,'slides'); ?>> Slides (cambia cada X ms)</label><br>
                        <label><input type="radio" name="twm_topbar_mode" value="marquee" <?php checked($mode,'marquee'); ?>> Marquee (desplazamiento infinito)</label>
                    </td>
                </tr>

                <tr>
                    <th scope="row"><label for="twm_topbar_interval_ms">Intervalo (ms) — Slides</label></th>
                    <td>
                        <input type="number" id="twm_topbar_interval_ms" name="twm_topbar_interval_ms" value="<?php echo esc_attr($interval_ms); ?>" min="200" step="100">
                        <p class="description">Default: <code>4000</code></p>
                    </td>
                </tr>

                <tr>
                    <th scope="row"><label for="twm_topbar_speed_px_s">Velocidad (px/s) — Marquee</label></th>
                    <td>
                        <input type="number" id="twm_topbar_speed_px_s" name="twm_topbar_speed_px_s" value="<?php echo esc_attr($speed_px_s); ?>" min="10" step="5">
                        <p class="description">Cuanto mayor el número, más rápido se desplaza.</p>
                    </td>
                </tr>

                <tr>
                    <th scope="row"><label for="twm_topbar_gap_px">Separación (px) — Marquee</label></th>
                    <td>
                        <input type="number" id="twm_topbar_gap_px" name="twm_topbar_gap_px" value="<?php echo esc_attr($gap_px); ?>" min="0" step="5">
                        <p class="description">Espacio entre carteles en la línea continua.</p>
                    </td>
                </tr>

                <tr>
                    <th scope="row"><label for="twm_topbar_custom_css">Custom CSS</label></th>
                    <td>
                        <textarea id="twm_topbar_custom_css" name="twm_topbar_custom_css" rows="8" cols="80" style="width: 600px;"><?php echo esc_textarea($custom_css); ?></textarea>
                        <p class="description">Ejemplo: <code>.twm-topbar{font-family:"Inter",sans-serif;font-size:15px;background:#111}</code></p>
                    </td>
                </tr>
            </table>

            <?php submit_button(); ?>
        </form>
						<div style="margin-top:30px; padding:15px; background:#f9f9f9; border-left:4px solid #0073aa;">
    <p style="margin:0; font-size:14px; line-height:1.4;">
        🔹 Conocé nuestra web: <a href="https://www.tuwebmaster.com.ar" target="_blank" style="color:#0073aa; text-decoration:none;">Tu Web Master</a> — Diseño, desarrollo y soporte técnico para tiendas online.<br>
        🔹 Si necesitás ayuda para adaptar este desarrollo, no dudes en <a href="https://www.tuwebmaster.com.ar" target="_blank" style="color:#0073aa; text-decoration:none;">contactarnos</a>.
    </p>
</div>

    </div>
    <?php
}

/** -----------------------------------
 *  Frontend: should we render the bar?
 *  ----------------------------------- */
if ( ! function_exists('twm_str_ends_with') ) {
    function twm_str_ends_with($haystack, $needle) {
        $len = strlen($needle);
        if ($len === 0) return true;
        return (substr($haystack, -$len) === $needle);
    }
}

function twm_topbar_should_display() {
    $mode    = get_option('twm_topbar_display_mode', 'all');
    $only    = array_filter(array_map('trim', explode(',', (string)get_option('twm_topbar_pages_only',''))));
    $exclude = array_filter(array_map('trim', explode(',', (string)get_option('twm_topbar_pages_exclude',''))));

    // Build a set of tokens describing the current view
    $current_tokens = [];

    // 1) Singular (page/post/product)
    if (is_singular()) {
        $post_id = get_queried_object_id();
        $post    = get_post($post_id);
        if ($post) {
            $current_tokens[] = (string) $post_id;             // by ID
            if (!empty($post->post_name)) {
                $current_tokens[] = strtolower($post->post_name); // by slug
            }
        }
    }

    // 2) WooCommerce special pages (shop, cart, checkout, account)
    if (function_exists('is_shop') && is_shop()) {
        // shop page can be a special archive backed by a page
        $shop_id = (int) get_option('woocommerce_shop_page_id');
        if ($shop_id) {
            $current_tokens[] = (string) $shop_id;
            $shop = get_post($shop_id);
            if ($shop && !empty($shop->post_name)) {
                $current_tokens[] = strtolower($shop->post_name); // ej: "tienda"
            }
        }
        $current_tokens[] = 'shop';
    }
    if (function_exists('is_cart') && is_cart()) {
        $current_tokens[] = 'cart';
        $cart_id = (int) get_option('woocommerce_cart_page_id');
        if ($cart_id) $current_tokens[] = (string)$cart_id;
    }
    if (function_exists('is_checkout') && is_checkout()) {
        $current_tokens[] = 'checkout';
        $chk_id = (int) get_option('woocommerce_checkout_page_id');
        if ($chk_id) $current_tokens[] = (string)$chk_id;
    }
    if (function_exists('is_account_page') && is_account_page()) {
        $current_tokens[] = 'my-account';
        $acc_id = (int) get_option('woocommerce_myaccount_page_id');
        if ($acc_id) $current_tokens[] = (string)$acc_id;
    }

    // 3) Product category / tag (taxonomy archives)
    if (function_exists('is_product_category') && is_product_category()) {
        $term = get_queried_object();
        if ($term && !empty($term->slug)) $current_tokens[] = strtolower($term->slug);
        $current_tokens[] = 'product-category';
    }
    if (function_exists('is_product_tag') && is_product_tag()) {
        $term = get_queried_object();
        if ($term && !empty($term->slug)) $current_tokens[] = strtolower($term->slug);
        $current_tokens[] = 'product-tag';
    }

    // 4) Fallback por path (soporta subdirectorios / idiomas)
    $path = '';
    $url  = home_url(add_query_arg([]));
    $p    = wp_parse_url($url);
    if (!empty($p['path'])) {
        $path = trim($p['path'], '/');
        if ($path !== '') $current_tokens[] = strtolower($path);          // full path
        // también el último segmento (slug final)
        $segments = array_filter(explode('/', $path));
        if (!empty($segments)) {
            $current_tokens[] = strtolower(end($segments));
        }
    }

    // Normaliza tokens del usuario
    $normalize = function($arr){
        return array_values(array_unique(array_filter(array_map(function($t){
            $t = strtolower(trim($t, " \t\n\r\0\x0B/"));
            return $t;
        }, $arr))));
    };

    $only    = $normalize($only);
    $exclude = $normalize($exclude);
    $current = $normalize($current_tokens);

    // Comparador: match exacto por ID/slug, o path que termine igual
    $matches = function($user_tokens) use ($current, $path) {
        foreach ($user_tokens as $t) {
            if (in_array($t, $current, true)) return true;
            if ($path !== '' && twm_str_ends_with($path, $t)) return true;
        }
        return false;
    };

    if ($mode === 'all')     return true;
    if ($mode === 'only')    return $matches($only);
    if ($mode === 'exclude') return ! $matches($exclude);
    return true;
}


/** -------------------------------
 *  Frontend: render bar and logic
 *  ------------------------------- */
add_action('wp_footer', 'twm_topbar_render');
function twm_topbar_render() {
    if (is_admin()) return;
    if ( ! twm_topbar_should_display() ) return;

    $raw = (string) get_option('twm_topbar_slides', '');
    $lines = array_filter(preg_split('/\r\n|\r|\n/', $raw), fn($s)=> trim($s) !== '');
    $slides = array_map(fn($s)=> trim($s), $lines);
    if (empty($slides)) return;

    $mode        = get_option('twm_topbar_mode', 'slides');
    $interval    = intval(get_option('twm_topbar_interval_ms', 4000));
    if ($interval < 200) $interval = 4000;

    $speed_px_s  = max(10, intval(get_option('twm_topbar_speed_px_s', 80)));
    $gap_px      = max(0,  intval(get_option('twm_topbar_gap_px', 40)));

    $custom_css  = (string) get_option('twm_topbar_custom_css', '');

    ?>
    <style>
    .twm-topbar {
        width: 100%;
        height: 34px;
        position: fixed;
        top: 0; left: 0;
        z-index: 999999;
        background: #000;
        color: #fff;
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        padding: 0 10px;
        box-sizing: border-box;
        line-height: 1.2;
        font-size: 14px;
        text-align: center;
        transform: translateZ(0);
    }

    /* Slides mode */
    .twm-topbar .twm-slide { display: none; white-space: nowrap; }
    .twm-topbar .twm-slide.is-active { display: block; }

    /* Marquee mode */
    .twm-topbar .twm-viewport { width: 100%; overflow: hidden; }
    .twm-topbar .twm-track {
        display: inline-flex;
        align-items: center;
        gap: var(--twm-gap, 40px);
        will-change: transform;
        animation: twm-marquee var(--twm-duration, 20s) linear infinite;
        transform: translateX(0);
    }
    .twm-topbar .twm-item { white-space: nowrap; flex: 0 0 auto; }

    @keyframes twm-marquee {
        from { transform: translateX(0); }
        to   { transform: translateX(var(--twm-distance, -100%)); }
    }

    /* Custom CSS injected by user */
    <?php echo $custom_css . "\n"; ?>
    </style>

    <?php if ($mode === 'marquee'): ?>
        <div class="twm-topbar twm-mode-marquee" role="region" aria-label="Site announcements">
            <div class="twm-viewport">
                <div class="twm-track">
                    <?php foreach ($slides as $txt): ?>
                        <div class="twm-item"><?php echo esc_html($txt); ?></div>
                    <?php endforeach; ?>
                </div>
            </div>
        </div>
        <script>
        (function(){
            var bar    = document.querySelector('.twm-topbar.twm-mode-marquee');
            if (!bar) return;
            var track  = bar.querySelector('.twm-track');
            var vp     = bar.querySelector('.twm-viewport');
            var speed  = <?php echo (int)$speed_px_s; ?>; // px per second
            var gap    = <?php echo (int)$gap_px; ?>;

            // Set gap CSS var
            bar.style.setProperty('--twm-gap', gap + 'px');

            // Ensure we have enough content to scroll seamlessly:
            // Duplicate content until track width >= 2x viewport width.
            function duplicateUntil() {
                var safety = 0, maxSafety = 12;
                var original = Array.from(track.children).map(function(n){ return n.cloneNode(true); });
                while (track.scrollWidth < vp.clientWidth * 2 && safety < maxSafety) {
                    original.forEach(function(node){ track.appendChild(node.cloneNode(true)); });
                    safety++;
                }
            }
            duplicateUntil();

            // Distance = half of the track (so the repetition matches)
            var distance = -Math.floor(track.scrollWidth / 2);
            var duration = Math.abs(distance) / speed; // seconds

            bar.style.setProperty('--twm-distance', distance + 'px');
            bar.style.setProperty('--twm-duration', duration + 's');

            // Recompute on resize
            var ro;
            function recompute(){
                // Reset
                bar.style.removeProperty('--twm-distance');
                bar.style.removeProperty('--twm-duration');
                // Remove duplicates, keep first block only
                var items = track.querySelectorAll('.twm-item');
                var half  = Math.ceil(items.length / 2);
                // Try to reduce to the first half if we previously duplicated
                while (items.length > 0 && items.length > half) {
                    track.removeChild(track.lastElementChild);
                    items = track.querySelectorAll('.twm-item');
                }
                duplicateUntil();
                distance = -Math.floor(track.scrollWidth / 2);
                duration = Math.abs(distance) / speed;
                bar.style.setProperty('--twm-distance', distance + 'px');
                bar.style.setProperty('--twm-duration', duration + 's');
            }
            window.addEventListener('resize', function(){ clearTimeout(ro); ro = setTimeout(recompute, 150); });
        })();
        </script>
    <?php else: ?>
        <div class="twm-topbar twm-mode-slides" role="region" aria-label="Site announcements">
            <?php foreach ($slides as $i => $txt): ?>
                <div class="twm-slide<?php echo $i===0 ? ' is-active' : ''; ?>"><?php echo esc_html($txt); ?></div>
            <?php endforeach; ?>
        </div>
        <script>
        (function(){
            var slides = document.querySelectorAll('.twm-topbar.twm-mode-slides .twm-slide');
            if (!slides.length) return;
            var current = 0;
            function show(i){
                for (var k=0;k<slides.length;k++){ slides[k].classList.remove('is-active'); }
                slides[i].classList.add('is-active');
            }
            show(0);
            setInterval(function(){
                current = (current + 1) % slides.length;
                show(current);
            }, <?php echo (int)$interval; ?>);
        })();
        </script>
    <?php endif; ?>
    <?php
}

				
			

¿Por qué compartirlo?

No es un producto comercial. Es una solución real a un problema común en sitios WordPress: cómo mostrar mensajes importantes sin depender de plugins pesados ni soluciones complejas.

Usar esta barra de anuncios animada puede ayudarte a:

  • Mejorar la visibilidad de tus promociones

  • Aumentar la interacción con tus visitantes

  • Hacer tu sitio más dinámico y profesional

 GitHub / Descarga: TopBar Slider para WordPress

Podés usarlo tal cual o adaptarlo a tu proyecto. Comentarios, sugerencias o mejoras son bienvenidos: la idea es que sea útil para toda la comunidad WordPress.

Si te sirve, genial. Si lo adaptás, mejor. Y si tenés dudas, sugerencias o querés compartir tu experiencia, los comentarios están abiertos. Me interesa que esto sea útil para otros técnicos que trabajen con Odoo y pasarelas de pago.

Nota del autor

Soy Sasha Herscovich, y me dedico al diseño, desarrollo y soporte técnico de tiendas online en WordPress y Odoo. Trabajo todos los días resolviendo problemas reales, y cada tanto me gusta compartir lo que me toca enfrentar, por si a alguien más le sirve.

En este blog vas a encontrar ideas, soluciones y fragmentos de código que uso en proyectos reales. Algunas cosas están pensadas para quienes quieren autogestionar su sitio, otras para desarrolladores o diseñadores que buscan mejorar lo que hacen, ahorrar tiempo o simplemente entender cómo resolver algo puntual.

No vendo fórmulas mágicas ni soluciones universales. Solo comparto lo que me funciona, lo que pruebo y lo que aprendo en el camino. Si te sirve, bienvenido. Y si querés comentar, mejorar o adaptar algo, me encantaría que lo hagas.

Añadir comentario

Su dirección de correo electrónico no será publicada. Los campos obligatorios están marcados