Marquee announcement banner flashes after having run through once

This is my small code for a „infinite scroll looped announcement bar“:

{%- if template.name != ‘product’ -%}

.custom-scrolling-banner { background-color: {{ section.settings.bg_color }}; color: {{ section.settings.text_color }}; overflow: hidden; position: absolute; top: 0; left: 0; width: 100%; height: 40px; display: flex; align-items: center; z-index: 1001; box-sizing: border-box; } .custom-scrolling-banner__viewport { width: 100%; overflow: hidden; display: flex; } .custom-scrolling-banner__content { display: flex; flex-wrap: nowrap; animation: marquee {{ section.settings.speed }}s linear infinite; will-change: transform; /* This ensures the content doesn't shrink */ min-width: fit-content; } .custom-scrolling-banner__track { display: flex; align-items: center; flex-shrink: 0; } .custom-scrolling-banner__item-wrapper { display: flex; align-items: center; white-space: nowrap; } .custom-scrolling-banner__item { font-family: 'GeistMono-Light', 'Gramatika', Arial, Helvetica, sans-serif !important; font-weight: 300 !important; letter-spacing: .02em !important; font-size: 13px; padding: 0 4.5rem; white-space: nowrap; } .custom-scrolling-banner__image { height: 25.2px; width: auto; object-fit: contain; display: block; flex-shrink: 0; } @keyframes marquee { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } }

{% assign icon_url = “https://cdn.shopify.com/s/files/1/0905/3110/3066/files/IMG_5962.png?v=1783437862” %}

{% for block in section.blocks %}
{{ block.settings.text }}
{% endfor %}
{% for block in section.blocks %}
{{ block.settings.text }}
{% endfor %}

{%- endif -%}

{% schema %}
{
“name”: “Scrolling Announcement”,
“settings”: [
{ “type”: “range”, “id”: “speed”, “min”: 20, “max”: 200, “step”: 5, “unit”: “s”, “label”: “Scrolling Speed”, “default”: 60 },
{ “type”: “color”, “id”: “bg_color”, “label”: “Background Color”, “default”: “#000000” },
{ “type”: “color”, “id”: “text_color”, “label”: “Text Color”, “default”: “#ffffff” }
],
“blocks”: [
{ “type”: “announcement”, “name”: “Announcement”, “settings”: [{ “type”: “text”, “id”: “text”, “label”: “Text”, “default”: “Welcome” }] }
],
“presets”: [{ “name”: “Scrolling Announcement” }]
}
{% endschema %}

Problem is that after having run once (everytime) through all the announcements, there is a black flash before re-starting the loop. And i cannot fix that flash. Can someone help me remove that flash everytime?

The problem is when it hits the end, the browser doesn’t always reload to the exact point seamlessly…

You could try using translate3d instead of translateX, this could force the GPU.

Something like @keyframes marquee { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(-50%, 0, 0); } }

Could work.

I found another idea, where you basically load 2 versions of the text, and as one is coming to an end, you reload the other, so there’s no “reset”, just a forever adding the next element. See here: https://medium.com/@archielister/make-an-infinitely-scrolling-banner-with-html-and-css-e7918506c496

Thank you for your quick reply. Already tried that and it didn‘t work :confused:

I will look into your linked article. But that could be hard to implement because of different screen sizes (desktop/mobile), right?

You might have to implement some kind of logic to get the length of the string/strings, and base it off that…

Another option is to loop the strings as cards on an unordered list… again the length would be the difficult part.

A “simpler” workaround, which isn’t actually a workaround at all, is that you duplicate the string in the announcement bar itself a few times, so that the user isn’t paying attention after like 5-10 times and then it doesn’t matter if it flashes.

Yeah, i think that is going to be the easiest solution. I am just stunned how brands like: https://www.ravemoreberlin.com have the banner scroll endlessly without any flashes. I have been looking at it for about a minute and no flashes at all :thinking:.

Didn‘t quite get the hang of how they did it though…

Tried my best to solve it.

Here is the video recording which is what i got as a full result. https://drive.google.com/file/d/1NNHF6xXGmlr8lUFB71Tdhnnc9YPMkNZV/view?usp=sharing

I think near the loop end the content runs out before the right edge and you see a slab of the bar’s black background with no text. That sweeping empty black, i think, is what you are saying to be the flash.

This code fixed it:
file sections/custom-scrolling-banner.liquid

{%- if template.name != 'product' -%}
<style>
.custom-scrolling-banner {
  background-color: {{ section.settings.bg_color }};
  color: {{ section.settings.text_color }};
  overflow: hidden;
  position: relative;
  width: 100%;
  height: 40px;
  display: flex;
  align-items: center;
  z-index: 1001;
  box-sizing: border-box;
}
.custom-scrolling-banner__viewport {
  width: 100%;
  overflow: hidden;
  display: flex;
}
.custom-scrolling-banner__content {
  display: flex;
  flex-wrap: nowrap;
  width: max-content;
  will-change: transform;
  animation-name: custom-marquee;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.custom-scrolling-banner:hover .custom-scrolling-banner__content { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .custom-scrolling-banner__content { animation: none; transform: none; }
}
.custom-scrolling-banner__copy {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.custom-scrolling-banner__item {
  font-weight: 300 !important;
  letter-spacing: .02em !important;
  font-size: 13px;
  padding: 0 4.5rem;
  white-space: nowrap;
}
@keyframes custom-marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(var(--marquee-shift, -50%)); }
}
</style>

<div class="custom-scrolling-banner" data-speed="{{ section.settings.speed }}">
  <div class="custom-scrolling-banner__viewport">
    <div class="custom-scrolling-banner__content">
      <div class="custom-scrolling-banner__copy" data-marquee-copy>
        {% for block in section.blocks %}
          <span class="custom-scrolling-banner__item">{{ block.settings.text }}</span>
        {% endfor %}
      </div>
    </div>
  </div>
</div>

<script>
(function () {
  function initMarquee(root) {
    var viewport = root.querySelector('.custom-scrolling-banner__viewport');
    var content  = root.querySelector('.custom-scrolling-banner__content');
    var firstCopy = root.querySelector('[data-marquee-copy]');
    if (!viewport || !content || !firstCopy) return;

    var baseSpeed = parseFloat(root.getAttribute('data-speed')) || 60;
    var originalHTML = firstCopy.outerHTML;

    function layout() {
      content.innerHTML = originalHTML; // reset to a single copy before measuring
      var copyWidth = content.firstElementChild.getBoundingClientRect().width;
      if (copyWidth === 0) return;
      var viewportWidth = viewport.getBoundingClientRect().width;

      // Clone whole copies until the track is at least (viewport + one copy) wide,
      // so a full copy is always sliding in before the loop resets.
      var needed = viewportWidth + copyWidth;
      var copies = 1;
      while (copies * copyWidth < needed) {
        content.insertAdjacentHTML('beforeend', originalHTML);
        copies++;
      }

      // Slide by EXACTLY one copy width -> restart is pixel-identical -> no flash.
      content.style.setProperty('--marquee-shift', '-' + copyWidth + 'px');
      // Keep perceived speed constant regardless of how many copies we cloned.
      content.style.animationDuration = baseSpeed + 's';
    }

    layout();
    if (window.ResizeObserver) {
      new ResizeObserver(layout).observe(viewport);
    } else {
      window.addEventListener('resize', layout);
    }
  }

  document.querySelectorAll('.custom-scrolling-banner').forEach(initMarquee);
})();
</script>
{%- endif -%}

{% schema %}
{
  "name": "Scrolling Announcement",
  "settings": [
    { "type": "range", "id": "speed", "min": 20, "max": 200, "step": 5, "unit": "s", "label": "Scrolling Speed", "default": 60 },
    { "type": "color", "id": "bg_color", "label": "Background Color", "default": "#000000" },
    { "type": "color", "id": "text_color", "label": "Text Color", "default": "#ffffff" }
  ],
  "blocks": [
    { "type": "announcement", "name": "Announcement", "settings": [{ "type": "text", "id": "text", "label": "Text", "default": "Welcome" }] }
  ],
  "presets": [{ "name": "Scrolling Announcement" }]
}
{% endschema %}

If it works, please like and mark it as Solution.
Best,
Ajay

The site does it the same way – they simply output 10 copies of the original set of announcements so that it covers entire screen width for sure.

But instead of applying animation on entire “slide-holder” element, they animate each set of slides to shift each of them 100% to the left (since they are all clones, it looks smooth).

And you can do this duplication in Liquid, or do it in JS.


Basically you want to shift your initial marquee elements 100% to the left. To imitate the “rotation”, you output a second set of elements.

Width of initial elements becomes 50% of the “slide-holder” and if you apply animation at the “slide-holder” level, then you need to shift entire holder 50% to the left.

However, you also need to make sure that even when your initial elements are shifted fully beyond the left edge, there are still enough content to cover entire width of the wrapper, so you may need to output another set of clones and size of initial set of elements will become 33.33% of entire “holder”.

Plus, to ensure smooth animation, you need to make sure that at the end of your animation the right edge of your last initial element is exactly at the left edge of the wrapper – so check for margins or gaps which may shift your slides.

I’ve just updated my old pen to better illustrate the concepts – https://codepen.io/tairli/pen/raaYxqm

The code uses JS to clone original items.

It also differs slightly because it relies on logo sizes being the same and multiplies it by the number of initial slides to define transform size in pixels, which is not best for textual content, but anyway.

Hi @Paul222

Please use the code below instead of the version currently running on your store.

To apply the update, copy the new code and go to: Online Store > Themes > Edit code

Locate the file you originally sent, select the old code, and replace it with this updated version.

{%- if template.name != 'product' -%}

<style>
  .custom-scrolling-banner {
    background-color: {{ section.settings.bg_color }};
    color: {{ section.settings.text_color }};
    overflow: hidden;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 40px;
    display: flex;
    align-items: center;
    z-index: 1001;
    box-sizing: border-box;
  }

  .custom-scrolling-banner__viewport {
    width: 100%;
    overflow: hidden;
    display: flex;
  }

  .custom-scrolling-banner__content {
    display: flex;
    flex-wrap: nowrap;
    width: max-content;
    animation: marquee {{ section.settings.speed }}s linear infinite;
    will-change: transform;
  }

  .custom-scrolling-banner__track {
    display: flex;
    align-items: center;
    flex-shrink: 0;
  }

  .custom-scrolling-banner__item-wrapper {
    display: flex;
    align-items: center;
    white-space: nowrap;
  }

  .custom-scrolling-banner__item {
    font-family: 'GeistMono-Light', 'Gramatika', Arial, Helvetica, sans-serif !important;
    font-weight: 300 !important;
    letter-spacing: .02em !important;
    font-size: 13px;
    padding: 0 4.5rem;
    white-space: nowrap;
  }

  .custom-scrolling-banner__image {
    height: 25.2px;
    width: auto;
    object-fit: contain;
    display: block;
    flex-shrink: 0;
  }

  @keyframes marquee {
    0% {
      transform: translateX(0);
    }

    100% {
      transform: translateX(-10%);
    }
  }
</style>

{% assign icon_url = "https://cdn.shopify.com/s/files/1/0905/3110/3066/files/IMG_5962.png?v=1783437862" %}

<div class="custom-scrolling-banner">
  <div class="custom-scrolling-banner__viewport">
    <div class="custom-scrolling-banner__content">

      {% for i in (1..10) %}
        <div class="custom-scrolling-banner__track">
          {% for block in section.blocks %}
            <div class="custom-scrolling-banner__item-wrapper" {{ block.shopify_attributes }}>
              <span class="custom-scrolling-banner__item">
                {{ block.settings.text }}
              </span>

              {% if icon_url != blank %}
                <img
                  src="{{ icon_url }}"
                  alt=""
                  class="custom-scrolling-banner__image"
                  loading="lazy"
                >
              {% endif %}
            </div>
          {% endfor %}
        </div>
      {% endfor %}

    </div>
  </div>
</div>

{%- endif -%}

{% schema %}
{
  "name": "Scrolling Announcement",
  "settings": [
    {
      "type": "range",
      "id": "speed",
      "min": 20,
      "max": 200,
      "step": 5,
      "unit": "s",
      "label": "Scrolling Speed",
      "default": 60
    },
    {
      "type": "color",
      "id": "bg_color",
      "label": "Background Color",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text Color",
      "default": "#ffffff"
    }
  ],
  "blocks": [
    {
      "type": "announcement",
      "name": "Announcement",
      "settings": [
        {
          "type": "text",
          "id": "text",
          "label": "Text",
          "default": "Welcome"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Scrolling Announcement"
    }
  ]
}
{% endschema %}

In this new version, I changed this specific part of your code:

{% for block in section.blocks %}
...
{% endfor %}

{% for block in section.blocks %}
...
{% endfor %}

To this,

{% for i in (1..10) %}
  <div class="custom-scrolling-banner__track">
    {% for block in section.blocks %}
      ...
    {% endfor %}
  </div>
{% endfor %}

And I also updated this CSS portion:

@keyframes marquee {

    0% {

      transform: translateX(0);

    }

    100% {

      transform: translateX(-50%);

    }

  }

To this:

@keyframes marquee {

    0% {

      transform: translateX(0);

    }

    100% {

      transform: translateX(-10%);

    }

  }

The original code only duplicated the announcement blocks twice and animated the full content to -50%. That approach only works if both halves are wide enough to cover the screen perfectly.

My modification loops the announcement content 10 times, ensuring there is always more text or imagery waiting on the right side before the animation resets. Since the content is now repeated 10 times, each set makes up exactly 10% of the total marquee width. Shifting the content by -10% moves it exactly one full set forward before instantly resetting to an identical position. This completely eliminates the blank flash and jerky “jump” you were seeing.

May be try this. It will work as needed.

{%- if template.name != 'product' -%}
<style>
  .custom-scrolling-banner {
    background-color: {{ section.settings.bg_color }};
    color: {{ section.settings.text_color }};
    overflow: hidden;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 40px;
    display: flex;
    align-items: center;
    z-index: 1001;
    box-sizing: border-box;
  }
  .custom-scrolling-banner__viewport {
    width: 100%;
    overflow: hidden;
    display: flex;
  }
  .custom-scrolling-banner__content {
    display: flex;
    flex-wrap: nowrap;
    animation: marquee {{ section.settings.speed }}s linear infinite;
    will-change: transform;
    min-width: fit-content;
  }
  .custom-scrolling-banner__track {
    display: flex;
    align-items: center;
    flex-shrink: 0;
  }
  .custom-scrolling-banner__item-wrapper {
    display: flex;
    align-items: center;
    white-space: nowrap;
  }
  .custom-scrolling-banner__item {
    font-family: 'GeistMono-Light', 'Gramatika', Arial, Helvetica, sans-serif !important;
    font-weight: 300 !important;
    letter-spacing: .02em !important;
    font-size: 13px;
    padding: 0 4.5rem;
    white-space: nowrap;
  }
  .custom-scrolling-banner__image {
    height: 25.2px;
    width: auto;
    object-fit: contain;
    display: block;
    flex-shrink: 0;
  }
  @keyframes marquee {
    0%   { transform: translateX(0); }
    100% { transform: translateX(-50%); }
  }
</style>

{% assign icon_url = "https://cdn.shopify.com/s/files/1/0905/3110/3066/files/IMG_5962.png?v=1783437862" %}

<div class="custom-scrolling-banner">
  <div class="custom-scrolling-banner__viewport">
    <div class="custom-scrolling-banner__content">
      {% comment %} Track 1 {% endcomment %}
      <div class="custom-scrolling-banner__track">
        {% for block in section.blocks %}
          <div class="custom-scrolling-banner__item-wrapper" {{ block.shopify_attributes }}>
            <span class="custom-scrolling-banner__item">{{ block.settings.text }}</span>
            <img class="custom-scrolling-banner__image" src="{{ icon_url }}" alt="" loading="lazy" width="26" height="26">
          </div>
        {% endfor %}
      </div>
      {% comment %} Track 2 — duplicate for seamless -50% loop {% endcomment %}
      <div class="custom-scrolling-banner__track" aria-hidden="true">
        {% for block in section.blocks %}
          <div class="custom-scrolling-banner__item-wrapper">
            <span class="custom-scrolling-banner__item">{{ block.settings.text }}</span>
            <img class="custom-scrolling-banner__image" src="{{ icon_url }}" alt="" loading="lazy" width="26" height="26">
          </div>
        {% endfor %}
      </div>
    </div>
  </div>
</div>
{%- endif -%}

{% schema %}
{
  "name": "Scrolling Announcement",
  "settings": [
    {
      "type": "range",
      "id": "speed",
      "min": 20,
      "max": 200,
      "step": 5,
      "unit": "s",
      "label": "Scrolling Speed",
      "default": 60
    },
    {
      "type": "color",
      "id": "bg_color",
      "label": "Background Color",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text Color",
      "default": "#ffffff"
    }
  ],
  "blocks": [
    {
      "type": "announcement",
      "name": "Announcement",
      "settings": [
        {
          "type": "text",
          "id": "text",
          "label": "Text",
          "default": "Welcome"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Scrolling Announcement"
    }
  ]
}
{% endschema %}

If your doing this with AI it can be simpler to just have it bounce back and forth beginning to end.

They solve it by delegating to a professional so the owner is wasting time on such stuff.
Though the real solve is to not use marquee’s or slideshows at all as they are MASSIVE user annoyances and just encourage adding inperformant behaviors to your theme.
See tims blurb about the 10 copies to fake the effect that should be a big fat clue something is rotten from the jump.
marquee’s & slideshows just cause banner blindness unless you specifically burn the cash on a/b testing to prove it isn’t wasting everyones time in trying to cram more into less space. “Well website X does it” isn’t proof it’s copying blindly and praying and that’s just bad business.

Make an actual business choice and prioritize ONE thing that actually matters your site and business will be better for it.