Saltmere Reviews Reviews Block - Let's templates
Unlock every premium block and template with All-Access from $20 / 3 months, cancel anytime. Get All-Access
All blocks
Free Reviews

Saltmere Reviews

A reviews block extracted from the Saltmere template, available in Tailwind CSS and Bootstrap.

0 1
Saltmere Reviews preview
saltmere.html
<!DOCTYPE html>
<html lang="en">
<head>
  <script>
    window.FontAwesomeConfig = { autoReplaceSvg: 'nest' };
  </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Saltmere Reviews | Tailwind CSS UI Block</title>
  <meta name="description" content="A reviews block extracted from the Saltmere template, available in Tailwind CSS and Bootstrap.">
  <meta name="keywords" content="Saltmere, Tailwind CSS template, HTML website template, ecommerce template, skincare shop, grooming products, subscription box, beauty store website, product page, shopping cart">
  <meta name="author" content="Let's templates">
  <link rel="canonical" href="">

  <meta property="og:type" content="product">
  <meta property="og:site_name" content="Let's templates">
  <meta property="og:locale" content="en_US">
  <meta property="og:url" content="">
  <meta property="og:title" content="Saltmere | Tailwind CSS Ecommerce Template">
  <meta property="og:description" content="A warm, art-deco Tailwind CSS ecommerce template for grooming and skincare brands, with a monthly box builder, membership pricing and a working cart.">
  <meta property="og:image" content="">
  <meta property="og:image:alt" content="Saltmere template preview">

  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:url" content="">
  <meta name="twitter:title" content="Saltmere | Tailwind CSS Ecommerce Template">
  <meta name="twitter:description" content="A warm, art-deco Tailwind CSS ecommerce template for grooming and skincare brands, with a monthly box builder, membership pricing and a working cart.">
  <meta name="twitter:image" content="">
  <meta name="twitter:image:alt" content="Saltmere template preview">
  <script src="https://cdn.tailwindcss.com/3.4.17"></script>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link href="https://fonts.googleapis.com/css2?family=Federo&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet">
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: { sand: '#ecd5b5', peach: '#f3cdb8', cream: '#f5e6be', ink: '#1d1b19', rust: { DEFAULT: '#a8443a', dark: '#87322a' }, sage: '#617d58' },
          fontFamily: { sans: ['"Space Grotesk"', 'sans-serif'], display: ['Federo', 'serif'] }
        }
      }
    }
  </script>
  <style>
    @view-transition { navigation: auto; }
    ::view-transition-old(root) { animation: 180ms ease-out both page-fade-out; }
    ::view-transition-new(root) { animation: 220ms ease-in both page-fade-in; }
    @keyframes page-fade-out { to { opacity: 0; } }
    @keyframes page-fade-in { from { opacity: 0; } }
    html { scroll-behavior: smooth; }
    [hidden] { display: none !important; }
    body { background: radial-gradient(circle at 0% 0%, #f3c4b4 0, transparent 40%), radial-gradient(circle at 100% 100%, #f5e6be 0, transparent 45%), #ecd5b5; background-attachment: fixed; }
    .grain { position: fixed; inset: 0; pointer-events: none; z-index: 60; opacity: .18; mix-blend-mode: multiply; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.6'/%3E%3C/svg%3E"); }
    .scribble path { stroke-dasharray: 420; stroke-dashoffset: 0; transition: stroke-dashoffset .7s ease; }
    .group:hover .scribble path, .group:focus-within .scribble path { stroke-dashoffset: 840; }
    .rail { scrollbar-width: none; }
    .rail::-webkit-scrollbar { display: none; }
    .ring-fill { transition: stroke-dashoffset .7s ease; }
    .hover-image { transition: opacity .3s ease, transform .3s ease; }
    .reveal-ready [data-reveal] { opacity: 0; transform: translateY(24px); transition: opacity .6s ease, transform .6s ease; transition-delay: var(--reveal-delay, 0ms); }
    .reveal-ready [data-reveal].is-visible { opacity: 1; transform: none; }
    @media (prefers-reduced-motion: reduce) {
      html { scroll-behavior: auto; }
      .scribble path, .ring-fill, .hover-image { transition: none; }
      ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
    }
  </style>
</head>
<body class="font-sans text-ink antialiased">
<% active_page = "index" %>
<% nav = { "catalog" => "Catalog", "monthly-box" => "Monthly box", "about" => "About", "contact" => "Contact" } %>
<% %w[EN ES FR DE].each do |lang| %>
<% end %>
<% { "index" => "Home" }.merge(nav).merge("cart" => "Cart").each do |slug, label| %>
<% active = active_page == slug %>
<% end %>
<% products = [["sea-salt-wash", "Sea salt face wash", "Cleanser", 18, "1620917669809-1af0497965de", "Cream tube of face wash on a white background"], ["cedar-oil", "Cedar beard oil", "Beard", 24, "1617897903246-719242758050", "Glass dropper bottle of oil on a wooden board with eucalyptus"], ["night-serum", "Night repair serum", "Serum", 32, "1608571423902-eed4a5ad8108", "Amber serum bottle on a wooden stand"], ["daily-lotion", "Daily light lotion", "Moisturiser", 26, "1631729371254-42c2892f0e6e", "Three white pump bottles beside a plant"], ["lip-balm", "Lip & brow balm", "Balm", 12, "1599305090598-fe179d501227", "Small open jar of balm on a stone surface"], ["olive-clay", "Olive clay bar", "Soap", 10, "1614806687007-2215a9db3b1c", "Stacked green olive soap bars with dried flowers"], ["lavender-bar", "Lavender bar", "Soap", 9, "1600857544200-b2f666a9a2ec", "Handmade lavender soap bars tied with string"], ["gel-mask", "Cooling gel mask", "Mask", 22, "1619451334792-150fd785ee74", "Hands holding an open jar of blue gel mask"]] %>
<% products.each do |id, name, kind, price, photo, alt| %>
<% end %>
<% products.each do |id, name, kind, price, photo, alt| %>
<% end %>
<% 4.times do |i| %>
<% end %>
<% ingredients = [["Sea salt", "Gently buffs and wakes up tired skin.", "1556760544-74068565f05c", "Hands applying oil from a small dropper bottle"], ["Cedar", "Warm, woody and calming for beard and scalp.", "1617897903246-719242758050", "Dropper bottle of oil on a wooden board"], ["Oat milk", "Soothes razor burn and redness.", "1596755389378-c31d21fd1273", "Person in a face mask holding half an avocado"], ["Green clay", "Draws out oil without stripping.", "1614806687007-2215a9db3b1c", "Green clay soap bars stacked on linen"], ["Rosehip", "Rich in vitamin A for overnight repair.", "1600428877878-1a0fd85beda8", "Serum bottle with a stone face roller and gua sha"]] %>
<% ingredients.each_with_index do |(name, text, photo, alt), index| %>
<% end %>
<% results = [[94, "said skin felt softer after two weeks"], [88, "noticed less razor burn by week three"], [100, "plastic-free outer packaging"]] %>
<% results.each do |value, label| %>
<% end %>
<% plans = [["monthly", "Every month", 29, "4 full-size items, swap anytime"], ["quarterly", "Every 3 months", 79, "12 items, ships as one bigger box"], ["yearly", "Once a year", 290, "2 months free and a travel kit"]] %>
<% plans.each_with_index do |(id, label, price, note), index| %>
<% end %>
<% reviews = [["Finally a face wash that doesn't leave my skin tight.", "Andre", "1500648767791-00dcc994a43e", "bg-white/70"], ["The beard oil smells like a forest after rain. My partner steals it.", "Tomás", "1507003211169-0a1dd7228f2d", "bg-peach"], ["Swapped two items this month in under a minute.", "Liam", "1539571696357-5a69c17a67c6", "bg-white/70"], ["Bought it for my brother, now we both subscribe. The gel mask is the hero.", "Nia", "1519699047748-de8e457a634e", "bg-cream"], ["Packaging is all paper and tins. Nothing to feel bad about.", "Marcus", "1506794778202-cad84cf45f1d", "bg-white/70"], ["Razor burn gone by week two.", "Ivo", "1531384441138-2736e62e0919", "bg-rust text-cream"]] %>
<!-- block:start reviews -->
  <section class="mx-auto max-w-[96rem] px-5 py-24 md:px-10">
    <div class="flex flex-wrap items-end justify-between gap-6">
      <h2 class="font-display text-5xl uppercase md:text-6xl">Overheard</h2>
      <p class="text-lg"><i class="fa-solid fa-star text-rust" aria-hidden="true"></i> 4.8 from 2,140 reviews</p>
    </div>
    <ul class="mt-12 gap-5 sm:columns-2 lg:columns-3">
      <% reviews.each_with_index do |(quote, name, photo, tone), index| %>
        <li class="mb-5 break-inside-avoid border-2 border-ink p-7 <%= tone %>" data-reveal style="--reveal-delay: <%= (index % 3) * 90 %>ms">
          <p class="font-display <%= quote.length < 50 ? 'text-4xl' : 'text-2xl' %> uppercase leading-tight">&ldquo;<%= quote %>&rdquo;</p>
          <p class="mt-6 flex items-center gap-3"><img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=96&h=96&q=70" alt="" class="h-10 w-10 rounded-full object-cover"><span class="font-bold"><%= name %></span><span class="text-sm opacity-70">· verified buyer</span></p>
        </li>
      <% end %>
    </ul>
  </section>
  <!-- block:end reviews -->
<script>
    const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
    const watchMotion = !reducedMotion && !navigator.webdriver && 'IntersectionObserver' in window;
    const store = { get: (key, fallback) => { try { return sessionStorage.getItem(key) ?? fallback; } catch (e) { return fallback; } }, set: (key, value) => { try { sessionStorage.setItem(key, value); } catch (e) {} } };

    document.querySelectorAll('[data-menu-toggle]').forEach((button) => {
      const menu = document.getElementById(button.getAttribute('aria-controls'));
      const close = menu.querySelector('[data-menu-close]');
      const set = (open) => { menu.hidden = !open; button.setAttribute('aria-expanded', String(open)); document.body.style.overflow = open ? 'hidden' : ''; (open ? close : button).focus(); };
      button.addEventListener('click', () => set(menu.hidden));
      close.addEventListener('click', () => set(false));
      document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !menu.hidden) set(false); });
    });

    document.querySelectorAll('[data-lang-toggle]').forEach((button) => {
      const menu = document.getElementById(button.getAttribute('aria-controls'));
      const set = (open) => { menu.hidden = !open; button.setAttribute('aria-expanded', String(open)); };
      button.addEventListener('click', () => set(menu.hidden));
      menu.querySelectorAll('[data-lang]').forEach((item) => item.addEventListener('click', () => { button.querySelector('[data-lang-label]').textContent = item.dataset.lang; set(false); button.focus(); }));
      document.addEventListener('click', (event) => { if (!button.parentElement.contains(event.target)) set(false); });
    });

    const cartCount = () => Number(store.get('saltmere-cart', '2'));
    const paintCart = () => document.querySelectorAll('[data-cart-count]').forEach((badge) => { badge.textContent = cartCount(); });
    const addToCart = (amount) => { store.set('saltmere-cart', String(cartCount() + amount)); paintCart(); };
    paintCart();
    document.querySelectorAll('[data-add-to-cart]').forEach((button) => button.addEventListener('click', () => {
      addToCart(Number(button.dataset.qty || 1));
      const toast = document.querySelector('[data-cart-toast]');
      if (toast) toast.textContent = `${button.dataset.name} added to your cart.`;
      const label = button.textContent;
      button.textContent = 'Added ✓';
      setTimeout(() => { button.textContent = label; }, 1400);
    }));

    document.querySelectorAll('[data-rail-root]').forEach((root) => {
      const track = root.querySelector('[data-rail-track]');
      const step = () => track.firstElementChild.getBoundingClientRect().width + 20;
      root.querySelector('[data-rail-prev]').addEventListener('click', () => track.scrollBy({ left: -step(), behavior: reducedMotion ? 'auto' : 'smooth' }));
      root.querySelector('[data-rail-next]').addEventListener('click', () => track.scrollBy({ left: step(), behavior: reducedMotion ? 'auto' : 'smooth' }));
    });

    document.querySelectorAll('[data-box-builder]').forEach((root) => {
      const max = Number(root.dataset.slots);
      const discount = Number(root.dataset.discount);
      const items = [...root.querySelectorAll('[data-box-item]')];
      const slots = [...root.querySelectorAll('[data-box-slot]')];
      const status = root.querySelector('[data-box-status]');
      const add = root.querySelector('[data-box-add]');
      let picked = [];
      const money = (value) => `$${value.toFixed(2).replace(/\.00$/, '')}`;
      const paint = () => {
        items.forEach((item) => item.setAttribute('aria-pressed', String(picked.includes(item))));
        slots.forEach((slot, i) => {
          const item = picked[i];
          slot.innerHTML = item ? `<img src="https://images.unsplash.com/photo-${item.dataset.photo}?auto=format&fit=crop&w=300&h=300&q=60" alt="${item.dataset.name}" class="h-full w-full object-cover">` : `Slot ${i + 1}`;
          slot.classList.toggle('border-solid', !!item);
        });
        const full = picked.reduce((sum, item) => sum + Number(item.dataset.price), 0);
        root.querySelector('[data-box-count]').textContent = picked.length;
        root.querySelector('[data-box-progress]').style.width = `${(picked.length / max) * 100}%`;
        root.querySelector('[data-box-full]').textContent = money(full);
        root.querySelector('[data-box-saving]').textContent = `−${money(full * discount)}`;
        root.querySelector('[data-box-total]').textContent = money(full * (1 - discount));
        add.disabled = picked.length !== max;
        status.textContent = picked.length === max ? 'Box full. Swap an item by tapping it again.' : `Pick ${max - picked.length} more to fill your box.`;
      };
      items.forEach((item) => item.addEventListener('click', () => {
        if (picked.includes(item)) picked = picked.filter((other) => other !== item);
        else if (picked.length < max) picked.push(item);
        else { status.textContent = 'Your box is full. Remove an item first.'; return; }
        paint();
      }));
      add.addEventListener('click', () => { addToCart(1); status.textContent = 'Monthly box added to your cart.'; });
      paint();
    });

    document.querySelectorAll('[data-hover-list]').forEach((root) => {
      const image = root.querySelector('[data-hover-image]');
      root.querySelectorAll('[data-hover-row]').forEach((row) => {
        const show = () => { image.src = row.dataset.image; image.alt = row.dataset.alt; image.style.opacity = '1'; };
        row.addEventListener('mouseenter', show);
        row.addEventListener('mousemove', (event) => { image.style.transform = `translate(${event.clientX + 24}px, ${event.clientY - 140}px) rotate(-3deg)`; });
        row.addEventListener('mouseleave', () => { image.style.opacity = '0'; });
      });
    });

    document.querySelectorAll('[data-rings]').forEach((root) => {
      const rings = root.querySelectorAll('[data-ring]');
      if (!watchMotion) return;
      const full = 326.73;
      rings.forEach((ring) => { ring.style.strokeDashoffset = full; });
      const observer = new IntersectionObserver((entries) => {
        if (!entries.some((entry) => entry.isIntersecting)) return;
        rings.forEach((ring) => { ring.style.strokeDashoffset = full * (1 - Number(ring.dataset.ring) / 100); });
        observer.disconnect();
      }, { threshold: 0.3 });
      observer.observe(root);
    });

    document.querySelectorAll('[data-membership]').forEach((root) => {
      root.addEventListener('change', (event) => {
        const plan = event.target;
        if (plan.name !== 'plan') return;
        root.querySelector('[data-plan-label]').textContent = plan.dataset.label;
        root.querySelector('[data-plan-note]').textContent = plan.dataset.note;
        root.querySelector('[data-plan-price]').textContent = plan.dataset.price;
      });
    });

    document.querySelectorAll('[data-catalog]').forEach((root) => {
      const grid = root.querySelector('[data-catalog-grid]');
      const items = [...grid.children];
      const count = root.querySelector('[data-catalog-count]');
      let kind = 'all';
      const apply = () => {
        const sort = root.querySelector('[data-catalog-sort]').value;
        const sorted = [...items].sort((a, b) => sort === 'low' ? a.dataset.price - b.dataset.price : sort === 'high' ? b.dataset.price - a.dataset.price : a.dataset.order - b.dataset.order);
        sorted.forEach((item) => { item.hidden = kind !== 'all' && item.dataset.kind !== kind; grid.append(item); });
        count.textContent = sorted.filter((item) => !item.hidden).length;
      };
      root.querySelectorAll('[data-kind-tab]').forEach((tab) => tab.addEventListener('click', () => {
        kind = tab.dataset.kindTab;
        root.querySelectorAll('[data-kind-tab]').forEach((other) => { other.setAttribute('aria-selected', String(other === tab)); other.classList.toggle('active', other === tab); });
        apply();
      }));
      root.querySelector('[data-catalog-sort]').addEventListener('change', apply);
      apply();
    });

    document.querySelectorAll('[data-product]').forEach((root) => {
      const main = root.querySelector('[data-product-main]');
      root.querySelectorAll('[data-product-thumb]').forEach((thumb) => thumb.addEventListener('click', () => {
        main.src = thumb.dataset.src; main.alt = thumb.dataset.alt;
        root.querySelectorAll('[data-product-thumb]').forEach((other) => other.setAttribute('aria-pressed', String(other === thumb)));
      }));
      const qty = root.querySelector('[data-qty]');
      const price = root.querySelector('[data-product-price]');
      const button = root.querySelector('[data-add-to-cart]');
      const paint = () => {
        const option = [...root.querySelectorAll('[name="purchase"]')].find((radio) => radio.checked);
        const total = Number(option.dataset.price) * Number(qty.value);
        price.textContent = `$${total}`;
        button.dataset.qty = qty.value;
      };
      root.querySelectorAll('[data-qty-step]').forEach((step) => step.addEventListener('click', () => { qty.value = Math.min(9, Math.max(1, Number(qty.value) + Number(step.dataset.qtyStep))); paint(); }));
      root.addEventListener('change', paint);
      paint();
    });

    document.querySelectorAll('[data-tabs]').forEach((tabs) => {
      const buttons = [...tabs.querySelectorAll('[role="tab"]')];
      const select = (tab) => {
        buttons.forEach((other) => { const on = other === tab; other.setAttribute('aria-selected', String(on)); other.tabIndex = on ? 0 : -1; other.classList.toggle('active', on); document.getElementById(other.getAttribute('aria-controls')).hidden = !on; });
      };
      buttons.forEach((tab, index) => {
        tab.addEventListener('click', () => select(tab));
        tab.addEventListener('keydown', (event) => {
          const move = { ArrowRight: 1, ArrowLeft: -1 }[event.key];
          if (!move) return;
          const next = buttons[(index + move + buttons.length) % buttons.length];
          select(next); next.focus();
        });
      });
    });

    document.querySelectorAll('[data-cart]').forEach((root) => {
      const threshold = Number(root.dataset.freeShipping);
      const paint = () => {
        const lines = [...root.querySelectorAll('[data-line]')];
        let subtotal = 0; let units = 0;
        lines.forEach((line) => { const qty = Number(line.querySelector('[data-line-qty]').value); units += qty; const total = qty * Number(line.dataset.price); subtotal += total; line.querySelector('[data-line-total]').textContent = `$${total}`; });
        const discount = root.dataset.promo === 'on' ? Math.round(subtotal * 0.1) : 0;
        const shipping = subtotal - discount >= threshold || subtotal === 0 ? 0 : 6;
        root.querySelector('[data-subtotal]').textContent = `$${subtotal}`;
        root.querySelector('[data-discount]').textContent = discount ? `−$${discount}` : '$0';
        root.querySelector('[data-shipping]').textContent = shipping ? `$${shipping}` : 'Free';
        root.querySelector('[data-total]').textContent = `$${subtotal - discount + shipping}`;
        const left = Math.max(0, threshold - (subtotal - discount));
        root.querySelector('[data-ship-progress]').style.width = `${Math.min(100, ((subtotal - discount) / threshold) * 100)}%`;
        root.querySelector('[data-ship-note]').textContent = left ? `Add $${left} more for free shipping.` : 'You get free shipping.';
        root.querySelector('[data-cart-empty]').hidden = lines.length > 0;
        store.set('saltmere-cart', String(units)); paintCart();
      };
      root.addEventListener('click', (event) => {
        const step = event.target.closest('[data-line-step]');
        const remove = event.target.closest('[data-line-remove]');
        if (step) { const input = step.closest('[data-line]').querySelector('[data-line-qty]'); input.value = Math.min(9, Math.max(1, Number(input.value) + Number(step.dataset.lineStep))); paint(); }
        if (remove) { remove.closest('[data-line]').remove(); paint(); }
      });
      const promo = root.querySelector('[data-promo-form]');
      promo.addEventListener('submit', (event) => {
        event.preventDefault();
        const code = promo.querySelector('[name="code"]').value.trim().toUpperCase();
        const status = promo.querySelector('[data-form-status]');
        if (code === 'SALT10') { root.dataset.promo = 'on'; status.textContent = 'SALT10 applied: 10% off.'; } else { status.textContent = 'That code does not work. Try SALT10.'; }
        paint();
      });
      paint();
    });

    document.querySelectorAll('[data-slot-picker]').forEach((root) => {
      const summary = root.querySelector('[data-slot-summary]');
      root.addEventListener('change', () => {
        const day = root.querySelector('[name="day"]:checked');
        const time = root.querySelector('[name="time"]:checked');
        summary.textContent = day && time ? `${day.dataset.label} at ${time.value}` : day ? `${day.dataset.label}, pick a time` : 'Pick a day and a time';
      });
    });

    document.querySelectorAll('[data-faq-index]').forEach((index) => {
      const links = [...index.querySelectorAll('[data-faq-link]')];
      const mark = (id) => links.forEach((link) => { const on = link.getAttribute('href') === `#${id}`; link.classList.toggle('border-rust', on); link.classList.toggle('font-bold', on); link.classList.toggle('border-transparent', !on); });
      links.forEach((link) => link.addEventListener('click', () => mark(link.getAttribute('href').slice(1))));
      if (!('IntersectionObserver' in window)) return;
      const observer = new IntersectionObserver((entries) => entries.forEach((entry) => { if (entry.isIntersecting) mark(entry.target.id); }), { rootMargin: '-30% 0px -60% 0px' });
      document.querySelectorAll('[data-faq-item]').forEach((item) => observer.observe(item));
    });

    document.querySelectorAll('[data-contact-form]').forEach((form) => {
      form.addEventListener('submit', (event) => {
        event.preventDefault();
        const status = form.querySelector('[data-form-status]');
        if (!form.checkValidity()) {
          status.textContent = form.dataset.error;
          form.querySelector(':invalid')?.focus();
          return;
        }
        status.textContent = form.dataset.success;
        form.reset();
      });
    });

    document.querySelectorAll('[data-timeline]').forEach((root) => {
      const fill = root.querySelector('[data-timeline-fill]');
      if (!watchMotion) return;
      const update = () => {
        const box = root.getBoundingClientRect();
        const progress = Math.min(1, Math.max(0, (innerHeight * 0.6 - box.top) / box.height));
        fill.style.transform = `scaleY(${progress})`;
      };
      update();
      addEventListener('scroll', update, { passive: true });
    });

    document.querySelectorAll('[data-current-year]').forEach((el) => { el.textContent = new Date().getFullYear(); });

    (() => {
      const items = document.querySelectorAll('[data-reveal]');
      if (!items.length || !watchMotion) return;
      document.documentElement.classList.add('reveal-ready');
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          entry.target.classList.add('is-visible');
          observer.unobserve(entry.target);
        });
      }, { rootMargin: '0px 0px -10% 0px' });
      items.forEach((item) => observer.observe(item));
    })();
  </script>
</body>
</html>

From this template

Saltmere screenshot

E-commerce

Saltmere

Unlimited access to all products

Unlock the entire library of premium templates and blocks. Renews automatically, cancel anytime.

$20 / 3 months or $60 / year
Get Unlimited Access