Saltmere Navbar Navbar 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 Navbar

Saltmere Navbar

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

0 1
Saltmere Navbar 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 Navbar | Tailwind CSS UI Block</title>
  <meta name="description" content="A navbar 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" } %>
<!-- block:start navbar -->
  <header class="sticky top-0 z-40 border-b border-ink bg-peach/80 backdrop-blur">
    <nav class="flex h-16 items-stretch md:h-[5.5rem]" aria-label="Main">
      <button type="button" data-menu-toggle aria-controls="site-menu" aria-expanded="false" aria-label="Open menu" class="grid w-16 shrink-0 place-items-center border-r border-ink transition hover:bg-ink hover:text-peach md:w-[5.5rem]"><i class="fa-solid fa-bars text-xl" aria-hidden="true"></i></button>
      <a href="contact.html" <%= 'aria-current="page"'.html_safe if active_page == "contact" %> class="hidden items-center border-r border-ink px-11 font-medium transition hover:bg-ink hover:text-peach md:flex <%= 'underline underline-offset-8' if active_page == 'contact' %>">Contact</a>
      <a href="index.html" class="flex flex-1 items-center justify-center font-sans text-lg font-bold uppercase tracking-[0.08em] md:text-2xl" aria-label="Saltmere home">Saltmere</a>
      <a href="catalog.html" <%= 'aria-current="page"'.html_safe if active_page == "catalog" %> class="hidden items-center border-l border-ink px-11 font-medium transition hover:bg-ink hover:text-peach md:flex <%= 'underline underline-offset-8' if active_page == 'catalog' %>">Catalog</a>
      <a href="cart.html" <%= 'aria-current="page"'.html_safe if active_page == "cart" %> class="relative flex w-16 shrink-0 items-center justify-center border-l border-ink transition hover:bg-ink hover:text-peach md:w-[5.5rem]" aria-label="Cart"><i class="fa-solid fa-bag-shopping text-xl" aria-hidden="true"></i><span data-cart-count class="absolute right-2 top-2 grid h-5 min-w-5 place-items-center rounded-full bg-rust px-1 text-[11px] font-bold text-cream md:right-4 md:top-4">2</span></a>
      <div class="relative hidden border-l border-ink sm:block">
        <button type="button" data-lang-toggle aria-controls="lang-menu" aria-expanded="false" class="flex h-full items-center gap-2 px-6 font-medium transition hover:bg-ink hover:text-peach"><span data-lang-label>EN</span><i class="fa-solid fa-chevron-down text-xs" aria-hidden="true"></i></button>
        <ul id="lang-menu" hidden class="absolute right-0 top-full grid min-w-full border border-ink bg-cream">
          <% %w[EN ES FR DE].each do |lang| %><li><button type="button" data-lang="<%= lang %>" class="w-full px-6 py-3 text-left font-medium hover:bg-ink hover:text-cream"><%= lang %></button></li><% end %>
        </ul>
      </div>
    </nav>
  </header>
  <div id="site-menu" hidden class="fixed inset-0 z-50 grid bg-ink text-cream lg:grid-cols-[1.3fr_1fr]">
    <div class="flex flex-col overflow-y-auto px-6 py-6 md:px-16">
      <button type="button" data-menu-close aria-label="Close menu" class="grid h-12 w-12 place-items-center self-end border border-cream/40 transition hover:bg-cream hover:text-ink"><i class="fa-solid fa-xmark text-xl" aria-hidden="true"></i></button>
      <ul class="my-auto grid gap-2 py-10">
        <% { "index" => "Home" }.merge(nav).merge("cart" => "Cart").each do |slug, label| %>
          <% active = active_page == slug %>
          <li><a href="<%= slug %>.html" <%= 'aria-current="page"'.html_safe if active %> class="group flex items-baseline gap-5 font-display text-5xl uppercase transition md:text-7xl <%= active ? 'text-peach' : 'hover:text-peach' %>"><span class="font-sans text-sm text-cream/50"><%= active ? '●' : '→' %></span><%= label %></a></li>
        <% end %>
      </ul>
      <p class="text-sm text-cream/60">hello@saltmere.shop · Free shipping over $60</p>
    </div>
    <img src="https://images.unsplash.com/photo-1503951914875-452162b0f3f1?auto=format&fit=crop&w=1000&q=80" alt="Barber giving a client a hot-towel shave" class="hidden h-full w-full object-cover lg:block">
  </div>
  <!-- block:end navbar -->
<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