Free
Hero
Saltmere Hero
A hero block extracted from the Saltmere template, available in Tailwind CSS and Bootstrap.
0
2
A hero block extracted from the Saltmere template, available in Tailwind CSS and Bootstrap.
<!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 Hero | Tailwind CSS UI Block</title>
<meta name="description" content="A hero 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 %>
<!-- block:start hero -->
<section class="mx-auto max-w-[96rem] overflow-x-clip px-5 pb-16 pt-14 md:px-10 lg:pt-20">
<div class="grid grid-cols-1 items-start gap-14 lg:grid-cols-[1.15fr_1fr] lg:gap-10">
<div class="lg:pl-[8%]">
<h1 class="font-display text-[15vw] uppercase leading-none tracking-[0.02em] sm:text-8xl xl:text-[8.6rem]">
<span class="flex items-center justify-between gap-6 border-b-2 border-ink pb-4">Sharp
<svg class="h-14 w-11 shrink-0 sm:h-20 sm:w-16 md:h-32 md:w-24" viewBox="0 0 60 80" aria-hidden="true"><path d="M30 4 56 40 30 76 4 40Z" fill="none" stroke="#1d1b19" stroke-width="2"/><path d="M4 40h52L30 76Z" fill="#1d1b19" opacity=".15"/><circle cx="30" cy="28" r="7" fill="none" stroke="#1d1b19" stroke-width="1.6"/><path d="M30 35v14M24 44l6 5 6-5" fill="none" stroke="#1d1b19" stroke-width="1.6"/><path d="M56 40 30 76" stroke="#1d1b19" stroke-width="4"/></svg>
</span>
<span class="flex items-center justify-between gap-6 border-b-2 border-ink py-4">
<svg class="h-14 w-14 shrink-0 sm:h-20 sm:w-20 md:h-32 md:w-32" viewBox="0 0 80 80" aria-hidden="true"><defs><path id="badge-arc" d="M14 40a26 26 0 0 1 52 0"/></defs><circle cx="40" cy="40" r="34" fill="none" stroke="#1d1b19" stroke-width="2"/><text font-family="Space Grotesk" font-size="7" font-weight="700" letter-spacing="2" fill="#1d1b19"><textPath href="#badge-arc" startOffset="50%" text-anchor="middle">SEA · SALT · CO</textPath></text><circle cx="40" cy="34" r="5" fill="#1d1b19"/><path d="M8 48c6-4 10-4 16 0s10 4 16 0 10-4 16 0 10 4 16 0V60a34 34 0 0 1-64 0Z" fill="#1d1b19" opacity=".2"/><path d="M8 48c6-4 10-4 16 0s10 4 16 0 10-4 16 0 10 4 16 0" fill="none" stroke="#1d1b19" stroke-width="2"/></svg>
Steady
</span>
<span class="flex items-center justify-between gap-6 pt-4">Self
<svg class="h-14 w-11 shrink-0 sm:h-20 sm:w-16 md:h-32 md:w-24" viewBox="0 0 60 80" aria-hidden="true"><path d="M6 78V30a24 24 0 0 1 48 0v48Z" fill="none" stroke="#1d1b19" stroke-width="2"/><path d="M6 62h48v16H6Z" fill="#1d1b19" opacity=".15"/><path d="M18 40a12 12 0 0 1 24 0Z" fill="#1d1b19"/><path d="M16 44h28M18 48h24" stroke="#1d1b19" stroke-width="1.5"/><path d="M16 62V50M16 50l-5 3M16 50l5 3M44 62V50M44 50l-5 3M44 50l5 3" stroke="#1d1b19" stroke-width="1.5" fill="none"/></svg>
</span>
</h1>
</div>
<div class="relative mx-auto w-full max-w-md lg:mt-8">
<div class="relative">
<span class="absolute inset-x-0 -bottom-2 top-6 -rotate-[4deg] bg-sage md:top-10" aria-hidden="true"></span>
<img src="https://images.unsplash.com/photo-1531384441138-2736e62e0919?auto=format&fit=crop&w=800&h=950&q=80" alt="Smiling man with a beard in glasses and a dark turtleneck" class="relative aspect-[4/5] w-full object-cover mix-blend-normal">
<span class="relative block h-4 bg-ink" aria-hidden="true"></span>
<a href="monthly-box.html" class="absolute -right-3 -top-8 w-40 bg-rust p-5 text-cream shadow-xl transition hover:-translate-y-1 md:-right-10 md:w-48 md:p-6">
<span class="block font-display text-lg uppercase leading-tight md:text-xl">Monthly care box</span>
<span class="mt-3 block h-px w-12 bg-cream/70" aria-hidden="true"></span>
<span class="mt-4 flex items-start font-sans text-5xl font-medium md:text-6xl">29<span class="ml-1 text-lg">$</span></span>
</a>
</div>
</div>
</div>
<div class="mt-20 grid grid-cols-1 gap-10 lg:grid-cols-[1.15fr_1fr] lg:gap-10">
<div class="grid gap-8 sm:grid-cols-[auto_1fr] sm:gap-16 lg:pl-[8%]">
<p class="font-display text-3xl uppercase leading-tight">Care you've<br>long since<br>earned</p>
<p class="text-lg leading-relaxed sm:text-right sm:text-xl">Small-batch grooming and skincare, made to fit how you actually live. <strong>Honest formulas, no fuss</strong> <span class="inline-flex translate-y-[-3px] items-center gap-1 align-middle" aria-hidden="true"><span class="h-1.5 w-1.5 rounded-full bg-ink"></span><span class="h-1 w-14 bg-ink"></span></span></p>
</div>
<form data-contact-form data-error="Please add a valid email." data-success="You're in. We'll email you the day the first batch ships." class="mx-auto w-full max-w-md" novalidate>
<label for="preorder-email" class="font-display text-xl uppercase">Join the first batch</label>
<div class="group relative mt-4 flex items-center bg-white shadow-sm">
<input id="preorder-email" type="email" name="email" required autocomplete="email" placeholder="Your email for early access" class="min-w-0 flex-1 bg-transparent px-6 py-6 placeholder:text-ink/45 focus:outline-none">
<button type="submit" class="relative px-8 py-6 font-display text-2xl uppercase">Send
<svg class="scribble pointer-events-none absolute -inset-3" viewBox="0 0 140 100" preserveAspectRatio="none" aria-hidden="true"><path d="M110 12C70-2 14 14 10 50s48 46 86 40 38-36 24-58C110 16 86 10 70 12" fill="none" stroke="#a8443a" stroke-width="2.5" stroke-linecap="round"/></svg>
</button>
</div>
<p data-form-status class="mt-3 text-sm font-medium text-rust-dark" role="status" aria-live="polite"></p>
</form>
</div>
</section>
<!-- block:end hero -->
<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>
E-commerce
Unlock the entire library of premium templates and blocks. Renews automatically, cancel anytime.
Tailwind CSS · HTML download