jelonyx
No-app fixes · Fix 09

Shopify announcement bar
without an app.

A slim bar at the top of every page is one of the highest-visibility placements in ecommerce. It is useful for flash sales, free shipping thresholds, and limited-time offers. Dawn has a built-in bar, but it has no countdown timer. Most countdown bar apps charge $10–20 per month. This snippet adds the countdown in about 15 minutes, keeps the dismiss behaviour, and costs nothing.

DifficultyBeginner
Time~15 minutes
CostFree
Theme accessRequired
Compatible with:Dawn 12+HorizonSenseCraftMost OS 2.0 themes

The code

Edit the four config variables at the top: message, link, countdown date, and bar ID. Everything else is automatic.

snippets/jlx-announcement-bar.liquid
Liquid · CSS · JS
{% comment %}
  Jelonyx · Announcement Bar : no app required
  Compatible with Dawn 12+, Horizon, and most Online Store 2.0 themes
  Source: jelonyx.com/shopify/no-app/announcement-bar
{% endcomment %}

{%- comment -%}
  Configuration : edit these four values, everything else is automatic.
    jlx_bar_message  : The text displayed in the bar.
    jlx_bar_link     : URL if the message should be a link. Leave blank for plain text.
    jlx_countdown_to : ISO 8601 target date/time for the countdown (YYYY-MM-DDTHH:MM:SS).
                       Leave blank to hide the countdown and show the message only.
    jlx_bar_id       : Unique ID used as the sessionStorage dismiss key.
                       Change this when you update the message so returning visitors
                       see the new bar even if they dismissed the previous one.
{%- endcomment -%}

{%- assign jlx_bar_message    = 'Free shipping on all orders this weekend: ends Monday.' -%}
{%- assign jlx_bar_link       = '' -%}
{%- assign jlx_countdown_to   = '2026-12-31T23:59:59' -%}
{%- assign jlx_bar_id         = 'bar-01' -%}

<div class="jlx-bar" id="jlx-bar-{{ jlx_bar_id }}" role="banner" aria-label="Announcement">
  <div class="jlx-bar__inner">
    <div class="jlx-bar__spacer" aria-hidden="true"></div>
    <div class="jlx-bar__content">
      {%- if jlx_bar_link != blank -%}
        <a class="jlx-bar__msg" href="{{ jlx_bar_link }}">{{ jlx_bar_message }}</a>
      {%- else -%}
        <span class="jlx-bar__msg">{{ jlx_bar_message }}</span>
      {%- endif -%}
      {%- if jlx_countdown_to != blank -%}
        <span
          class="jlx-bar__countdown"
          data-jlx-target="{{ jlx_countdown_to }}"
          aria-live="off"
          aria-label="Time remaining"
        ></span>
      {%- endif -%}
    </div>
    <button
      class="jlx-bar__close"
      data-jlx-bar="{{ jlx_bar_id }}"
      type="button"
      aria-label="Dismiss announcement"
    >
      <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
        <path d="M1 1l10 10M11 1L1 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
      </svg>
    </button>
  </div>
</div>

<style>
  .jlx-bar {
    background: #1a1a1a;
    color: #fff;
    font-family: inherit;
    font-size: 13px;
    line-height: 1;
  }
  .jlx-bar[hidden] { display: none; }
  .jlx-bar__inner {
    display: flex;
    align-items: center;
    padding: 10px 16px;
    gap: 8px;
    max-width: 1200px;
    margin: 0 auto;
  }
  .jlx-bar__spacer { width: 28px; flex-shrink: 0; }
  .jlx-bar__content {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    flex-wrap: wrap;
  }
  .jlx-bar__msg {
    color: inherit;
    text-decoration: none;
    font-weight: 500;
    letter-spacing: 0.01em;
  }
  a.jlx-bar__msg:hover { text-decoration: underline; }
  .jlx-bar__countdown {
    font-family: var(--font-heading-family, monospace);
    font-size: 13px;
    opacity: 0.7;
    letter-spacing: 0.04em;
    white-space: nowrap;
  }
  .jlx-bar__close {
    background: none;
    border: none;
    color: inherit;
    opacity: 0.5;
    cursor: pointer;
    padding: 4px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    transition: opacity 0.15s ease;
  }
  .jlx-bar__close:hover { opacity: 1; }
</style>

<script>
  (function () {
    'use strict';
    var barId      = '{{ jlx_bar_id | escape }}';
    var storageKey = 'jlx-bar-' + barId;
    var bar        = document.getElementById('jlx-bar-' + barId);
    if (!bar) return;

    // Hide if dismissed in this session
    if (sessionStorage.getItem(storageKey) === '1') {
      bar.hidden = true;
      return;
    }

    // Dismiss button
    var closeBtn = bar.querySelector('[data-jlx-bar]');
    if (closeBtn) {
      closeBtn.addEventListener('click', function () {
        bar.hidden = true;
        sessionStorage.setItem(storageKey, '1');
      });
    }

    // Countdown timer
    var countdownEl = bar.querySelector('[data-jlx-target]');
    if (countdownEl) {
      var rawTarget = countdownEl.getAttribute('data-jlx-target');
      var target    = rawTarget ? new Date(rawTarget).getTime() : 0;

      function pad(n) { return n < 10 ? '0' + n : String(n); }

      function tick() {
        var diff = target - Date.now();
        if (diff <= 0) {
          countdownEl.textContent = 'Offer ended';
          return;
        }
        var d = Math.floor(diff / 86400000);
        var h = Math.floor((diff % 86400000) / 3600000);
        var m = Math.floor((diff % 3600000) / 60000);
        var s = Math.floor((diff % 60000) / 1000);
        countdownEl.textContent = (d > 0 ? d + 'd ' : '') + pad(h) + 'h ' + pad(m) + 'm ' + pad(s) + 's';
      }

      tick();
      setInterval(tick, 1000);
    }
  })();
</script>

How to install

  1. 01
    Open your theme code editor.

    In Shopify Admin, go to Online Store → Themes. On your active theme, click the three-dot menu and select Edit code.

  2. 02
    Create a new snippet.

    Under the Snippets folder, click Add a new snippet. Name it jlx-announcement-bar and click Done.

  3. 03
    Paste the code and configure it.

    Delete any placeholder content and paste the entire code block above. Edit the four config variables at the top of the file:

    • jlx_bar_message: the text shown in the bar.
    • jlx_bar_link: a URL if the message should be clickable. Leave as '' for plain text.
    • jlx_countdown_to: your offer end date/time in YYYY-MM-DDTHH:MM:SS format (e.g. '2026-12-31T23:59:59'). Leave as '' to hide the countdown.
    • jlx_bar_id: any short unique string. Change this each time you update the message so returning visitors see the new bar.

    Save the file.

  4. 04
    Add the render tag to theme.liquid.

    Open layout/theme.liquid. Find the opening <body tag (it will have several classes on it). On the very next line, before anything else inside the body, add:

    {% render 'jlx-announcement-bar' %}

    Save the file.

  5. 05
    Optionally disable Dawn's built-in bar.

    If you are on Dawn and already have an announcement bar active, you will now have two bars. To remove the theme's default bar, go to Online Store → Themes → Customize, click on the Header section, find the Announcement bar block in the left panel, and click the eye icon to hide it, or delete the block entirely. Save.

  6. 06
    Test the bar.

    Open your storefront. The bar should appear above the header. Confirm the countdown is ticking. Click the ✕ button and confirm the bar hides. Open a new browser tab and the bar should reappear since the dismiss state is per-session. Open the same tab again after dismissing and the bar should stay hidden.

Customisation

Change the bar colour. The snippet defaults to a dark bar on a light store. To change it, edit the background and color values in the .jlx-bar CSS rule. To match your theme's accent colour, replace the hex with var(--color-button) and var(--color-button-text).

Make the bar persist across sessions. By default, a dismissed bar stays hidden for the current browser session only. To persist the dismiss across sessions (so it stays hidden on return visits), change sessionStorage to localStorage in both places in the JavaScript. Remember to change jlx_bar_id when you update the message so returning visitors see the new bar.

Hide the countdown and show a message only. Set jlx_countdown_to to an empty string: assign jlx_countdown_to = ''. The countdown element will not be rendered.

Make the bar sticky. By default, the bar scrolls with the page. To keep it pinned at the top as the user scrolls, add position: sticky; top: 0; to the .jlx-bar CSS rule. You may also need to add z-index: 200; to ensure it appears above the header.

How it works

The snippet renders on every page because it lives in layout/theme.liquid, which wraps every storefront page. Liquid renders the bar server-side with the message and config baked in. No client-side fetch is required.

On page load, the JavaScript checks sessionStorage for the dismiss key. If found, it sets bar.hidden = true immediately before the browser paints. This ensures there is no flash of the bar before it hides. The hidden HTML attribute maps directly to display: none, which is overridden by the .jlx-bar[hidden] rule in the snippet CSS.

The countdown reads the target date from the data-jlx-target attribute, calculates the difference from Date.now(), and formats it into days, hours, minutes, and seconds. It updates every second with setInterval. When the target date passes, it shows “Offer ended” instead of negative numbers.

The jlx_bar_id variable is the dismiss key in sessionStorage. Changing it (e.g. from bar-01 to bar-02) means returning visitors who dismissed the old bar will see the new one since their stored key no longer matches.

Compatibility

Placing the snippet in layout/theme.liquid means it renders on every page including the cart, checkout pages excluded. Shopify checkout uses its own layout and ignores theme.liquid.

Dawn 12+ applies its own --color-foreground variable to text inside the <body> element. The snippet overrides that with explicit color: #fff on .jlx-bar, so the white text renders correctly regardless of the theme's body text colour.

The snippet uses no ES6+ features and no <template> elements, so it works in all browsers that reach Shopify storefronts without transpilation.

Limitations

  • Message is in code: the bar message is set in the snippet file. Non-technical team members cannot update it from the Shopify customizer without theme code access. For a version manageable from the customizer, a section with schema settings is the right approach, which is a more involved build.
  • Countdown is client-side only: the timer runs in the visitor's browser using their local clock. If the visitor's clock is significantly wrong, the countdown will be off. For an authoritative server-time countdown, you would need a metaobject or API call. This is overkill for most promotions.
  • Dismiss is per session by default: closing the bar only persists for the browser session. Switching to localStorage (see Customisation) makes it persist across sessions but means you must change jlx_bar_id for every new campaign.
  • One bar at a time: the snippet renders one bar per page. To show different messages on different page types (e.g. a product-specific offer on product pages), wrap the config variables in a Liquid {% if request.page_type == 'product' %} condition.
No-App Shopify Fix Sprint

Need this installed?

If you would rather not edit theme code, we can install and style the announcement bar for your store. We will match it to your brand colours, configure it with your offer message and countdown date, and test it across your live theme.