Innovations
heroes /

Landscape Card Hero

Centered white card hero floating on a full-bleed landscape photograph. Headline supports orange-underlined emphasis on selected words. Mobile-first, no shadcn, no JS.

Preview

Source

tsx
import type { ReactNode } from "react";

export interface LandscapeCardNavItem {
  label: string;
  href: string;
}

export interface LandscapeCardHeroProps {
  brand?: string;
  brandIcon?: ReactNode;
  navItems?: LandscapeCardNavItem[];
  navCta?: { label: string; href: string };
  /** Words inside the headline to highlight in orange + underline. Matched case-insensitively. */
  emphasizedWords?: string[];
  headline?: string;
  subhead?: string;
  primaryCta?: { label: string; href: string };
  /** URL of the full-bleed background image. Default ships with the component at /heroes/landscape-card-bg.webp */
  backgroundSrc?: string;
  /** Alt text for the background image (decorative by default). */
  backgroundAlt?: string;
}

const DefaultBrandIcon = () => (
  <svg
    aria-hidden
    viewBox="0 0 24 24"
    className="h-6 w-6"
    fill="none"
  >
    <path
      d="M12 2 L21 8 L18 20 L6 20 L3 8 Z"
      fill="#f97316"
      stroke="#c2410c"
      strokeWidth="0.8"
      strokeLinejoin="round"
    />
    <path d="M12 2 L18 20 M12 2 L6 20 M3 8 L21 8" stroke="#fff" strokeOpacity="0.55" strokeWidth="0.6" />
  </svg>
);

function highlightWords(text: string, words: string[]) {
  if (!words || words.length === 0) return text;
  // Build a regex matching any of the target words, case-insensitive, whole-word.
  const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
  const re = new RegExp(`\\b(${escaped.join("|")})\\b`, "gi");
  const parts: (string | { match: string })[] = [];
  let lastIndex = 0;
  let m: RegExpExecArray | null;
  while ((m = re.exec(text)) !== null) {
    if (m.index > lastIndex) parts.push(text.slice(lastIndex, m.index));
    parts.push({ match: m[0] });
    lastIndex = re.lastIndex;
  }
  if (lastIndex < text.length) parts.push(text.slice(lastIndex));
  return parts.map((part, i) =>
    typeof part === "string" ? (
      <span key={i}>{part}</span>
    ) : (
      <span
        key={i}
        className="text-[#f97316] underline decoration-[3px] underline-offset-[6px] decoration-[#f97316]"
      >
        {part.match}
      </span>
    )
  );
}

export default function LandscapeCardHero({
  brand = "Orelloo",
  brandIcon,
  navItems = [
    { label: "Home", href: "#" },
    { label: "Service", href: "#service" },
    { label: "Resources", href: "#resources" },
    { label: "About us", href: "#about" },
  ],
  navCta = { label: "Contact us", href: "#contact" },
  emphasizedWords = ["Secure", "future"],
  headline = "Secure your business, Secure your future",
  subhead = "Data analysis software is a type of software tool used for data analysis and reporting. It is designed to help businesses, organizations, and individuals process, visualize.",
  primaryCta = { label: "Get Started Free", href: "#start" },
  backgroundSrc = "/heroes/landscape-card-bg.webp",
  backgroundAlt = "",
}: LandscapeCardHeroProps) {
  return (
    <section className="relative isolate min-h-[120svh] w-full overflow-hidden bg-slate-900 sm:min-h-[125svh] lg:min-h-[130svh]">
      {/* Full-bleed background image */}
      <img
        src={backgroundSrc}
        alt={backgroundAlt}
        width={1800}
        height={1012}
        loading="eager"
        fetchPriority="high"
        decoding="async"
        className="absolute inset-0 -z-10 h-full w-full object-cover"
      />
      {/* Full-width white panel that fades to transparent at its bottom edge,
          letting the landscape image blend through into the lower viewport. */}
      <div
        className="w-full bg-gradient-to-b from-white from-0% via-white via-65% to-transparent to-100% pb-40 sm:pb-56 lg:pb-64"
      >
        <div className="mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8">
          {/* Nav */}
          <nav className="flex items-center justify-between gap-4 py-4 sm:py-5">
            <a href="/" className="flex items-center gap-2">
              {brandIcon ?? <DefaultBrandIcon />}
              <span className="text-lg font-bold text-slate-900">{brand}</span>
            </a>

            <div className="hidden items-center gap-7 md:flex">
              {navItems.map((item, i) => (
                <a
                  key={item.label}
                  href={item.href}
                  className={`text-sm font-medium transition-colors ${
                    i === 0
                      ? "text-[#f97316] underline decoration-2 underline-offset-4 decoration-[#f97316]"
                      : "text-slate-600 hover:text-slate-900"
                  }`}
                >
                  {item.label}
                </a>
              ))}
            </div>

            <a
              href={navCta.href}
              className="hidden rounded-full bg-[#f97316] px-5 py-2.5 text-sm font-semibold text-white shadow-md shadow-[#f97316]/25 transition-all hover:-translate-y-0.5 hover:bg-[#ea580c] hover:shadow-lg sm:inline-flex"
            >
              {navCta.label}
            </a>

            <button
              type="button"
              aria-label="Open menu"
              className="rounded-md p-2 text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 md:hidden"
            >
              <svg viewBox="0 0 24 24" fill="none" className="h-5 w-5">
                <path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
              </svg>
            </button>
          </nav>

          {/* Headline + subhead + CTA */}
          <div className="pb-12 pt-6 text-center sm:pb-16 sm:pt-10 lg:pb-20">
            <h1 className="mx-auto max-w-3xl text-4xl font-semibold leading-[1.15] tracking-tight text-slate-900 sm:text-5xl lg:text-[3.5rem]">
              {highlightWords(headline, emphasizedWords)}
            </h1>
            <p className="mx-auto mt-6 max-w-xl text-sm leading-relaxed text-slate-500 sm:text-base">
              {subhead}
            </p>
            <div className="mt-8 flex justify-center">
              <a
                href={primaryCta.href}
                className="inline-flex items-center justify-center rounded-full bg-[#f97316] px-8 py-3.5 text-sm font-semibold text-white shadow-lg shadow-[#f97316]/30 transition-all hover:-translate-y-0.5 hover:bg-[#ea580c] hover:shadow-xl hover:shadow-[#f97316]/40"
              >
                {primaryCta.label}
              </a>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add landscape-card

Where to use it

Drop this at the top of a landing page where the brand wants a moody, premium, photographic feel. In Astro (src/pages/index.astro): --- import LandscapeCardHero from '../components/innovations/heroes/landscape-card'; --- <LandscapeCardHero /> No client:* directive needed — ships zero JS by default. In Next.js (app/page.tsx): import LandscapeCardHero from '@/components/innovations/heroes/landscape-card'; // Drop at the top of the page return. ASSETS: The default background image is served from /heroes/landscape-card-bg.webp. When installing into a target project, also copy public/heroes/landscape-card-bg.webp into that project's public/ directory (or pass your own backgroundSrc prop). The image is a misty forest landscape generated for this hero. Swap to your own photo via: <LandscapeCardHero backgroundSrc="/your-image.webp" /> Aim for a wide image (1600-2000px wide), JPEG or WebP, around 80-120 KB. The composition should have visual interest at the bottom (foreground) and softer / lighter content at the top so the white card edges blend in. CUSTOMIZATION: Highlight ANY words in the headline by passing emphasizedWords: <LandscapeCardHero headline="Build something brilliant for tomorrow" emphasizedWords={["brilliant", "tomorrow"]} /> The matcher is case-insensitive and whole-word. ACCENT COLOR (orange #f97316 / #ea580c) is hard-coded for visual fidelity. To rebrand, search/replace those two hex values across the file. NAV STRIP: included inside the white card. If you already have a global navbar, delete the <nav> block — the card collapses cleanly. PERFORMANCE NOTES: - The background <img> uses loading="eager" + fetchPriority="high" because it's above the fold. - Section uses min-h-[100svh] (small-viewport height unit) so iOS Safari address-bar UI doesn't add extra scroll on first paint.