jelonyx
No-app fixes · Fix 07

Shopify product tabs
without an app.

Product pages that cram description, shipping policy, returns info, and FAQs into one long block lose customers to scroll fatigue. Tabs organise the same content without adding length to the page. Most stores pay a monthly app fee for this. This fix adds accessible, keyboard-navigable tabs with a single Liquid snippet without an app or library.

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

The code

One file. Edit the tab labels and replace the placeholder content inside each panel.

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

{%- comment -%}
  Edit the tab labels in the <button> elements below.
  Edit the content inside each <div role="tabpanel">.
  To add a tab: copy any button + panel pair, increment the number in the IDs.
  To remove a tab: delete the button and its matching panel.
  The first tab uses product.description automatically: replace with custom content if preferred.
{%- endcomment -%}

<div class="jlx-tabs" id="jlx-tabs">

  <div class="jlx-tabs__nav" role="tablist" aria-label="Product information">
    <button class="jlx-tabs__tab" role="tab" aria-selected="true"  aria-controls="jlx-tab-1" id="jlx-btn-1" type="button">Description</button>
    <button class="jlx-tabs__tab" role="tab" aria-selected="false" aria-controls="jlx-tab-2" id="jlx-btn-2" type="button">Shipping</button>
    <button class="jlx-tabs__tab" role="tab" aria-selected="false" aria-controls="jlx-tab-3" id="jlx-btn-3" type="button">Returns</button>
  </div>

  <div class="jlx-tabs__panel" role="tabpanel" id="jlx-tab-1" aria-labelledby="jlx-btn-1">
    {{ product.description }}
  </div>

  <div class="jlx-tabs__panel" role="tabpanel" id="jlx-tab-2" aria-labelledby="jlx-btn-2" hidden>
    <p>We offer free standard shipping on all orders over $75. Standard shipping takes 3–5 business days. Express shipping (1–2 business days) is available at checkout.</p>
    <p>Orders are dispatched within 1 business day of being placed. You will receive a tracking email once your order ships.</p>
  </div>

  <div class="jlx-tabs__panel" role="tabpanel" id="jlx-tab-3" aria-labelledby="jlx-btn-3" hidden>
    <p>We accept returns within 30 days of delivery. Items must be unused and in their original packaging. To start a return, email us at support@yourstore.com with your order number.</p>
    <p>Refunds are processed within 5 business days of receiving your return. Original shipping fees are non-refundable.</p>
  </div>

</div>

<style>
  .jlx-tabs {
    font-family: inherit;
    margin-top: 24px;
  }
  .jlx-tabs__nav {
    display: flex;
    border-bottom: 1px solid var(--color-border, rgba(0, 0, 0, 0.1));
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .jlx-tabs__nav::-webkit-scrollbar { display: none; }
  .jlx-tabs__tab {
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    padding: 10px 16px;
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    color: var(--color-foreground, #1a1a1a);
    opacity: 0.45;
    cursor: pointer;
    white-space: nowrap;
    flex-shrink: 0;
    transition: opacity 0.15s ease, border-color 0.15s ease;
  }
  .jlx-tabs__tab:hover { opacity: 0.7; }
  .jlx-tabs__tab[aria-selected="true"] {
    opacity: 1;
    border-bottom-color: var(--color-foreground, #1a1a1a);
  }
  .jlx-tabs__panel {
    padding: 20px 0 4px;
    font-size: 14px;
    line-height: 1.65;
    color: var(--color-foreground, #1a1a1a);
    opacity: 0.8;
  }
  .jlx-tabs__panel p,
  .jlx-tabs__panel ul,
  .jlx-tabs__panel ol { margin: 0 0 12px; }
  .jlx-tabs__panel p:last-child,
  .jlx-tabs__panel ul:last-child,
  .jlx-tabs__panel ol:last-child { margin-bottom: 0; }
  .jlx-tabs__panel[hidden] { display: none; }
</style>

<script>
  (function () {
    'use strict';

    var container = document.getElementById('jlx-tabs');
    if (!container) return;

    var tabs   = Array.prototype.slice.call(container.querySelectorAll('[role="tab"]'));
    var panels = Array.prototype.slice.call(container.querySelectorAll('[role="tabpanel"]'));

    function activate(tab) {
      tabs.forEach(function (t) {
        t.setAttribute('aria-selected', t === tab ? 'true' : 'false');
      });
      panels.forEach(function (p) {
        if (p.id === tab.getAttribute('aria-controls')) {
          p.removeAttribute('hidden');
        } else {
          p.setAttribute('hidden', '');
        }
      });
    }

    tabs.forEach(function (tab, i) {
      tab.addEventListener('click', function () { activate(tab); });
      tab.addEventListener('keydown', function (e) {
        var n = tabs.length;
        if (e.key === 'ArrowRight') { e.preventDefault(); activate(tabs[(i + 1) % n]); tabs[(i + 1) % n].focus(); }
        if (e.key === 'ArrowLeft')  { e.preventDefault(); activate(tabs[(i - 1 + n) % n]); tabs[(i - 1 + n) % n].focus(); }
        if (e.key === 'Home')       { e.preventDefault(); activate(tabs[0]); tabs[0].focus(); }
        if (e.key === 'End')        { e.preventDefault(); activate(tabs[n - 1]); tabs[n - 1].focus(); }
      });
    });
  })();
</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-product-tabs and click Done.

  3. 03
    Paste the code and edit your tabs.

    Delete any placeholder content and paste the entire code block above. Edit the text inside each <button> to set your tab labels. Replace the placeholder content inside each <div role="tabpanel"> with your store's actual shipping and returns policies. The first panel uses {{ product.description }} automatically. Leave it or replace it with custom HTML. Save the file.

  4. 04
    Add the render tag via the theme customizer.

    In Shopify Admin, go to Online Store → Themes → Customize. Navigate to a product page. Click on the product information section, then click Add block → Custom Liquid. Enter the following, then drag the block to where you want the tabs to appear. This is typically below the variant pickers or below the Buy buttons block. Save.

    {% render 'jlx-product-tabs' %}
  5. 05
    Optionally hide the theme's built-in description.

    If your first tab shows {{ product.description }}, the description will appear twice: once in the tab and once in the theme's default description block. To fix this, stay in the customizer, click on the product information section, find the Description block in the left panel, and click the eye icon to hide it. Save.

  6. 06
    Test on a product page.

    Open any product page. Click each tab to confirm the content switches. Test keyboard navigation: tab to the tab list, then use / to move between tabs, and Home / End to jump to the first or last tab.

Adding and removing tabs

The snippet ships with three tabs: Description, Shipping, and Returns. To add a fourth tab (e.g. for FAQs or Ingredients), copy any <button> and its matching <div role="tabpanel">, then increment the number in both IDs:

<button ... aria-controls="jlx-tab-4" id="jlx-btn-4">FAQs</button>
...
<div ... id="jlx-tab-4" aria-labelledby="jlx-btn-4" hidden>
  <p>Your FAQ content here.</p>
</div>

To remove a tab, delete the <button> and its matching <div role="tabpanel"> block. The JavaScript discovers tabs dynamically, so removing a button and panel pair requires no other changes.

How it works

The tabs use the ARIA tablist pattern: role="tablist" on the nav, role="tab" on each button, and role="tabpanel" on each content panel. This gives screen readers the information they need to announce the tab structure correctly without requiring any additional ARIA configuration on your part.

Inactive panels use the HTML hidden attribute, which the browser treats as display: none natively. The JavaScript adds and removes it via setAttribute / removeAttribute. No class toggling or CSS visibility tricks are needed.

The active tab is indicated by aria-selected="true", which the CSS uses as a selector to apply the bottom border highlight. This keeps the visual state and the accessibility state in sync from a single source of truth. There is no separate CSS class to manage.

Keyboard navigation follows the ARIA Authoring Practices Guide tab pattern: / move focus between tabs and activate them, Home and End jump to the first and last tab. The tab nav scrolls horizontally on narrow screens using overflow-x: auto with the scrollbar hidden. Labels are never truncated on mobile.

Compatibility

Tested against Dawn 12+ and Horizon. The snippet reads --color-foreground and --color-border from your theme, so the tab colours and separator line inherit your store's palette without any CSS edits.

Some Dawn versions apply their own styles to <p> elements inside product content areas like margins, font sizes, or line heights. If the tab panel content inherits unexpected spacing, override those styles by targeting .jlx-tabs__panel p in the snippet CSS.

The hidden attribute and overflow-x: auto are supported in all browsers that reach Shopify storefronts. The JavaScript uses no ES6+ features, so no transpilation is required.

Limitations

  • Tab content is in code: tab labels and content live directly in the snippet file. Editing them requires access to the theme code editor. For a version where non-technical team members can manage tab content from the Shopify customizer, a section with schema settings is the cleaner approach, which is a more involved build.
  • Same tabs on every product: the snippet renders the same tabs on every product page it is placed on. If you need product-specific tabs (e.g. an Ingredients tab only for food products), add a Liquid {% if %} condition around the relevant panel using product.type or product.tags.
  • One tab set per page: the snippet uses a fixed id="jlx-tabs" for the JavaScript selector. Rendering it more than once on the same page will result in only the first instance being interactive. Remove the id and update the JS selector to .jlx-tabs with querySelectorAll if you need multiple independent tab groups.
  • No tab focus trap: pressing Tab while focus is inside the tablist moves focus to the next interactive element in the page (the standard browser behaviour for tablist components). This is correct per the ARIA spec. Tab navigates the page, and Arrow keys navigate within the tablist.
  • Headless storefronts: this is a Liquid snippet and does not apply to Hydrogen or other headless setups.
No-App Shopify Fix Sprint

Need this installed?

If you would rather not edit theme code, we can install and style the tabs for your store. We will match them to your theme, populate them with your actual policies, and set them up so they can be managed from the Shopify customizer without touching code.