Innovations
heroes /

Crouching Tiger Hero (Local Service)

Exact replica of the Crouching Tiger Exterior Cleaning production hero. Full-bleed bg image with navy tint, 5-star + Top Rated, headline with accent-orange word, body with mobile-floated blob, 2 CTAs, trust-badge row, blob portrait with 3 staggered fade-in review toasts + Google rating badge, wave divider. From ctigerclean.com.

Preview

Source

tsx
"use client";

import { useState, useEffect } from "react";
import { Phone, Shield, Star, MapPin, Clock } from "lucide-react";

interface CTQuote {
  text: string;
  name: string;
  short: string;
  initial: string;
}

interface CTTrustBadge {
  text: string;
  icon: "Shield" | "MapPin" | "Clock" | "Star";
}

export interface CrouchingTigerHeroProps {
  topRatedLabel?: string;
  headline?: string;
  highlightedText?: string;
  description?: string;
  primaryCta?: { label: string; href: string };
  phone?: string;
  callLabel?: string;
  trustBadges?: CTTrustBadge[];
  heroQuotes?: CTQuote[];
  bgImageSrc?: string;
  bgImageMobileSrc?: string;
  bgImageAlt?: string;
  blobImageSrc?: string;
  blobImageMobileSrc?: string;
  blobImageAlt?: string;
  /** Default = CT primary navy #0A1628 */
  primaryColor?: string;
  /** Default = CT accent tiger-orange #D14F1B */
  accentColor?: string;
  /** Default = wave divider color #F8FAFC */
  waveColor?: string;
}

const iconMap: Record<CTTrustBadge["icon"], React.ComponentType<{ className?: string; style?: React.CSSProperties }>> = {
  Shield,
  Star,
  MapPin,
  Clock,
};

const GoogleG = ({ size = 24 }: { size?: number }) => (
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style={{ width: size, height: size }} className="flex-shrink-0">
    <path fill="#4285F4" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
    <path fill="#34A853" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
    <path fill="#FBBC05" d="M10.53 28.59A14.5 14.5 0 0 1 9.5 24c0-1.59.28-3.14.76-4.59l-7.98-6.19A23.99 23.99 0 0 0 0 24c0 3.77.9 7.34 2.56 10.5l7.97-5.91z"/>
    <path fill="#EA4335" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 5.91C6.51 42.62 14.62 48 24 48z"/>
  </svg>
);

const FiveStars = ({ size = 10 }: { size?: number }) => (
  <span className="flex gap-0.5">
    {[...Array(5)].map((_, i) => (
      <Star key={i} className="text-yellow-400 fill-yellow-400" style={{ width: size, height: size }} />
    ))}
  </span>
);

export default function CrouchingTigerHero({
  topRatedLabel = "Top Rated",
  headline = "South Florida's Most Trusted",
  highlightedText = "Exterior Cleaning",
  description = "Professional exterior cleaning for homes and businesses across Palm Beach County. Family-owned, fully insured, and backed by a satisfaction guarantee on every job.",
  primaryCta = { label: "Get Instant Quote", href: "#quote-form" },
  phone = "561-440-1188",
  callLabel = "Call Now:",
  trustBadges = [
    { text: "Fully Insured", icon: "Shield" },
    { text: "Locally Owned", icon: "MapPin" },
    { text: "Same-Day Quotes", icon: "Clock" },
  ],
  heroQuotes = [
    { text: "These guys are amazing! Couldn't recommend them more!", name: "Brandon Franklin", short: "Amazing! Couldn't recommend more!", initial: "B" },
    { text: "My outdoor space looks brand new again!", name: "Anna Degrezia", short: "Outdoor space looks brand new!", initial: "A" },
    { text: "Great job, very professional!", name: "Frank Giordano", short: "Great job, very professional!", initial: "F" },
  ],
  bgImageSrc = "/heroes/crouchingtiger/bg.webp",
  bgImageMobileSrc = "/heroes/crouchingtiger/bg-mobile.webp",
  bgImageAlt = "Professional exterior cleaning of a Palm Beach luxury home",
  blobImageSrc = "/heroes/crouchingtiger/micah-roof-cleaning.webp",
  blobImageMobileSrc = "/heroes/crouchingtiger/micah-roof-cleaning-mobile.webp",
  blobImageAlt = "Owner cleaning gutters on a Palm Beach roof",
  primaryColor = "#0A1628",
  accentColor = "#D14F1B",
  waveColor = "#F8FAFC",
}: CrouchingTigerHeroProps) {
  const [visibleToasts, setVisibleToasts] = useState<number[]>([]);
  const [mobileIndex, setMobileIndex] = useState(0);
  const [showMobileToast, setShowMobileToast] = useState(false);

  useEffect(() => {
    const timers = heroQuotes.map((_, i) =>
      setTimeout(() => setVisibleToasts((prev) => [...prev, i]), i === 0 ? 500 : 500 + i * 1400)
    );
    const mobileFirst = setTimeout(() => setShowMobileToast(true), 2000);
    const mobileRotate = setInterval(() => {
      setShowMobileToast(false);
      setTimeout(() => {
        setMobileIndex((prev) => (prev + 1) % heroQuotes.length);
        setShowMobileToast(true);
      }, 400);
    }, 5000);
    return () => {
      timers.forEach(clearTimeout);
      clearTimeout(mobileFirst);
      clearInterval(mobileRotate);
    };
  }, [heroQuotes.length]);

  const tel = phone.replace(/[^\d]/g, "");

  return (
    <section className="relative min-h-screen flex items-center pt-20 overflow-hidden">
      {/* Background image with primary tint overlay */}
      <div className="absolute inset-0 z-0">
        <div
          className="absolute inset-0 z-10"
          style={{ background: primaryColor, opacity: 0.7 }}
        />
        <picture>
          <source media="(max-width: 768px)" srcSet={bgImageMobileSrc} type="image/webp" />
          <img
            src={bgImageSrc}
            alt={bgImageAlt}
            className="absolute inset-0 w-full h-full object-cover"
            width={1920}
            height={1080}
            fetchPriority="high"
            decoding="sync"
          />
        </picture>
      </div>

      <div className="relative z-20 mx-auto max-w-7xl w-full px-4 sm:px-6 lg:px-8 py-20">
        <div className="flex items-center gap-12 lg:gap-16">
          {/* Text content */}
          <div className="max-w-2xl flex-1">
            <div className="flex items-center gap-2 mb-5 lg:mb-6">
              <FiveStars size={28} />
              <span className="text-white font-bold text-lg lg:text-xl tracking-wide uppercase">
                {topRatedLabel}
              </span>
            </div>
            <h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white leading-tight mb-3 lg:mb-6">
              {headline}{" "}
              <span style={{ color: accentColor }}>{highlightedText}</span> Team
            </h1>

            {/* Mobile rotating toast */}
            <a
              href="#reviews"
              className={`lg:hidden inline-flex items-center gap-2 bg-white/95 backdrop-blur-sm rounded-full shadow-md px-3 py-1.5 mb-4 transition-all duration-400 ${
                showMobileToast ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2 pointer-events-none"
              }`}
            >
              <FiveStars size={10} />
              <span className="text-gray-700 text-xs italic">
                &ldquo;{heroQuotes[mobileIndex].short}&rdquo;
              </span>
              <span className="text-gray-400 text-xs whitespace-nowrap">
                — {heroQuotes[mobileIndex].name.split(" ")[0]}
              </span>
            </a>

            <p className="text-xl text-gray-200 mb-8">
              {/* Mobile blob */}
              <span className="lg:hidden float-right ml-4 mb-2 w-[40vw] relative">
                <span className="block overflow-hidden rounded-[55%_45%_60%_40%/60%_55%_45%_40%] shadow-xl border-2 border-white/40">
                  <img
                    src={blobImageMobileSrc}
                    alt={blobImageAlt}
                    className="w-full h-auto object-cover aspect-[3/4] scale-150"
                    width={300}
                    height={400}
                    loading="eager"
                    fetchPriority="high"
                    decoding="sync"
                  />
                </span>
                <a
                  href="#reviews"
                  className="absolute -bottom-5 left-1/2 -translate-x-1/2 bg-white rounded-xl shadow-lg px-3 py-2 flex items-center gap-2 whitespace-nowrap"
                >
                  <GoogleG size={20} />
                  <span className="flex flex-col leading-tight">
                    <span className="flex items-center gap-1">
                      <FiveStars size={10} />
                      <span className="text-gray-900 font-bold text-xs">5.0</span>
                    </span>
                    <span className="text-gray-500 text-[9px] font-medium">Google Reviews</span>
                  </span>
                </a>
              </span>
              {description}
            </p>

            <div className="flex flex-col sm:flex-row gap-4 mb-12">
              <a
                href={primaryCta.href}
                className="inline-flex items-center justify-center px-7 py-3.5 rounded-md text-white font-semibold transition-all hover:-translate-y-0.5"
                style={{ background: accentColor }}
              >
                {primaryCta.label}
              </a>
              <a
                href={`tel:${tel}`}
                className="inline-flex items-center justify-center px-7 py-3.5 rounded-md bg-white border-2 border-white font-semibold transition-colors"
                style={{ color: primaryColor }}
              >
                <Phone className="mr-2 h-5 w-5" />
                {callLabel} {phone}
              </a>
            </div>

            {/* Trust Badges */}
            <div className="flex flex-wrap gap-4">
              {trustBadges.map((badge) => {
                const Icon = iconMap[badge.icon];
                return (
                  <div
                    key={badge.text}
                    className="flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-full hover:bg-white/20 transition-colors"
                  >
                    {Icon && <Icon className="h-5 w-5" style={{ color: accentColor }} />}
                    <span className="text-white text-sm font-medium">{badge.text}</span>
                  </div>
                );
              })}
            </div>
          </div>

          {/* Blob image - desktop only */}
          <div className="hidden lg:block flex-shrink-0">
            <div className="relative w-96 xl:w-[28rem]">
              {heroQuotes.map((quote, i) => {
                const positions = [
                  "absolute -top-6 -right-6 z-10",
                  "absolute -bottom-8 -right-8 z-10",
                  "absolute top-1/3 -left-10 z-10",
                ];
                return (
                  <a
                    key={i}
                    href="#reviews"
                    className={`${positions[i]} bg-white rounded-xl shadow-lg px-3 py-2.5 max-w-[200px] transition-all duration-700 hover:shadow-xl ${
                      visibleToasts.includes(i) ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-4 pointer-events-none"
                    }`}
                  >
                    <FiveStars size={10} />
                    <p className="text-gray-700 text-xs italic leading-snug mt-1">
                      &ldquo;{quote.text}&rdquo;
                    </p>
                    <p className="text-gray-500 text-[10px] mt-1 font-medium">
                      — {quote.name}
                    </p>
                  </a>
                );
              })}

              {/* Decorative accent ring */}
              <div
                className="absolute -inset-3 rounded-[60%_40%_50%_50%/50%_60%_40%_50%] border-2"
                style={{ borderColor: `${accentColor}4D` }}
              />

              {/* Blob image */}
              <div className="relative overflow-hidden rounded-[60%_40%_50%_50%/50%_60%_40%_50%] shadow-2xl">
                <img
                  src={blobImageSrc}
                  alt={blobImageAlt}
                  className="w-full h-auto object-cover aspect-square"
                  style={{ transform: "scale(1.3)" }}
                  width={500}
                  height={500}
                  fetchPriority="high"
                />
              </div>

              {/* Google review floating badge */}
              <a
                href="#reviews"
                className="absolute -bottom-2 -left-4 bg-white rounded-xl shadow-lg px-4 py-2.5 flex items-center gap-2.5 hover:shadow-xl transition-shadow"
              >
                <GoogleG size={24} />
                <span className="flex flex-col leading-tight">
                  <span className="flex items-center gap-1">
                    <FiveStars size={14} />
                    <span className="text-gray-900 font-bold text-sm">5.0</span>
                  </span>
                  <span className="text-gray-500 text-[10px] font-medium">Google Reviews</span>
                </span>
              </a>
            </div>
          </div>
        </div>
      </div>

      {/* Wave divider */}
      <div className="absolute -bottom-1 left-0 right-0 z-10">
        <svg
          viewBox="0 0 1440 120"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
          className="w-full h-auto block"
          preserveAspectRatio="none"
        >
          <path
            d="M0 120L60 105C120 90 240 60 360 45C480 30 600 30 720 37.5C840 45 960 60 1080 67.5C1200 75 1320 75 1380 75L1440 75V120H1380C1320 120 1200 120 1080 120C960 120 840 120 720 120C600 120 480 120 360 120C240 120 120 120 60 120H0Z"
            fill={waveColor}
          />
        </svg>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add crouchingtiger-hero

Where to use it

Production-grade hero from the Crouching Tiger Exterior Cleaning site. Best for local service businesses (cleaning, landscaping, contractors, trades) where reviews + Google rating are the conversion lever. In Astro: --- import CrouchingTigerHero from '../components/innovations/heroes/crouchingtiger-hero'; --- <CrouchingTigerHero client:load /> client:load is REQUIRED — the desktop review toasts stagger-fade-in via useEffect, and the mobile toast rotates every 5s. In Next.js: import CrouchingTigerHero from '@/components/innovations/heroes/crouchingtiger-hero'; // "use client" is set inside the component. ASSETS: Default images live at /heroes/crouchingtiger/. Copy bg.webp, bg-mobile.webp, micah-roof-cleaning.webp, and micah-roof-cleaning-mobile.webp into your project's public/heroes/crouchingtiger/ directory, OR override via bgImageSrc / blobImageSrc props. CUSTOMIZATION: <CrouchingTigerHero headline="Your area's Most Trusted" highlightedText="Service" description="..." primaryCta={{ label: "Get Quote", href: "/quote" }} phone="555-123-4567" trustBadges={[ { text: "Fully Insured", icon: "Shield" }, { text: "Locally Owned", icon: "MapPin" }, { text: "Same-Day Quotes", icon: "Clock" }, ]} heroQuotes={[ { text: "Long quote for desktop toast.", short: "Short quote for mobile.", name: "First Last", initial: "F" }, ... ]} bgImageSrc="/your-bg.webp" blobImageSrc="/your-portrait.webp" primaryColor="#0A1628" accentColor="#D14F1B" /> QUOTES rotate on mobile (5s interval) and stagger-fade-in on desktop (500ms / 1.4s steps). The "5.0 Google Reviews" badge is hardcoded; edit the JSX to change the rating. PALETTE: navy + tiger-orange. Wave divider color separately customizable via waveColor (default = slate-50 #F8FAFC).