Tressa UI Template - Let's templates
Tressa preview 1
Free
0 2

Tressa

By Mina Ortiz

Tressa is a clean, minimal hair salon template with numbered services, a work gallery, stylist profiles, service packages, client reviews and booking.

Live Demo
tressa-tailwind.html
<%# letstemplates: access=free category=health-fitness %>
<!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>Tressa | Tailwind CSS Health & Fitness Template</title>
  <meta name="description" content="Tressa is a clean, minimal Tailwind CSS hair salon template with numbered services, a work gallery, stylist profiles, service packages, client reviews and booking.">
  <meta name="keywords" content="Tressa, Tailwind CSS template, HTML website template, hair salon template, hair studio, hairdresser website, balayage, keratin treatment, salon booking">
  <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="Tressa | Tailwind CSS Health & Fitness Template">
  <meta property="og:description" content="A clean, minimal Tailwind CSS hair salon template with numbered services, a work gallery, stylist profiles and service packages.">
  <meta property="og:image" content="">
  <meta property="og:image:alt" content="Tressa template preview">

  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:url" content="">
  <meta name="twitter:title" content="Tressa | Tailwind CSS Health & Fitness Template">
  <meta name="twitter:description" content="A clean, minimal Tailwind CSS hair salon template with numbered services, a work gallery, stylist profiles and service packages.">
  <meta name="twitter:image" content="">
  <meta name="twitter:image:alt" content="Tressa 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=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: { ink: '#111827', mist: '#f8f9fb', line: '#e5e7eb' },
          fontFamily: { sans: ['Inter', '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; }
    .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; }
      ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
    }
  </style>
</head>
<body class="bg-white font-sans text-ink antialiased">
  <% active_page = "index" %>
  <!-- block:start navbar -->
  <header class="sticky top-0 z-40 border-b border-line/70 bg-white/95 backdrop-blur">
    <nav class="mx-auto flex max-w-6xl items-center justify-between gap-6 px-5 py-5 md:px-8" aria-label="Main">
      <div class="flex items-center gap-12">
        <a href="index.html" class="text-xl font-medium tracking-tight">Tressa Studio</a>
        <ul class="hidden items-center gap-10 text-[15px] text-ink/70 lg:flex">
          <% { "services" => "Services", "work" => "Work", "team" => "Team", "contact" => "Contact" }.each do |slug, label| %>
            <% active = active_page == slug %>
            <li><a href="<%= slug %>.html" <%= 'aria-current="page"'.html_safe if active %> class="<%= active ? 'font-medium text-ink underline underline-offset-8' : 'hover:text-ink' %>"><%= label %></a></li>
          <% end %>
        </ul>
      </div>
      <div class="flex items-center gap-3">
        <a href="contact.html" class="hidden border border-ink px-5 py-2.5 text-[15px] font-medium transition hover:bg-ink hover:text-white sm:inline-block">Book Appointment</a>
        <button type="button" data-menu-toggle aria-controls="mobile-menu" aria-expanded="false" aria-label="Open menu" class="grid h-11 w-11 place-items-center border border-line lg:hidden"><i class="fa-solid fa-bars" aria-hidden="true"></i></button>
      </div>
    </nav>
    <div id="mobile-menu" class="hidden border-t border-line px-5 pb-6 lg:hidden">
      <ul class="grid gap-1 pt-3 text-lg">
        <% { "index" => "Home", "services" => "Services", "work" => "Work", "team" => "Team", "pricing" => "Pricing", "contact" => "Contact" }.each do |slug, label| %>
          <% active = active_page == slug %>
          <li><a href="<%= slug %>.html" <%= 'aria-current="page"'.html_safe if active %> class="block px-2 py-2 <%= active ? 'bg-mist font-medium' : 'text-ink/70' %>"><%= label %></a></li>
        <% end %>
      </ul>
    </div>
  </header>
  <!-- block:end navbar -->

  <!-- block:start hero -->
  <section id="intro" class="mx-auto grid max-w-6xl gap-12 px-5 pb-20 pt-14 md:px-8 lg:grid-cols-2 lg:items-center lg:pb-28">
    <div>
      <p class="text-sm uppercase tracking-[0.15em] text-ink/60">Calm hair studio</p>
      <h1 data-reveal class="mt-6 text-5xl font-medium leading-[1.08] tracking-tight md:text-6xl lg:text-7xl">Thoughtful Cuts, Colour &amp; Care</h1>
      <p data-reveal class="mt-8 max-w-lg text-lg leading-relaxed text-ink/70">Unhurried hair care in a bright, quiet space. Our stylists listen first, then shape a look that suits your hair and your routine.</p>
      <div data-reveal class="mt-10 flex flex-wrap items-center gap-6">
        <a href="contact.html" class="inline-block bg-ink px-8 py-4 font-medium text-white transition hover:bg-black">Book Now</a>
        <a href="services.html" class="inline-flex items-center gap-2 font-medium hover:underline">View Services <i class="fa-solid fa-arrow-right text-sm" aria-hidden="true"></i></a>
      </div>
      <dl class="mt-12 flex gap-12 border-t border-line pt-8">
        <% [["12", "+", "Years"], ["6", "K+", "Guests"], ["4.9", "", "Rating"]].each do |value, suffix, label| %>
          <div class="flex flex-col"><dt class="order-2 mt-1 text-sm text-ink/60"><%= label %></dt><dd class="order-1 text-3xl font-light"><span <%= "data-count-to=\"#{value}\" data-count-suffix=\"#{suffix}\"".html_safe unless value.include?(".") %>><%= value %><%= suffix %></span></dd></div>
        <% end %>
      </dl>
    </div>
    <img src="https://images.unsplash.com/photo-1634449571010-02389ed0f9b0?auto=format&fit=crop&w=1000&h=1040&q=80" alt="Stylist rinsing a client's long hair at a backwash basin" class="aspect-[25/26] w-full object-cover">
  </section>
  <!-- block:end hero -->

  <!-- block:start features -->
  <section id="services" class="bg-mist">
    <div class="mx-auto max-w-6xl px-5 py-24 md:px-8">
      <p class="text-sm uppercase tracking-[0.15em] text-ink/60">Our services</p>
      <h2 data-reveal class="mt-4 text-4xl font-medium tracking-tight md:text-5xl">What We Do Best</h2>
      <div class="mt-14 grid divide-y divide-line bg-white sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-4">
        <% [
          ["Haircut", "Precise cuts shaped around your face and hair type.", "From $80", "services.html"],
          ["Colour", "Natural colour, glosses and hand-painted balayage.", "From $140", "balayage.html"],
          ["Styling", "Blow-dries and occasion styles that hold all day.", "From $60", "services.html"],
          ["Treatment", "Keratin smoothing and deep repair for tired hair.", "From $90", "keratin-treatment.html"]
        ].each_with_index do |(title, copy, price, href), index| %>
          <a href="<%= href %>" data-reveal style="--reveal-delay: <%= index * 80 %>ms" class="group block border-b-4 border-transparent p-10 transition hover:border-ink">
            <span class="block text-6xl font-extralight text-ink/15">0<%= index + 1 %></span>
            <span class="mt-8 block text-2xl font-medium"><%= title %></span>
            <span class="mt-4 block leading-relaxed text-ink/70"><%= copy %></span>
            <span class="mt-8 block text-lg"><%= price %></span>
          </a>
        <% end %>
      </div>
    </div>
  </section>
  <!-- block:end features -->

  <!-- block:start gallery -->
  <section id="work" class="mx-auto grid max-w-6xl gap-5 px-5 py-24 md:px-8 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.4fr)]">
    <div class="flex flex-col justify-center bg-mist p-10 md:p-14">
      <p class="text-sm uppercase tracking-[0.15em] text-ink/60">Our work</p>
      <h2 data-reveal class="mt-4 text-3xl font-medium leading-tight tracking-tight md:text-4xl">Changes You'll Love to Wear</h2>
      <p data-reveal class="mt-6 leading-relaxed text-ink/70">Every guest gets our full attention and a plan built around their hair, so the result feels like you on your best day.</p>
      <a href="work.html" class="mt-8 inline-flex items-center gap-2 font-medium hover:underline">View Gallery <i class="fa-solid fa-arrow-right text-sm" aria-hidden="true"></i></a>
    </div>
    <div class="grid grid-cols-2 gap-5">
      <% [["1582095133179-bfd08e2fc6b3", "Stylist pinning soft curls into an elegant updo"], ["1633681926022-84c23e8cb2d6", "Bright modern salon with round mirrors and styling chairs"], ["1562322140-8baeececf3df", "Stylist blow-drying a guest's shoulder-length hair"], ["1492106087820-71f1a00d2b11", "Long wavy lavender-toned hair"]].each_with_index do |(image, alt), index| %>
        <img data-reveal style="--reveal-delay: <%= index * 70 %>ms" src="https://images.unsplash.com/photo-<%= image %>?auto=format&fit=crop&w=700&h=640&q=80" alt="<%= alt %>" class="aspect-[11/10] w-full object-cover">
      <% end %>
    </div>
  </section>
  <!-- block:end gallery -->

  <!-- block:start team -->
  <section id="team" class="bg-mist">
    <div class="mx-auto max-w-6xl px-5 py-24 md:px-8">
      <p class="text-sm uppercase tracking-[0.15em] text-ink/60">The team</p>
      <h2 data-reveal class="mt-4 text-4xl font-medium tracking-tight md:text-5xl">Meet Our Stylists</h2>
      <div class="mt-14 grid gap-8 sm:grid-cols-3">
        <% [["Clara Moreau", "Creative Director", "1582095133179-bfd08e2fc6b3", "Updo in progress by Clara"], ["Owen Hart", "Colour Specialist", "1554519515-242161756769", "Vivid multi-tone colour by Owen"], ["Mei Tanaka", "Senior Stylist", "1595476108010-b4d1f102b1b1", "Mei washing a guest's hair before a cut"]].each_with_index do |(name, role, image, alt), index| %>
          <figure data-reveal style="--reveal-delay: <%= index * 80 %>ms">
            <img src="https://images.unsplash.com/photo-<%= image %>?auto=format&fit=crop&w=700&h=930&q=80" alt="<%= alt %>" class="aspect-[3/4] w-full object-cover">
            <figcaption class="mt-5"><span class="block text-xl font-medium"><%= name %></span><span class="text-ink/60"><%= role %></span></figcaption>
          </figure>
        <% end %>
      </div>
    </div>
  </section>
  <!-- block:end team -->

  <!-- block:start about -->
  <section id="why" class="mx-auto grid max-w-6xl gap-12 px-5 py-24 md:px-8 lg:grid-cols-2 lg:items-center">
    <img src="https://images.unsplash.com/photo-1600948836101-f9ffda59d250?auto=format&fit=crop&w=1000&h=860&q=80" alt="Salon interior with black walls, round mirrors and styling chairs" class="aspect-[50/43] w-full object-cover">
    <div>
      <p class="text-sm uppercase tracking-[0.15em] text-ink/60">Why Tressa</p>
      <h2 data-reveal class="mt-4 text-4xl font-medium tracking-tight">Small Details, Big Difference</h2>
      <ul class="mt-10 space-y-8">
        <% [["Salon-grade products", "Gentle, professional formulas chosen for each hair type."], ["Always learning", "Our team trains every season in new cuts and colour."], ["Made for you", "A real consultation before every service, never a script."], ["A calm space", "Natural light, quiet music and no rushing between guests."]].each_with_index do |(title, copy), index| %>
          <li data-reveal style="--reveal-delay: <%= index * 70 %>ms" class="flex gap-5"><i class="fa-solid fa-check mt-1 text-lg" aria-hidden="true"></i><div><h3 class="text-xl font-medium"><%= title %></h3><p class="mt-1 text-ink/70"><%= copy %></p></div></li>
        <% end %>
      </ul>
    </div>
  </section>
  <!-- block:end about -->

  <!-- block:start pricing -->
  <section id="packages" class="bg-mist">
    <div class="mx-auto max-w-6xl px-5 py-24 md:px-8">
      <p class="text-sm uppercase tracking-[0.15em] text-ink/60">Packages</p>
      <h2 data-reveal class="mt-4 text-4xl font-medium tracking-tight md:text-5xl">Bundled for Better Hair</h2>
      <div class="mt-14 grid gap-6 md:grid-cols-3">
        <% [
          ["Essential", "$189", ["Haircut", "Repair treatment", "Simple styling", "Consultation"], false],
          ["Signature", "$379", ["Haircut", "Colour service", "Repair treatment", "Finish styling", "Take-home kit"], true],
          ["Complete", "$569", ["Haircut", "Full colour", "Keratin smoothing", "Event styling", "Take-home kit", "Priority booking"], false]
        ].each_with_index do |(name, price, items, featured), index| %>
          <article data-reveal style="--reveal-delay: <%= index * 80 %>ms" class="relative flex flex-col bg-white p-10 <%= featured ? 'border border-ink' : 'border border-line' %>">
            <% if featured %><span class="absolute -top-3 left-10 bg-ink px-4 py-1 text-xs font-medium uppercase tracking-wider text-white">Popular</span><% end %>
            <h3 class="text-xl font-medium"><%= name %></h3>
            <p class="mt-4 text-5xl font-light"><%= price %></p>
            <ul class="mt-8 flex-1 space-y-3 text-ink/80">
              <% items.each do |item| %><li class="flex items-center gap-3"><span class="h-1 w-1 rounded-full bg-ink"></span><%= item %></li><% end %>
            </ul>
            <a href="contact.html" class="mt-10 block py-3.5 text-center font-medium transition <%= featured ? 'bg-ink text-white hover:bg-black' : 'border border-ink hover:bg-ink hover:text-white' %>">Choose Package</a>
          </article>
        <% end %>
      </div>
    </div>
  </section>
  <!-- block:end pricing -->

  <!-- block:start reviews -->
  <section id="reviews" class="mx-auto max-w-6xl px-5 py-24 md:px-8">
    <p class="text-sm uppercase tracking-[0.15em] text-ink/60">Testimonials</p>
    <h2 data-reveal class="mt-4 text-4xl font-medium tracking-tight md:text-5xl">What Guests Say</h2>
    <div class="mt-14 grid gap-6 md:grid-cols-2">
      <% [
        ["Clara gave me the cut I've been describing to stylists for years. I finally feel like myself again.", "Hannah Brooks"],
        ["Fair prices and real listening. They talked me out of a colour that would have washed me out.", "Daniel Reyes"],
        ["Bright, calm and spotless. My balayage grew out beautifully and still looks soft months later.", "Priya Sharma"],
        ["I book every visit now. The whole team is kind, skilled and never rushes you out of the chair.", "Ethan Walsh"]
      ].each_with_index do |(quote, name), index| %>
        <figure data-reveal style="--reveal-delay: <%= (index % 2) * 80 %>ms" class="border border-line p-10">
          <p class="flex gap-1 text-sm" aria-label="5 out of 5 stars"><% 5.times do %><i class="fa-solid fa-star" aria-hidden="true"></i><% end %></p>
          <blockquote class="mt-6 leading-relaxed text-ink/75">“<%= quote %>”</blockquote>
          <figcaption class="mt-6 font-medium"><%= name %></figcaption>
        </figure>
      <% end %>
    </div>
  </section>
  <!-- block:end reviews -->

  <section id="recent" class="bg-mist">
    <div class="mx-auto max-w-6xl px-5 py-24 md:px-8">
      <p class="text-sm uppercase tracking-[0.15em] text-ink/60">Gallery</p>
      <h2 data-reveal class="mt-4 text-4xl font-medium tracking-tight md:text-5xl">Latest Looks</h2>
      <div class="mt-14 grid grid-cols-2 gap-5 md:grid-cols-4">
        <% [["1522337360788-8b13dee7a37e", "Long glossy waves seen from behind"], ["1582095133179-bfd08e2fc6b3", "Soft updo pinned in place"], ["1554519515-242161756769", "Vivid rainbow colour"], ["1562322140-8baeececf3df", "Smooth blow-dry finish"], ["1492106087820-71f1a00d2b11", "Lavender-toned waves"], ["1559599101-f09722fb4948", "Three smiling stylists with their tools"], ["1634449571010-02389ed0f9b0", "Relaxing hair wash at the basin"], ["1595476108010-b4d1f102b1b1", "Stylist laughing with a guest during a wash"]].each_with_index do |(image, alt), index| %>
          <img data-reveal style="--reveal-delay: <%= (index % 4) * 60 %>ms" src="https://images.unsplash.com/photo-<%= image %>?auto=format&fit=crop&w=600&h=600&q=80" alt="<%= alt %>" class="aspect-square w-full object-cover">
        <% end %>
      </div>
    </div>
  </section>

  <!-- block:start cta -->
  <section class="mx-auto max-w-3xl px-5 py-24 text-center md:px-8">
    <h2 data-reveal class="text-4xl font-medium tracking-tight md:text-5xl">Ready for a Fresh Start?</h2>
    <p class="mx-auto mt-6 max-w-xl text-lg text-ink/70">Book your visit today and enjoy expert hair care in a space that feels like a deep breath.</p>
    <ul class="mt-10 flex flex-wrap justify-center gap-x-8 gap-y-3 text-ink/70">
      <li class="flex items-center gap-2"><i class="fa-regular fa-clock" aria-hidden="true"></i>Tue–Sat · 9:00–19:00</li>
      <li class="flex items-center gap-2"><i class="fa-solid fa-phone" aria-hidden="true"></i><a href="tel:+13125550194" class="hover:text-ink">(312) 555-0194</a></li>
      <li class="flex items-center gap-2"><i class="fa-solid fa-location-dot" aria-hidden="true"></i>West Loop, Chicago</li>
    </ul>
    <a href="contact.html" class="mt-10 inline-block bg-ink px-12 py-4 font-medium text-white transition hover:bg-black">Book Appointment</a>
  </section>
  <!-- block:end cta -->

  <!-- block:start footer -->
  <footer class="border-t border-line bg-mist">
    <div class="mx-auto grid max-w-6xl gap-10 px-5 py-16 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)] md:px-8">
      <div>
        <a href="index.html" class="text-2xl font-medium tracking-tight">Tressa Studio</a>
        <p class="mt-5 max-w-sm leading-relaxed text-ink/70">A calm hair studio for precise cuts, natural colour and treatments that keep hair healthy.</p>
        <ul class="mt-6 flex gap-3">
          <li><a href="https://instagram.com" aria-label="Instagram" class="grid h-11 w-11 place-items-center border border-line bg-white hover:border-ink"><i class="fa-brands fa-instagram" aria-hidden="true"></i></a></li>
          <li><a href="https://facebook.com" aria-label="Facebook" class="grid h-11 w-11 place-items-center border border-line bg-white hover:border-ink"><i class="fa-brands fa-facebook-f" aria-hidden="true"></i></a></li>
        </ul>
      </div>
      <div>
        <h2 class="text-sm font-medium uppercase tracking-[0.15em]">Links</h2>
        <ul class="mt-5 space-y-3 text-ink/70">
          <% { "services" => "Services", "work" => "Work", "team" => "Team", "pricing" => "Pricing", "contact" => "Contact" }.each do |slug, label| %>
            <li><a href="<%= slug %>.html" class="hover:text-ink"><%= label %></a></li>
          <% end %>
        </ul>
      </div>
      <div>
        <h2 class="text-sm font-medium uppercase tracking-[0.15em]">Contact</h2>
        <ul class="mt-5 space-y-3 text-ink/70">
          <li class="flex gap-3"><i class="fa-solid fa-location-dot mt-1" aria-hidden="true"></i><address class="not-italic">905 W Fulton St<br>Chicago, IL 60607</address></li>
          <li class="flex gap-3"><i class="fa-solid fa-phone mt-1" aria-hidden="true"></i><a href="tel:+13125550194" class="hover:text-ink">(312) 555-0194</a></li>
          <li class="flex gap-3"><i class="fa-regular fa-clock mt-1" aria-hidden="true"></i>Tue–Sat · 9:00–19:00</li>
        </ul>
      </div>
    </div>
    <div class="mx-auto flex max-w-6xl flex-col gap-3 border-t border-line px-5 py-6 text-sm text-ink/60 md:flex-row md:items-center md:justify-between md:px-8">
      <p>&copy; <span data-current-year>2026</span> Tressa Studio. All rights reserved.</p>
      <a href="contact.html" class="hover:text-ink">Privacy &amp; terms</a>
    </div>
  </footer>
  <!-- block:end footer -->

  <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);
      });
    });

    document.querySelectorAll('[data-filter-group]').forEach((group) => {
      const buttons = group.querySelectorAll('[data-filter]');
      buttons.forEach((button) => {
        button.addEventListener('click', () => {
          buttons.forEach((other) => {
            const active = other === button;
            other.setAttribute('aria-pressed', String(active));
            other.classList.toggle('bg-ink', active);
            other.classList.toggle('text-white', active);
          });
          document.querySelectorAll('[data-filter-item]').forEach((item) => {
            item.hidden = button.dataset.filter !== 'all' && item.dataset.category !== button.dataset.filter;
          });
        });
      });
    });

    document.querySelectorAll('[data-accordion]').forEach((accordion) => {
      const triggers = accordion.querySelectorAll('[data-accordion-trigger]');
      triggers.forEach((trigger) => {
        trigger.addEventListener('click', () => {
          const willOpen = trigger.getAttribute('aria-expanded') !== 'true';
          triggers.forEach((other) => {
            const open = other === trigger && willOpen;
            other.setAttribute('aria-expanded', String(open));
            document.getElementById(other.getAttribute('aria-controls')).classList.toggle('hidden', !open);
            other.querySelector('[data-accordion-icon]')?.classList.toggle('rotate-45', open);
          });
        });
      });
    });

    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 || 'Please complete the required fields.';
          return;
        }
        status.textContent = form.dataset.success || 'Thanks! We will be in touch soon.';
        form.reset();
      });
    });

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

    (() => {
      const counters = document.querySelectorAll('[data-count-to]');
      if (!counters.length || navigator.webdriver || matchMedia('(prefers-reduced-motion: reduce)').matches || !('IntersectionObserver' in window)) return;
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(({ isIntersecting, target }) => {
          if (!isIntersecting) return;
          observer.unobserve(target);
          const end = Number(target.dataset.countTo);
          const suffix = target.dataset.countSuffix || '';
          const start = performance.now();
          const tick = (now) => {
            const progress = Math.min((now - start) / 700, 1);
            target.textContent = Math.round(end * progress) + suffix;
            if (progress < 1) requestAnimationFrame(tick);
          };
          requestAnimationFrame(tick);
        });
      }, { threshold: 0.6 });
      counters.forEach((counter) => observer.observe(counter));
    })();

    (() => {
      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>

Unlimited access to all templates

Unlock our entire library of premium UI kits, dashboards, and landing pages. Renews automatically, cancel anytime.

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

More Business Templates