Shelfline Announcement Announcement 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 Announcement

Shelfline Announcement

A announcement block extracted from the Shelfline template, available in Tailwind CSS and Bootstrap.

0 96
Shelfline Announcement preview
shelfline.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>Shelfline Announcement | Tailwind CSS UI Block</title>
  <meta name="description" content="A announcement block extracted from the Shelfline template, available in Tailwind CSS and Bootstrap.">
  <meta name="keywords" content="Shelfline, Tailwind CSS template, HTML website template, ecommerce template, online marketplace, multi-category store, electronics shop, deals countdown, responsive ecommerce design">
  <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="Shelfline | Tailwind CSS Marketplace Ecommerce Template">
  <meta property="og:description" content="A fresh, green Tailwind CSS marketplace template for multi-category online stores selling electronics, gaming, fashion, books and beauty, with deal countdowns, category pages, product pages, and a cart.">
  <meta property="og:image" content="">
  <meta property="og:image:alt" content="Shelfline template preview">

  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:url" content="">
  <meta name="twitter:title" content="Shelfline | Tailwind CSS Marketplace Ecommerce Template">
  <meta name="twitter:description" content="A fresh, green Tailwind CSS marketplace template for multi-category online stores selling electronics, gaming, fashion, books and beauty, with deal countdowns, category pages, product pages, and a cart.">
  <meta name="twitter:image" content="">
  <meta name="twitter:image:alt" content="Shelfline template preview">
  <script src="https://cdn.tailwindcss.com/3.4.17"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;0,400;0,700;0,900;1,700;1,900&amp;display=swap" rel="stylesheet">
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            forest: '#115e3b',
            leaf: '#22c55e',
            mint: '#dcfce7',
            smoke: '#f4f4f4',
            deal: '#ef3b3b'
          },
          fontFamily: {
            sans: ['Lato', 'ui-sans-serif', 'system-ui', 'sans-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; }
    .no-scrollbar { scrollbar-width: none; }
    .no-scrollbar::-webkit-scrollbar { display: none; }
    .btn-leaf { background: radial-gradient(circle at 35% 30%, #86efac, #22c55e 60%, #16a34a); }
    .marquee-track { animation: marquee 30s linear infinite; }
    .marquee:hover .marquee-track { animation-play-state: paused; }
    @keyframes marquee { to { transform: translateX(-50%); } }
    [data-hero-slide] { transition: opacity .6s ease; }
    [data-trend] [data-trend-card] { flex: 1 1 0; min-width: 0; }
    [data-trend] [data-trend-card][data-expanded="true"] { flex-grow: 2.2; }
    [data-trend] [data-trend-info], [data-trend] [data-trend-buy] { opacity: 0; transition: opacity .3s ease; }
    [data-trend] [data-trend-card][data-expanded="true"] [data-trend-info], [data-trend] [data-trend-card][data-expanded="true"] [data-trend-buy] { opacity: 1; }
    [data-trend] [data-trend-card][data-expanded="true"] [data-trend-go] { display: none; }
    [data-trend] [data-trend-card]:not([data-expanded="true"]) [data-trend-buy] { display: none; }
    @media (max-width: 767px) {
      [data-trend] { overflow-x: auto; scroll-snap-type: x mandatory; }
      [data-trend] [data-trend-card] { flex: 0 0 70%; scroll-snap-align: start; }
      [data-trend] [data-trend-info], [data-trend] [data-trend-buy] { opacity: 1; }
    }
    .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; }
      .marquee-track { animation: none; }
      [data-hero-slide] { transition: none; }
      ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
    }
  </style>
</head>
<body class="bg-white font-sans text-neutral-900 antialiased overflow-x-hidden">
<%
  categories = [
    ["electronics", "Electronics"], ["gaming", "Video Games"], ["movies-tv", "Movies & TV"], ["toys-collection", "Toys & Collection"],
    ["books", "Books"], ["fashion", "Fashion"], ["winter", "Winter"], ["health-beauty", "Health & Beauty"], ["luxury-item", "Luxury Item"]
  ]
  products = [
    { slug: "pulse-portable-speaker", name: "Pulse Portable Speaker", category: "electronics", price: 89, was: 119, image: "photo-1547052178-7f2c5a20c332", alt: "Grey fabric portable speaker with a wrist strap on a white surface" },
    { slug: "aura-mini-smart-speaker", name: "Aura Mini Smart Speaker", category: "electronics", price: 99, was: 129, image: "photo-1586078875290-c22eb791ad5d", alt: "Compact white smart speaker on a pale background" },
    { slug: "halo-studio-headphones", name: "Halo Studio Headphones", category: "electronics", price: 149, was: 199, image: "photo-1700087151960-178ea946e608", alt: "White and black over-ear headphones on a peach background" },
    { slug: "nova-x-ultra-phone", name: "Nova X Ultra 12GB+256GB", category: "electronics", price: 699, was: 849, image: "photo-1727941035071-910fd07135bb", alt: "Dark smartphone lying face down showing its camera bar" },
    { slug: "drift-wireless-earbuds", name: "Drift Wireless Earbuds", category: "electronics", price: 59, was: 79, image: "photo-1757168120889-4317e57a4849", alt: "Pair of black wireless earbuds on a yellow background" },
    { slug: "tempo-over-ear-headphones", name: "Tempo Over-Ear Headphones", category: "electronics", price: 119, was: 159, image: "photo-1779914944094-18c5724dad27", alt: "Black over-ear headphones resting on piano keys against yellow" },
    { slug: "neon-wireless-controller", name: "Neon Wireless Controller", category: "gaming", price: 64, was: 85, image: "photo-1612287230202-1ff1d85d1bdf", alt: "Wireless game controller lit by cyan and magenta light" },
    { slug: "raid-gaming-headset", name: "Raid Gaming Headset", category: "gaming", price: 79, was: 105, image: "photo-1610041321327-b794c052db27", alt: "Black and red wired gaming headset on a desk" },
    { slug: "keyline-mechanical-keyboard", name: "Keyline Mechanical Keyboard", category: "gaming", price: 109, was: 139, image: "photo-1618384887929-16ec33fab9ef", alt: "Compact black mechanical keyboard with orange accent keys on marble" },
    { slug: "lumen-home-projector", name: "Lumen Home Projector", category: "movies-tv", price: 329, was: 419, image: "photo-1535016120720-40c646be5580", alt: "Home projector beaming light in a dark red room" },
    { slug: "movie-night-popcorn-kit", name: "Movie Night Popcorn Kit", category: "movies-tv", price: 24, was: 32, image: "photo-1691480213129-106b2c7d1ee8", alt: "Striped yellow and white bucket overflowing with popcorn" },
    { slug: "cuddle-teddy-bear", name: "Cuddle Teddy Bear", category: "toys-collection", price: 29, was: 39, image: "photo-1556012018-50c5c0da73bf", alt: "Soft brown teddy bear with a red bow" },
    { slug: "brick-builder-1000", name: "Brick Builder 1000", category: "toys-collection", price: 45, was: 60, image: "photo-1587654780291-39c9404d746b", alt: "Pile of colourful plastic building bricks" },
    { slug: "classics-hardcover-set", name: "Classics Hardcover Set", category: "books", price: 54, was: 72, image: "photo-1520467795206-62e33627e6ce", alt: "Stack of colourful hardcover books" },
    { slug: "weekend-reads-bundle", name: "Weekend Reads Bundle", category: "books", price: 32, was: 42, image: "photo-1602396111763-06556dd0063a", alt: "Stack of pale paperback books on a white surface" },
    { slug: "everyday-cotton-tee", name: "Everyday Cotton Tee", category: "fashion", price: 22, was: 30, image: "photo-1523381294911-8d3cead13475", alt: "Row of sage green t-shirts on wooden hangers" },
    { slug: "mint-canvas-sneakers", name: "Mint Canvas Sneakers", category: "fashion", price: 58, was: 75, image: "photo-1564518125914-014ebd5f0346", alt: "Pair of mint canvas low-top sneakers on white" },
    { slug: "cloud-fleece-hoodie", name: "Cloud Fleece Hoodie", category: "winter", price: 48, was: 64, image: "photo-1620799140188-3b2a02fd9a77", alt: "White fleece hoodie hanging beside light blue jeans" },
    { slug: "quilted-puffer-jacket", name: "Quilted Puffer Jacket", category: "winter", price: 139, was: 185, image: "photo-1614031679232-0dae776a72ee", alt: "Man in a black quilted puffer jacket against a concrete wall" },
    { slug: "chunky-knit-beanie", name: "Chunky Knit Beanie", category: "winter", price: 19, was: 26, image: "photo-1544967919-44c1ef2f9e7a", alt: "Grey chunky knit beanie with knitting needles and dried leaves" },
    { slug: "calming-face-ampoule", name: "Calming Face Ampoule", category: "health-beauty", price: 36, was: 48, image: "photo-1713768704571-6aeb0d0e5105", alt: "Amber dropper bottle of face ampoule on an orange background" },
    { slug: "golden-cleansing-oil", name: "Golden Cleansing Oil", category: "health-beauty", price: 28, was: 37, image: "photo-1741896135512-084b251887f7", alt: "Clear bottle of golden cleansing oil with a dropper on pink" },
    { slug: "midnight-steel-watch", name: "Midnight Steel Watch", category: "luxury-item", price: 420, was: 560, image: "photo-1636639818651-d97365346a5c", alt: "Steel wristwatch with a deep blue dial on black" },
    { slug: "heritage-leather-satchel", name: "Heritage Leather Satchel", category: "luxury-item", price: 210, was: 280, image: "photo-1605733513597-a8f8341084e6", alt: "Grey leather satchel with gold buckles on a lilac surface" },
    { slug: "rose-crystal-perfume", name: "Rose Crystal Perfume", category: "luxury-item", price: 96, was: 128, image: "photo-1543422655-ac1c6ca993ed", alt: "Faceted glass perfume bottle resting on blush silk" }
  ]
  photo = ->(id, width = 700) { "https://images.unsplash.com/#{id}?auto=format&fit=crop&w=#{width}&q=80" }
  find = ->(slug) { products.find { |product| product[:slug] == slug } }
  nav_links = [["Home", "index.html"], ["Movies & TV", "movies-tv.html"], ["Toys & Collection", "toys-collection.html"], ["Books", "books.html"], ["Fashion", "fashion.html"], ["Electronics", "electronics.html"], ["Winter", "winter.html"], ["Health & Beauty", "health-beauty.html"], ["Luxury Item", "luxury-item.html"], ["Blogs", "blogs.html"]]
  slides = [
    { eyebrow: "Deal of the week", title: "Fresh drops<br>winter savings", text: "Rich sound, deep bass and", off: "30%", tail: "off. Your new favourite soundtrack starts now.", product: "halo-studio-headphones", href: "halo-studio-headphones.html" },
    { eyebrow: "Just landed", title: "Play longer<br>play louder", text: "Low-latency control with", off: "25%", tail: "off every gaming accessory this week.", product: "neon-wireless-controller", href: "gaming.html" },
    { eyebrow: "Gift edit", title: "Timeless gifts<br>for less", text: "Steel watches and leather goods at", off: "25%", tail: "off while stock lasts.", product: "midnight-steel-watch", href: "luxury-item.html" }
  ]
  tiles = [
    ["Shoes", "fashion.html", "photo-1564518125914-014ebd5f0346", "Mint canvas sneakers", "#8fdcc8"],
    ["Toys & Collection", "toys-collection.html", "photo-1556012018-50c5c0da73bf", "Brown teddy bear with a red bow", "#aebcf0"],
    ["Movies & TV", "movies-tv.html", "photo-1691480213129-106b2c7d1ee8", "Striped bucket of popcorn", "#f7aba3"],
    ["Fashion", "fashion.html", "photo-1523381294911-8d3cead13475", "Green t-shirts on hangers", "#e0da8e"],
    ["Electronics", "electronics.html", "photo-1700087151960-178ea946e608", "White over-ear headphones", "#e4b3c8"],
    ["Winter", "winter.html", "photo-1620799140188-3b2a02fd9a77", "White fleece hoodie", "#9ed4b1"],
    ["Books", "books.html", "photo-1520467795206-62e33627e6ce", "Stack of hardcover books", "#f5cf94"],
    ["Health & Beauty", "health-beauty.html", "photo-1713768704571-6aeb0d0e5105", "Amber serum bottle", "#c9b8f0"]
  ]
  for_you = %w[pulse-portable-speaker aura-mini-smart-speaker lumen-home-projector halo-studio-headphones nova-x-ultra-phone keyline-mechanical-keyboard cuddle-teddy-bear raid-gaming-headset].map { |slug| find.call(slug) }
  trending = [
    { name: "Linen Band Collar Shirt", category: "fashion", price: 60, image: "photo-1627686011747-74adda3d2343", alt: "Man in an off-white linen shirt outdoors", href: "fashion.html" },
    { name: "Shearling Flight Jacket", category: "winter", price: 120, image: "photo-1627637454030-5ddd536e06e5", alt: "Man in a brown shearling flight jacket against a concrete wall", href: "winter.html" },
    { name: "Washed Overshirt", category: "fashion", price: 66, image: "photo-1719418271955-79273259772d", alt: "Man in a grey overshirt against a white wall", href: "fashion.html" },
    { name: "Quilted Puffer Jacket", category: "winter", price: 139, image: "photo-1614031679232-0dae776a72ee", alt: "Man in a black quilted puffer jacket against a concrete wall", href: "quilted-puffer-jacket.html" },
    { name: "Pleated Work Trousers", category: "fashion", price: 72, image: "photo-1540924782561-3fc182603b86", alt: "Man in stone pleated trousers and braces", href: "fashion.html" },
    { name: "Mint Canvas Sneakers", category: "fashion", price: 58, image: "photo-1564518125914-014ebd5f0346", alt: "Pair of mint canvas low-top sneakers", href: "mint-canvas-sneakers.html" },
    { name: "Halo Studio Headphones", category: "electronics", price: 149, image: "photo-1700087151960-178ea946e608", alt: "White and black over-ear headphones", href: "halo-studio-headphones.html" },
    { name: "Calming Face Ampoule", category: "health-beauty", price: 36, image: "photo-1713768704571-6aeb0d0e5105", alt: "Amber dropper bottle of face ampoule", href: "calming-face-ampoule.html" }
  ]
  trend_tabs = [["fashion", "Fashion"], ["winter", "Winter"], ["electronics", "Electronic"], ["health-beauty", "Health & Beauty"]]
  brands = [["Voltra", "fa-bolt", "#f97316"], ["Orbix", "fa-circle-nodes", "#111827"], ["Pinepeak", "fa-mountain", "#312e81"], ["Lumio", "fa-lightbulb", "#111827"], ["Kestra", "fa-feather", "#dc2626"], ["Norda", "fa-compass", "#0f766e"], ["Wavely", "fa-wave-square", "#2563eb"], ["Cubik", "fa-cube", "#65a30d"], ["Fernly", "fa-leaf", "#15803d"], ["Tallo", "fa-star", "#ca8a04"]]
  gaming = %w[neon-wireless-controller raid-gaming-headset keyline-mechanical-keyboard drift-wireless-earbuds].map { |slug| find.call(slug) }
  popular_tabs = [["all", "All Product"], ["electronics", "Electronic"], ["gaming", "Video Games"], ["books", "Books"], ["toys-collection", "Kids & Baby"], ["luxury-item", "Luxury"]]
  popular = %w[drift-wireless-earbuds tempo-over-ear-headphones classics-hardcover-set brick-builder-1000 midnight-steel-watch neon-wireless-controller weekend-reads-bundle heritage-leather-satchel rose-crystal-perfume raid-gaming-headset].map { |slug| find.call(slug) }
  deals = [["aura-mini-smart-speaker", "25%"], ["nova-x-ultra-phone", "30%"], ["lumen-home-projector", "25%"]].map { |slug, off| [find.call(slug), off] }
  posts = [
    { slug: "future-of-online-shopping", title: "Where online shopping goes next", text: "How smarter search, better photos and honest reviews are changing the way we fill a basket.", image: "photo-1760565030283-d199530b41bc", alt: "Person holding colourful shopping bags with a thumbs up" },
    { slug: "shopping-on-the-go", title: "Shopping on the go is the norm", text: "Most orders now start on a phone. Here is how we made every step faster on a small screen.", image: "photo-1625669981647-dc396c990a6d", alt: "Man holding a black smartphone" },
    { slug: "inside-a-great-product-page", title: "Inside a product page that sells", text: "Clear photos, real sizing and fast delivery dates: the details that turn browsing into buying.", image: "photo-1605733513597-a8f8341084e6", alt: "Grey leather satchel with gold buckles on a lilac surface" }
  ]
  faqs = [
    ["How fast is delivery?", "Orders placed before 3pm ship the same day. Standard delivery takes 2–4 working days and express arrives next day in most cities."],
    ["Can I return an item?", "Yes. Most items can be returned within 30 days in their original condition. Start a return from your account and print the free label."],
    ["Which payment methods do you accept?", "All major cards, digital wallets and pay-later options at checkout. Every payment is encrypted end to end."],
    ["Do members get extra discounts?", "Members save up to 50% during exclusive monthly events and get free express delivery on every order."],
    ["Can I track my order?", "You will get a tracking link by email and text as soon as your parcel leaves our warehouse."]
  ]
  for_you_grid = { id: "for-you", title: "Picked for you", items: for_you, tabs: nil, link: "electronics.html", link_label: "See all products" }
  popular_grid = { id: "most-popular", title: "Most popular", items: popular, tabs: popular_tabs, link: "electronics.html", link_label: "See all products" }
%>
<!-- block:start announcement -->
  <div id="announcement" class="bg-forest text-[13px] text-white">
    <div class="mx-auto flex max-w-[1440px] items-center justify-center gap-6 px-5 py-2 md:justify-between md:px-[60px]">
      <nav class="hidden gap-7 md:flex" aria-label="Company">
        <a href="contact.html" class="hover:text-mint">About</a>
        <a href="contact.html#privacy" class="hover:text-mint">Privacy Policy</a>
        <a href="contact.html#terms" class="hover:text-mint">Terms &amp; Conditions</a>
      </nav>
      <p>Members save up to 50% all month long</p>
      <a href="tel:+14805550103" class="hidden items-center gap-2 hover:text-mint md:inline-flex"><i class="fa-solid fa-phone text-[11px]"></i> (480) 555-0103</a>
    </div>
  </div>
  <!-- block:end announcement -->
<script>
    document.querySelectorAll("[data-menu-toggle]").forEach((button) => {
      const menu = document.getElementById(button.getAttribute("aria-controls"));
      if (!menu) return;
      button.addEventListener("click", () => {
        const open = button.getAttribute("aria-expanded") === "true";
        button.setAttribute("aria-expanded", String(!open));
        menu.classList.toggle("hidden", open);
      });
    });

    (() => {
      const read = () => { try { return Number(localStorage.getItem("shelfline-cart") ?? 2); } catch (e) { return 2; } };
      const write = (value) => { try { localStorage.setItem("shelfline-cart", String(value)); } catch (e) {} };
      const paint = () => document.querySelectorAll("[data-cart-count]").forEach((el) => { el.textContent = read(); });
      window.shelflineCart = { read, write, paint };
      paint();
      document.addEventListener("click", (event) => {
        const add = event.target.closest("[data-add-cart]");
        if (add) {
          write(read() + 1);
          paint();
          add.animate([{ transform: "scale(1)" }, { transform: "scale(1.15)" }, { transform: "scale(1)" }], { duration: 300 });
        }
        const wish = event.target.closest("[data-wish]");
        if (wish) {
          const saved = wish.getAttribute("aria-pressed") !== "true";
          wish.setAttribute("aria-pressed", String(saved));
          const icon = wish.querySelector("[data-wish-icon]");
          if (icon) { icon.classList.toggle("fa-solid", saved); icon.classList.toggle("fa-regular", !saved); }
        }
      });
    })();

    document.querySelectorAll("[data-hero]").forEach((hero) => {
      const slides = [...hero.querySelectorAll("[data-hero-slide]")];
      const dots = [...hero.querySelectorAll("[data-hero-dot]")];
      let index = 0;
      let timer;
      const show = (next) => {
        index = (next + slides.length) % slides.length;
        slides.forEach((slide, i) => {
          slide.classList.toggle("opacity-0", i !== index);
          slide.classList.toggle("pointer-events-none", i !== index);
          slide.setAttribute("aria-hidden", String(i !== index));
        });
        dots.forEach((dot, i) => dot.setAttribute("aria-current", String(i === index)));
      };
      const start = () => {
        if (navigator.webdriver || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
        clearInterval(timer);
        timer = setInterval(() => show(index + 1), 6000);
      };
      dots.forEach((dot, i) => dot.addEventListener("click", () => { show(i); start(); }));
      hero.addEventListener("mouseenter", () => clearInterval(timer));
      hero.addEventListener("mouseleave", start);
      start();
    });

    document.querySelectorAll("[data-scroller]").forEach((scroller) => {
      const track = scroller.querySelector("[data-scroll-track]");
      const step = () => (track.firstElementChild?.offsetWidth || 200) + 18;
      scroller.querySelector("[data-scroll-prev]")?.addEventListener("click", () => track.scrollBy({ left: -step() * 2 }));
      scroller.querySelector("[data-scroll-next]")?.addEventListener("click", () => {
        if (track.scrollLeft >= track.scrollWidth - track.clientWidth - 4) return track.scrollTo({ left: 0 });
        track.scrollBy({ left: step() * 2 });
      });
    });

    document.querySelectorAll("[data-trend-section]").forEach((section) => {
      const cards = [...section.querySelectorAll("[data-trend-card]")];
      const tabs = [...section.querySelectorAll("[data-trend-tab]")];
      const expand = (card) => cards.forEach((other) => { other.dataset.expanded = String(other === card); });
      const filter = (value) => {
        tabs.forEach((tab) => tab.setAttribute("aria-selected", String(tab.dataset.trendTab === value)));
        const visible = cards.filter((card) => card.dataset.category === value || (value === "fashion" && card.dataset.category === "winter"));
        const shown = (visible.length >= 2 ? visible : cards).slice(0, 5);
        cards.forEach((card) => { card.hidden = !shown.includes(card); });
        expand(shown[Math.min(1, shown.length - 1)]);
      };
      tabs.forEach((tab) => tab.addEventListener("click", () => filter(tab.dataset.trendTab)));
      cards.forEach((card) => {
        card.addEventListener("mouseenter", () => { if (matchMedia("(hover: hover)").matches) expand(card); });
        card.addEventListener("focusin", () => expand(card));
      });
      filter(tabs[0]?.dataset.trendTab || "fashion");
    });

    document.querySelectorAll("[data-grid]").forEach((grid) => {
      const tabs = [...grid.querySelectorAll("[data-grid-tab]")];
      if (!tabs.length) return;
      const items = [...grid.querySelectorAll("[data-grid-item]")];
      tabs.forEach((tab) => tab.addEventListener("click", () => {
        const value = tab.dataset.gridTab;
        tabs.forEach((other) => other.setAttribute("aria-selected", String(other === tab)));
        let shown = 0;
        items.forEach((item) => {
          const match = value === "all" || item.dataset.category === value;
          item.hidden = !(match && shown < 8);
          if (match && shown < 8) shown += 1;
        });
      }));
    });

    document.querySelectorAll("[data-countdown]").forEach((box) => {
      if (navigator.webdriver) return;
      const end = Date.now() + Number(box.dataset.hours || 48) * 3600 * 1000;
      const units = { days: 86400, hours: 3600, mins: 60, secs: 1 };
      const tick = () => {
        let left = Math.max(0, Math.floor((end - Date.now()) / 1000));
        Object.entries(units).forEach(([key, size]) => {
          const el = box.querySelector(`[data-unit="${key}"]`);
          if (el) el.textContent = String(Math.floor(left / size)).padStart(2, "0");
          left %= size;
        });
      };
      tick();
      setInterval(tick, 1000);
    });

    document.querySelectorAll("[data-accordion]").forEach((accordion) => {
      const items = [...accordion.querySelectorAll("[data-accordion-item]")];
      items.forEach((item) => {
        const button = item.querySelector("[data-accordion-button]");
        const panel = document.getElementById(button.getAttribute("aria-controls"));
        button.addEventListener("click", () => {
          const open = button.getAttribute("aria-expanded") !== "true";
          items.forEach((other) => {
            const otherButton = other.querySelector("[data-accordion-button]");
            const isTarget = other === item && open;
            otherButton.setAttribute("aria-expanded", String(isTarget));
            other.setAttribute("aria-expanded", String(isTarget));
            document.getElementById(otherButton.getAttribute("aria-controls")).hidden = !isTarget;
          });
        });
      });
    });

    document.querySelectorAll("[data-newsletter]").forEach((form) => {
      form.addEventListener("submit", (event) => {
        event.preventDefault();
        const note = form.parentElement.querySelector("[data-newsletter-note]");
        if (note) note.hidden = false;
        form.reset();
      });
    });

    (() => {
      const items = document.querySelectorAll("[data-reveal]");
      const skip = navigator.webdriver || !("IntersectionObserver" in window) || matchMedia("(prefers-reduced-motion: reduce)").matches;
      if (!items.length || skip) 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

Shelfline screenshot Premium

E-commerce

Shelfline

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