← All sections

Announcement Ticker

A seamless marquee that keeps your offer moving on every page: slim bar or bold band.

FreeUrgency & promo21 settingsInstall
Full screen ↗

Install

One file: sections/mf-announcement-ticker.liquid. It appears in the theme editor as EX · Announcement ticker.

  1. Connect EXPERAISE Sections to Claude (once). In Claude Code:
    terminal
    claude mcp add --transport http experaise-sections https://sections.experaise.com/api/mcp
    In the Claude app: Settings → Connectors → Add custom connector → https://sections.experaise.com/api/mcp. Full setup
  2. Ask Claude. It fetches sections/mf-announcement-ticker.liquid, pushes only that file to your theme with Shopify CLI, and tells you where to find it.
    prompt
    Install the EXPERAISE Sections section "Announcement Ticker" (announcement-ticker) on my Shopify store your-store.myshopify.com. Put it on an unpublished copy of my live theme, then tell me how to add it in the theme editor.
  3. Customize. Online Store → Themes → Customize → Add section → EX · Announcement ticker. Every text, color and product is a setting.
sections/mf-announcement-ticker.liquidDownload
{%- liquid
  assign s = section.settings
  assign count = section.blocks.size

  case s.separator
    when 'dot'
      assign glyph = '•'
    when 'slash'
      assign glyph = '/'
    when 'dash'
      assign glyph = '–'
    else
      assign glyph = '✦'
  endcase

  comment
    Copies of the message group per half of the track. Two identical halves
    translate by -50% for a seamless loop; JavaScript adds copies on very wide
    screens. Odd message counts use an even number of copies so the
    filled / outlined rhythm of the Statement style stays unbroken.
  endcomment
  assign copies = 2
  if s.style == 'statement'
    if count == 1
      assign copies = 4
    endif
    assign pxps = s.speed | times: 22
    assign item_estimate = s.statement_size | times: 9
  else
    if count == 1
      assign copies = 8
    elsif count < 4
      assign copies = 4
    endif
    assign pxps = s.speed | times: 12
    assign item_estimate = 300
  endif
  assign is_odd = count | modulo: 2
  assign duration = count | times: item_estimate | times: copies | divided_by: pxps | at_least: 8
-%}

{%- capture mf_items_visible -%}
  {%- for block in section.blocks -%}
    {%- liquid
      assign b = block.settings
    -%}
    <li class="mf-ticker__item{% if b.highlight %} mf-ticker__item--hl{% endif %}" {{ block.shopify_attributes }}>
      {%- if b.link != blank -%}<a class="mf-ticker__msg mf-ticker__msg--link" href="{{ b.link }}">{%- else -%}<span class="mf-ticker__msg">{%- endif -%}
        {%- case b.icon -%}
          {%- when 'sparkle' -%}
            <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M9 1.8c.55 4.1 2.2 5.7 6.2 6.2-4 .55-5.65 2.2-6.2 6.2C8.45 10.2 6.8 8.55 2.8 8 6.8 7.45 8.45 5.9 9 1.8Z" fill="currentColor"/><path d="M15.3 12.2c.25 1.9 1 2.6 2.9 2.9-1.9.25-2.65 1-2.9 2.9-.25-1.9-1-2.65-2.9-2.9 1.9-.3 2.65-1 2.9-2.9Z" fill="currentColor"/></svg>
          {%- when 'truck' -%}
            <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M1.8 5.5h10v8.2h-10zM11.8 8.2h3.6l2.8 2.9v2.6h-6.4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="5.3" cy="15.2" r="1.7" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="14.7" cy="15.2" r="1.7" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
          {%- when 'gift' -%}
            <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M2.8 7.8h14.4v3.4H2.8zM4.2 11.2v6h11.6v-6M10 7.8v9.4M10 7.8C8.6 4.2 5.2 4 5.6 6.2c.25 1.3 2.6 1.6 4.4 1.6Zm0 0c1.4-3.6 4.8-3.8 4.4-1.6-.25 1.3-2.6 1.6-4.4 1.6Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
          {%- when 'star' -%}
            <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="m10 2.2 2.3 4.8 5.2.7-3.8 3.6.95 5.2L10 14l-4.65 2.5.95-5.2-3.8-3.6 5.2-.7Z" fill="currentColor"/></svg>
          {%- when 'leaf' -%}
            <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M3.6 16.4C3.2 8.6 7.8 3.8 17 3.4c-.2 9-5.2 13.4-13.4 13ZM3.6 16.4c2.6-3.8 5.6-6.4 9.2-8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
        {%- endcase -%}
        <span class="mf-ticker__text">{{ b.text | escape }}</span>
      {%- if b.link != blank -%}</a>{%- else -%}</span>{%- endif -%}
      <span class="mf-ticker__sep" aria-hidden="true"><span>{{ glyph }}</span></span>
    </li>
  {%- endfor -%}
{%- endcapture -%}

{%- capture mf_group_hidden -%}
  <ul class="mf-ticker__group" aria-hidden="true">
    {%- for block in section.blocks -%}
      {%- assign b = block.settings -%}
      <li class="mf-ticker__item{% if b.highlight %} mf-ticker__item--hl{% endif %}">
        {%- if b.link != blank -%}<a class="mf-ticker__msg mf-ticker__msg--link" href="{{ b.link }}" tabindex="-1">{%- else -%}<span class="mf-ticker__msg">{%- endif -%}
          {%- case b.icon -%}
            {%- when 'sparkle' -%}
              <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M9 1.8c.55 4.1 2.2 5.7 6.2 6.2-4 .55-5.65 2.2-6.2 6.2C8.45 10.2 6.8 8.55 2.8 8 6.8 7.45 8.45 5.9 9 1.8Z" fill="currentColor"/><path d="M15.3 12.2c.25 1.9 1 2.6 2.9 2.9-1.9.25-2.65 1-2.9 2.9-.25-1.9-1-2.65-2.9-2.9 1.9-.3 2.65-1 2.9-2.9Z" fill="currentColor"/></svg>
            {%- when 'truck' -%}
              <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M1.8 5.5h10v8.2h-10zM11.8 8.2h3.6l2.8 2.9v2.6h-6.4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="5.3" cy="15.2" r="1.7" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="14.7" cy="15.2" r="1.7" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
            {%- when 'gift' -%}
              <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M2.8 7.8h14.4v3.4H2.8zM4.2 11.2v6h11.6v-6M10 7.8v9.4M10 7.8C8.6 4.2 5.2 4 5.6 6.2c.25 1.3 2.6 1.6 4.4 1.6Zm0 0c1.4-3.6 4.8-3.8 4.4-1.6-.25 1.3-2.6 1.6-4.4 1.6Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
            {%- when 'star' -%}
              <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="m10 2.2 2.3 4.8 5.2.7-3.8 3.6.95 5.2L10 14l-4.65 2.5.95-5.2-3.8-3.6 5.2-.7Z" fill="currentColor"/></svg>
            {%- when 'leaf' -%}
              <svg class="mf-ticker__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M3.6 16.4C3.2 8.6 7.8 3.8 17 3.4c-.2 9-5.2 13.4-13.4 13ZM3.6 16.4c2.6-3.8 5.6-6.4 9.2-8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
          {%- endcase -%}
          <span class="mf-ticker__text">{{ b.text | escape }}</span>
        {%- if b.link != blank -%}</a>{%- else -%}</span>{%- endif -%}
        <span class="mf-ticker__sep"><span>{{ glyph }}</span></span>
      </li>
    {%- endfor -%}
  </ul>
{%- endcapture -%}

<mf-announcement-ticker
  class="mf-ticker mf-ticker--{{ s.style }} mf-ticker--{{ s.direction }}{% if s.pause_on_hover %} mf-ticker--hover-pause{% endif %}{% if s.show_border %} mf-ticker--border{% endif %}{% if is_odd == 1 %} mf-ticker--odd{% endif %}{% if count == 1 %} mf-ticker--single{% endif %}{% if s.show_controls %} mf-ticker--controls{% endif %}"
  role="region"
  aria-label="{{ s.region_label | escape }}"
  data-speed="{{ pxps }}"
  data-pause-label="{{ s.pause_label | escape }}"
  data-play-label="{{ s.play_label | escape }}"
  style="--mf-ticker-bg: {{ s.color_bg }}; --mf-ticker-fg: {{ s.color_text }}; --mf-ticker-accent: {{ s.color_accent }}; --mf-ticker-line: {{ s.color_text | color_modify: 'alpha', 0.16 }}; --mf-ticker-line-strong: {{ s.color_text | color_modify: 'alpha', 0.3 }}; --mf-ticker-soft: {{ s.color_text | color_modify: 'alpha', 0.08 }}; --mf-ticker-size: {{ s.statement_size }}px; --mf-ticker-h: {{ s.bar_height }}px; --mf-ticker-duration: {{ duration }}s;"
>
  {%- if count > 0 -%}
    <div class="mf-ticker__viewport" data-mf-viewport>
      <div class="mf-ticker__track" data-mf-track>
        <div class="mf-ticker__half" data-mf-half>
          <ul class="mf-ticker__group" role="list">{{ mf_items_visible }}</ul>
          {%- for i in (2..copies) -%}{{ mf_group_hidden }}{%- endfor -%}
        </div>
        <div class="mf-ticker__half" data-mf-half aria-hidden="true">
          {%- for i in (1..copies) -%}{{ mf_group_hidden }}{%- endfor -%}
        </div>
      </div>
    </div>

    {%- if s.show_controls -%}
      <div class="mf-ticker__controls">
        {%- if count > 1 -%}
          <button type="button" class="mf-ticker__btn mf-ticker__btn--prev" data-mf-prev aria-label="{{ s.prev_label | escape }}">
            <svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true" focusable="false"><path d="m12.5 4.5-5.5 5.5 5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
          </button>
          <button type="button" class="mf-ticker__btn mf-ticker__btn--next" data-mf-next aria-label="{{ s.next_label | escape }}">
            <svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true" focusable="false"><path d="m7.5 4.5 5.5 5.5-5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
          </button>
        {%- endif -%}
        <button type="button" class="mf-ticker__btn mf-ticker__btn--toggle" data-mf-toggle aria-label="{{ s.pause_label | escape }}">
          <svg class="mf-ticker__pause" viewBox="0 0 20 20" width="12" height="12" aria-hidden="true" focusable="false"><rect x="5" y="4" width="3.2" height="12" rx="1" fill="currentColor"/><rect x="11.8" y="4" width="3.2" height="12" rx="1" fill="currentColor"/></svg>
          <svg class="mf-ticker__play" viewBox="0 0 20 20" width="12" height="12" aria-hidden="true" focusable="false"><path d="M6.5 4.2v11.6a.7.7 0 0 0 1.05.6l9.2-5.8a.7.7 0 0 0 0-1.2l-9.2-5.8a.7.7 0 0 0-1.05.6Z" fill="currentColor"/></svg>
        </button>
      </div>
    {%- endif -%}
  {%- endif -%}
</mf-announcement-ticker>

{% stylesheet %}
  .mf-ticker {
    --mf-ticker-fade: 72px;
    --mf-ticker-fade-end: var(--mf-ticker-fade);
    --mf-ticker-gap: 28px;
    position: relative;
    display: block;
    overflow: hidden;
    background: var(--mf-ticker-bg, #151311);
    color: var(--mf-ticker-fg, #f1ebe1);
    font-family: inherit;
  }
  .mf-ticker *,
  .mf-ticker *::before,
  .mf-ticker *::after {
    box-sizing: border-box;
  }
  .mf-ticker--border {
    border-top: 1px solid var(--mf-ticker-line);
    border-bottom: 1px solid var(--mf-ticker-line);
  }
  .mf-ticker__viewport {
    position: relative;
    display: flex;
    align-items: center;
    overflow: hidden;
    overflow: clip;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--mf-ticker-fade), #000 calc(100% - var(--mf-ticker-fade-end)), transparent);
    mask-image: linear-gradient(90deg, transparent, #000 var(--mf-ticker-fade), #000 calc(100% - var(--mf-ticker-fade-end)), transparent);
  }
  .mf-ticker--controls.mf-ticker--ready {
    --mf-ticker-fade-end: calc(var(--mf-ticker-fade) + 40px);
  }
  .mf-ticker__track {
    display: flex;
    flex-shrink: 0;
    width: max-content;
    animation: mf-ticker-scroll var(--mf-ticker-duration, 40s) linear infinite;
    will-change: transform;
  }
  .mf-ticker--right .mf-ticker__track {
    animation-direction: reverse;
  }
  .mf-ticker--hover-pause .mf-ticker__viewport:hover .mf-ticker__track,
  .mf-ticker__viewport:focus-within .mf-ticker__track,
  .mf-ticker--paused .mf-ticker__track,
  .mf-ticker--offscreen .mf-ticker__track {
    animation-play-state: paused;
  }
  .mf-ticker__half,
  .mf-ticker__group {
    display: flex;
    flex-shrink: 0;
    align-items: center;
  }
  .mf-ticker__group {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .mf-ticker__item {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    margin: 0;
    padding: 0;
    white-space: nowrap;
  }
  .mf-ticker__msg {
    display: inline-flex;
    align-items: center;
    gap: 0.62em;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-decoration: none;
  }
  .mf-ticker__icon {
    flex-shrink: 0;
    display: block;
    color: var(--mf-ticker-accent);
  }
  .mf-ticker__text {
    display: block;
  }
  .mf-ticker__msg--link .mf-ticker__text {
    background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat;
    transition: background-size 0.35s cubic-bezier(0.2, 0.7, 0.2, 1);
  }
  .mf-ticker__msg--link:hover .mf-ticker__text,
  .mf-ticker__msg--link:focus-visible .mf-ticker__text {
    background-size: 100% 1px;
  }
  .mf-ticker__msg--link:focus-visible {
    outline: 2px solid var(--mf-ticker-accent);
    outline-offset: 4px;
    border-radius: 2px;
  }
  .mf-ticker__item--hl .mf-ticker__text {
    color: var(--mf-ticker-accent);
  }
  .mf-ticker__sep {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 var(--mf-ticker-gap);
    color: var(--mf-ticker-accent);
    line-height: 1;
  }
  .mf-ticker__sep span {
    display: block;
  }

  /* Bar */
  .mf-ticker--bar .mf-ticker__viewport {
    min-height: var(--mf-ticker-h, 44px);
  }
  .mf-ticker--bar .mf-ticker__item {
    font-size: 12px;
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: 0.14em;
    text-transform: uppercase;
  }
  .mf-ticker--bar .mf-ticker__icon {
    width: 15px;
    height: 15px;
  }
  .mf-ticker--bar .mf-ticker__msg {
    gap: 9px;
  }
  .mf-ticker--bar .mf-ticker__sep {
    font-size: 11px;
    opacity: 0.85;
  }

  /* Statement */
  .mf-ticker--statement {
    --mf-ticker-fade: 140px;
    --mf-ticker-gap: 0.34em;
    background:
      radial-gradient(60% 140% at 50% 50%, var(--mf-ticker-soft), transparent 70%),
      var(--mf-ticker-bg, #f1ebe1);
  }
  .mf-ticker--statement .mf-ticker__viewport {
    padding: clamp(12px, 1.5vw, 22px) 0;
  }
  .mf-ticker--statement .mf-ticker__item {
    font-size: clamp(40px, 7vw, var(--mf-ticker-size, 88px));
    font-weight: 800;
    line-height: 1.04;
    letter-spacing: -0.035em;
    text-transform: uppercase;
  }
  .mf-ticker--statement .mf-ticker__msg {
    gap: 0.28em;
  }
  .mf-ticker--statement .mf-ticker__icon {
    width: 0.56em;
    height: 0.56em;
  }
  .mf-ticker--statement .mf-ticker__sep {
    font-size: 0.52em;
    font-weight: 400;
  }
  .mf-ticker--statement .mf-ticker__sep span {
    animation: mf-ticker-spin 7s linear infinite;
  }
  .mf-ticker--statement .mf-ticker__item:nth-child(even) .mf-ticker__text,
  .mf-ticker--statement.mf-ticker--odd .mf-ticker__group:nth-child(even) .mf-ticker__item:nth-child(odd) .mf-ticker__text {
    color: transparent;
    -webkit-text-stroke: max(1.5px, 0.02em) var(--mf-ticker-fg);
    font-style: italic;
    font-weight: 700;
    font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
    letter-spacing: -0.02em;
    text-transform: none;
  }
  .mf-ticker--statement.mf-ticker--odd .mf-ticker__group:nth-child(even) .mf-ticker__item:nth-child(even) .mf-ticker__text {
    color: inherit;
    -webkit-text-stroke: 0;
    font-style: normal;
    font-weight: 800;
    font-family: inherit;
    letter-spacing: -0.035em;
    text-transform: uppercase;
  }
  .mf-ticker--statement .mf-ticker__item--hl .mf-ticker__text,
  .mf-ticker--statement.mf-ticker--odd .mf-ticker__group:nth-child(even) .mf-ticker__item--hl:nth-child(even) .mf-ticker__text {
    color: var(--mf-ticker-accent);
  }
  .mf-ticker--statement .mf-ticker__item--hl:nth-child(even) .mf-ticker__text,
  .mf-ticker--statement.mf-ticker--odd .mf-ticker__group:nth-child(even) .mf-ticker__item--hl:nth-child(odd) .mf-ticker__text {
    color: transparent;
    -webkit-text-stroke-color: var(--mf-ticker-accent);
  }
  .mf-ticker--statement .mf-ticker__msg--link .mf-ticker__text {
    background-position: 0 92%;
    background-size: 0 0.06em;
  }
  .mf-ticker--statement .mf-ticker__msg--link:hover .mf-ticker__text,
  .mf-ticker--statement .mf-ticker__msg--link:focus-visible .mf-ticker__text {
    background-size: 100% 0.06em;
  }

  /* Controls: pause/play while moving, previous/next when motion is reduced */
  .mf-ticker__controls {
    position: absolute;
    z-index: 2;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    padding: 0 8px;
    pointer-events: none;
  }
  .mf-ticker--ready .mf-ticker__controls {
    display: flex;
  }
  .mf-ticker__btn {
    -webkit-appearance: none;
    appearance: none;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font: inherit;
    line-height: 1;
    letter-spacing: 0;
    text-transform: none;
    cursor: pointer;
    pointer-events: auto;
    opacity: 0.85;
    transition: opacity 0.2s ease;
  }
  .mf-ticker__btn::before {
    content: "";
    position: absolute;
    inset: 7px;
    border: 1px solid var(--mf-ticker-line-strong);
    border-radius: 50%;
    background: var(--mf-ticker-bg);
    box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.45);
    transition: border-color 0.2s ease, transform 0.2s ease;
  }
  .mf-ticker__btn svg {
    position: relative;
  }
  .mf-ticker__btn:hover {
    opacity: 1;
  }
  .mf-ticker__btn:hover::before {
    border-color: currentColor;
  }
  .mf-ticker__btn:focus-visible {
    opacity: 1;
    outline: 2px solid var(--mf-ticker-accent);
    outline-offset: -2px;
  }
  .mf-ticker__btn--prev,
  .mf-ticker__btn--next,
  .mf-ticker__play,
  .mf-ticker--paused .mf-ticker__pause {
    display: none;
  }
  .mf-ticker--paused .mf-ticker__play {
    display: block;
  }
  .mf-ticker--statement .mf-ticker__controls {
    padding: 0 clamp(8px, 1.4vw, 20px);
  }
  .mf-ticker--statement .mf-ticker__btn {
    width: 48px;
    height: 48px;
  }
  .mf-ticker--statement .mf-ticker__btn::before {
    inset: 4px;
  }

  @keyframes mf-ticker-scroll {
    from { transform: translate3d(0, 0, 0); }
    to { transform: translate3d(-50%, 0, 0); }
  }
  @keyframes mf-ticker-spin {
    to { transform: rotate(360deg); }
  }

  @media screen and (max-width: 749px) {
    .mf-ticker {
      --mf-ticker-fade: 36px;
      --mf-ticker-gap: 20px;
    }
    .mf-ticker--statement {
      --mf-ticker-fade: 48px;
      --mf-ticker-gap: 0.3em;
    }
    .mf-ticker--bar .mf-ticker__item {
      font-size: 11px;
      letter-spacing: 0.12em;
    }
    .mf-ticker--statement .mf-ticker__item {
      font-size: clamp(34px, 11vw, var(--mf-ticker-size, 88px));
    }
    .mf-ticker__controls {
      padding: 0 2px;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .mf-ticker__track {
      display: block;
      width: 100%;
      animation: none;
      transform: none;
    }
    .mf-ticker__half {
      display: block;
      width: 100%;
    }
    .mf-ticker__half + .mf-ticker__half,
    .mf-ticker__group[aria-hidden="true"] {
      display: none;
    }
    .mf-ticker__group {
      width: 100%;
    }
    .mf-ticker__viewport {
      overflow-x: auto;
      overflow-y: hidden;
      overscroll-behavior-x: contain;
      scroll-snap-type: x mandatory;
      scrollbar-width: none;
      -webkit-mask-image: none;
      mask-image: none;
    }
    .mf-ticker__viewport::-webkit-scrollbar {
      display: none;
    }
    .mf-ticker__item {
      flex: 0 0 100%;
      justify-content: center;
      padding: 0 52px;
      white-space: normal;
      text-align: center;
      scroll-snap-align: center;
    }
    .mf-ticker--statement .mf-ticker__item {
      font-size: clamp(22px, 4.4vw, calc(var(--mf-ticker-size, 88px) * 0.7));
      line-height: 1.1;
    }
    .mf-ticker__sep {
      display: none;
    }
    .mf-ticker__sep span,
    .mf-ticker__msg--link .mf-ticker__text,
    .mf-ticker__btn,
    .mf-ticker__btn::before {
      animation: none;
      transition: none;
    }
    .mf-ticker__controls {
      justify-content: space-between;
    }
    .mf-ticker__btn--toggle {
      display: none;
    }
    .mf-ticker__btn--prev,
    .mf-ticker__btn--next {
      display: inline-flex;
    }
  }
{% endstylesheet %}

{% javascript %}
  if (!customElements.get('mf-announcement-ticker')) {
    customElements.define(
      'mf-announcement-ticker',
      class extends HTMLElement {
        connectedCallback() {
          this.viewport = this.querySelector('[data-mf-viewport]');
          this.track = this.querySelector('[data-mf-track]');
          this.halves = Array.from(this.querySelectorAll('[data-mf-half]'));
          if (!this.viewport || !this.track || this.halves.length < 2) return;
          this.toggleBtn = this.querySelector('[data-mf-toggle]');
          this.prevBtn = this.querySelector('[data-mf-prev]');
          this.nextBtn = this.querySelector('[data-mf-next]');
          this.speed = Number(this.dataset.speed) || 60;
          this.motion = window.matchMedia('(prefers-reduced-motion: reduce)');
          this.paused = false;

          this.onToggle = () => this.setPaused(!this.paused);
          this.onPrev = () => this.step(-1);
          this.onNext = () => this.step(1);
          this.onChange = () => this.schedule();
          if (this.toggleBtn) this.toggleBtn.addEventListener('click', this.onToggle);
          if (this.prevBtn) this.prevBtn.addEventListener('click', this.onPrev);
          if (this.nextBtn) this.nextBtn.addEventListener('click', this.onNext);
          if (this.motion.addEventListener) this.motion.addEventListener('change', this.onChange);

          if ('ResizeObserver' in window) {
            this.ro = new ResizeObserver(this.onChange);
            this.ro.observe(this.viewport);
          } else {
            window.addEventListener('resize', this.onChange);
          }
          if ('IntersectionObserver' in window) {
            this.io = new IntersectionObserver((entries) => {
              this.classList.toggle('mf-ticker--offscreen', !entries[entries.length - 1].isIntersecting);
            });
            this.io.observe(this);
          }
          if (document.fonts && document.fonts.ready) document.fonts.ready.then(this.onChange);

          this.classList.add('mf-ticker--ready');
          this.layout();
        }

        disconnectedCallback() {
          if (!this.viewport) return;
          cancelAnimationFrame(this.frame);
          if (this.toggleBtn) this.toggleBtn.removeEventListener('click', this.onToggle);
          if (this.prevBtn) this.prevBtn.removeEventListener('click', this.onPrev);
          if (this.nextBtn) this.nextBtn.removeEventListener('click', this.onNext);
          if (this.motion && this.motion.removeEventListener) this.motion.removeEventListener('change', this.onChange);
          if (this.ro) this.ro.disconnect();
          else window.removeEventListener('resize', this.onChange);
          if (this.io) this.io.disconnect();
        }

        schedule() {
          cancelAnimationFrame(this.frame);
          this.frame = requestAnimationFrame(() => this.layout());
        }

        layout() {
          if (!this.isConnected || this.motion.matches) return;
          const group = this.halves[0].firstElementChild;
          const template = this.halves[1].firstElementChild;
          const groupWidth = group ? group.getBoundingClientRect().width : 0;
          const viewWidth = this.viewport.clientWidth;
          if (!groupWidth || !viewWidth || !template) return;

          // Each half must be at least as wide as the viewport for a seamless loop.
          let needed = Math.max(1, Math.ceil(viewWidth / groupWidth));
          if (this.classList.contains('mf-ticker--odd') && needed % 2) needed += 1;
          needed = Math.min(needed, 40);
          this.halves.forEach((half) => {
            while (half.children.length < needed) half.appendChild(template.cloneNode(true));
          });

          const halfWidth = this.halves[0].getBoundingClientRect().width;
          const duration = Math.max(6, halfWidth / this.speed);
          if (Math.abs(duration - (this.duration || 0)) > 0.25) {
            this.duration = duration;
            this.style.setProperty('--mf-ticker-duration', duration.toFixed(2) + 's');
          }
        }

        setPaused(paused) {
          this.paused = paused;
          this.classList.toggle('mf-ticker--paused', paused);
          if (this.toggleBtn) {
            this.toggleBtn.setAttribute('aria-label', paused ? this.dataset.playLabel : this.dataset.pauseLabel);
          }
        }

        step(direction) {
          const vp = this.viewport;
          const width = vp.clientWidth;
          const max = vp.scrollWidth - width;
          let next = Math.round((vp.scrollLeft + direction * width) / width) * width;
          if (next > max + 2) next = 0;
          if (next < -2) next = max;
          vp.scrollTo({ left: next, behavior: 'auto' });
        }
      }
    );
  }
{% endjavascript %}

{% schema %}
{
  "name": "EX · Announcement ticker",
  "tag": "section",
  "class": "mf-section",
  "disabled_on": {
    "groups": ["footer"]
  },
  "settings": [
    {
      "type": "header",
      "content": "Style"
    },
    {
      "type": "select",
      "id": "style",
      "label": "Style",
      "options": [
        { "value": "bar", "label": "Bar (slim announcement bar)" },
        { "value": "statement", "label": "Statement (tall band of large type)" }
      ],
      "default": "bar"
    },
    {
      "type": "range",
      "id": "bar_height",
      "label": "Bar height",
      "info": "Bar style only.",
      "min": 36,
      "max": 64,
      "step": 2,
      "unit": "px",
      "default": 44
    },
    {
      "type": "range",
      "id": "statement_size",
      "label": "Statement text size",
      "info": "Statement style only. Scales down on small screens.",
      "min": 40,
      "max": 128,
      "step": 4,
      "unit": "px",
      "default": 88
    },
    {
      "type": "select",
      "id": "separator",
      "label": "Separator",
      "options": [
        { "value": "dot", "label": "•" },
        { "value": "star", "label": "✦" },
        { "value": "slash", "label": "/" },
        { "value": "dash", "label": "–" }
      ],
      "default": "star"
    },
    {
      "type": "checkbox",
      "id": "show_border",
      "label": "Show top and bottom border",
      "default": false
    },
    {
      "type": "header",
      "content": "Motion"
    },
    {
      "type": "range",
      "id": "speed",
      "label": "Scroll speed",
      "min": 1,
      "max": 10,
      "step": 1,
      "default": 5
    },
    {
      "type": "select",
      "id": "direction",
      "label": "Direction",
      "options": [
        { "value": "left", "label": "Right to left" },
        { "value": "right", "label": "Left to right" }
      ],
      "default": "left"
    },
    {
      "type": "checkbox",
      "id": "pause_on_hover",
      "label": "Pause on hover",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "show_controls",
      "label": "Show pause button",
      "info": "Lets visitors stop the motion, recommended for accessibility. With reduced motion it becomes previous and next arrows.",
      "default": true
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "color_bg",
      "label": "Background",
      "default": "#151311"
    },
    {
      "type": "color",
      "id": "color_text",
      "label": "Text",
      "default": "#f1ebe1"
    },
    {
      "type": "color",
      "id": "color_accent",
      "label": "Accent (icons, separators, highlights)",
      "default": "#ff5a36"
    },
    {
      "type": "header",
      "content": "Accessibility labels"
    },
    {
      "type": "text",
      "id": "region_label",
      "label": "Region name",
      "default": "Announcements"
    },
    {
      "type": "text",
      "id": "pause_label",
      "label": "Pause button label",
      "default": "Pause announcements"
    },
    {
      "type": "text",
      "id": "play_label",
      "label": "Play button label",
      "default": "Play announcements"
    },
    {
      "type": "text",
      "id": "prev_label",
      "label": "Previous button label",
      "default": "Previous announcement"
    },
    {
      "type": "text",
      "id": "next_label",
      "label": "Next button label",
      "default": "Next announcement"
    }
  ],
  "blocks": [
    {
      "type": "message",
      "name": "Message",
      "settings": [
        {
          "type": "text",
          "id": "text",
          "label": "Text",
          "default": "Free shipping over €60"
        },
        {
          "type": "url",
          "id": "link",
          "label": "Link"
        },
        {
          "type": "select",
          "id": "icon",
          "label": "Icon",
          "options": [
            { "value": "sparkle", "label": "Sparkle" },
            { "value": "truck", "label": "Truck" },
            { "value": "gift", "label": "Gift" },
            { "value": "star", "label": "Star" },
            { "value": "leaf", "label": "Leaf" },
            { "value": "none", "label": "None" }
          ],
          "default": "sparkle"
        },
        {
          "type": "checkbox",
          "id": "highlight",
          "label": "Highlight in accent color",
          "default": false
        }
      ]
    }
  ],
  "max_blocks": 12,
  "presets": [
    {
      "name": "EX · Announcement ticker",
      "settings": {
        "style": "statement",
        "color_bg": "#f1ebe1",
        "color_text": "#151311",
        "color_accent": "#ff5a36",
        "show_border": true
      },
      "blocks": [
        { "type": "message", "settings": { "text": "Free shipping over €60", "icon": "none" } },
        { "type": "message", "settings": { "text": "New: Night Repair Cream", "icon": "none", "link": "/collections/all" } },
        { "type": "message", "settings": { "text": "30-day money-back guarantee", "icon": "none" } },
        { "type": "message", "settings": { "text": "Carbon-neutral delivery", "icon": "none" } }
      ]
    },
    {
      "name": "EX · Ticker bar",
      "settings": {
        "style": "bar"
      },
      "blocks": [
        { "type": "message", "settings": { "text": "Free shipping over €60", "icon": "truck" } },
        { "type": "message", "settings": { "text": "New: Night Repair Cream", "icon": "sparkle", "link": "/collections/all", "highlight": true } },
        { "type": "message", "settings": { "text": "30-day money-back guarantee", "icon": "star" } },
        { "type": "message", "settings": { "text": "Carbon-neutral delivery", "icon": "leaf" } }
      ]
    }
  ]
}
{% endschema %}
Why it converts

The offer that tips a purchase (free shipping, guarantee, new launch) stays in view on every page instead of being read once.

Specs

File: sections/mf-announcement-ticker.liquidIn the theme editor: EX · Announcement tickerBest on: index, product, collection, cart, page templatesSettings: 21 · blocks: MessageValidated: Shopify Theme Check, no external assets

About this section

Announcement Ticker is a free Shopify Online Store 2.0 section that turns store announcements into an infinite, seamless marquee. It has two styles. Bar is a slim strip of small uppercase text with icons and separators that can sit in the header group above your navigation. Statement is a tall band of large type in which messages alternate between a filled sans-serif and an outlined italic serif, with accent separators that slowly rotate. Each message is a block with its own text, optional link, an icon (sparkle, truck, gift, star, leaf or none) and an accent highlight. Merchants set the scroll speed, direction, separator glyph, bar height or statement text size, colors, a border line and whether the ticker pauses on hover. The loop is built by duplicating the message group in Liquid; the copies are hidden from screen readers and their links are removed from the tab order, and a small script adds copies on very wide screens and derives the duration from the real width so the speed stays constant. Edges fade out with a mask. A pause button stops the motion, keyboard focus pauses it too, and the animation pauses when the ticker is off screen. For visitors who prefer reduced motion, the ticker becomes static: the first message is centered and the others are reachable with previous and next arrows or a swipe. The conversion principle is persistence: the reason to buy now stays visible wherever the shopper browses. It is one self-contained .liquid file with no external assets.

Questions

Can I use it as the announcement bar above my header?

Yes. Choose the Bar style and add the section to the header group in the theme editor. The section can be placed in the header or in any template, and is only disabled in the footer group.

How do I add, reorder or link messages?

Each message is a block. Add up to 12, drag them to reorder, and give any of them a link, an icon and an accent highlight. The ticker loops them seamlessly whatever their number or length.

Is a scrolling ticker accessible?

Screen readers get each message once as a list, because the duplicated copies are hidden and their links skipped. A pause button stops the motion, hovering or focusing a link pauses it, and with reduced motion the ticker is static with previous and next arrows.

Does the speed change with the number of messages?

No. A small script measures the real width of your messages and sets the duration so the text always moves at the chosen scroll speed. Without JavaScript the loop still runs with an estimated duration.

More sections