Browse the library
Blocks / Hero
Hero, agency
FreeStudio hero with oversized display type, animated grid, availability CTA and a client wordmark marquee.
components/facade/hero-agency.tsx
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { ArrowRight, ArrowUpRight, Calendar } from "lucide-react";
import { cn } from "@/lib/utils";
export interface HeroAgencyProps {
eyebrow?: string;
/** Headline lines. Wrap a word in *asterisks* to render it in the brand accent italic. */
headline?: string[];
description?: string;
primaryCta?: { label: string; href: string };
secondaryCta?: { label: string; href: string };
availability?: string;
clientsLabel?: string;
clients?: string[];
className?: string;
}
const EASE = [0.22, 1, 0.36, 1] as const;
const WORDMARK_STYLES = [
"font-display font-bold tracking-tight",
"font-serif italic font-medium",
"font-mono font-semibold uppercase tracking-widest text-[0.85em]",
"font-sans font-black uppercase tracking-tighter",
"font-display font-light tracking-[0.2em] uppercase text-[0.9em]",
"font-serif font-semibold tracking-tight",
];
function renderLine(line: string) {
const parts = line.split(/(\*[^*]+\*)/g).filter(Boolean);
return parts.map((p, i) =>
p.startsWith("*") && p.endsWith("*") ? (
<span key={i} className="font-serif font-normal italic text-brand">
{p.slice(1, -1)}
</span>
) : (
<React.Fragment key={i}>{p}</React.Fragment>
),
);
}
function AnimatedGrid() {
const reduce = useReducedMotion();
const cells = React.useMemo(
() => [
[2, 1], [5, 3], [9, 2], [12, 5], [3, 6], [14, 1], [7, 7], [10, 8], [1, 9], [15, 7],
],
[],
);
return (
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
<div
className="absolute inset-0 [mask-image:radial-gradient(ellipse_80%_70%_at_50%_30%,black,transparent)]"
style={{
backgroundImage:
"linear-gradient(to right, color-mix(in oklch, var(--foreground) 7%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklch, var(--foreground) 7%, transparent) 1px, transparent 1px)",
backgroundSize: "72px 72px",
}}
>
{cells.map(([cx, cy], i) => (
<motion.span
key={i}
className="absolute size-[72px] bg-brand/10"
style={{ left: cx * 72, top: cy * 72 }}
initial={{ opacity: 0 }}
animate={reduce ? { opacity: 0.5 } : { opacity: [0, 1, 0] }}
transition={{
duration: 4,
repeat: reduce ? 0 : Infinity,
delay: i * 0.9,
repeatDelay: 3,
ease: "easeInOut",
}}
/>
))}
</div>
{/* Sweeping scan line */}
{!reduce && (
<motion.div
className="absolute inset-x-0 h-40 bg-linear-to-b from-transparent via-brand/[0.07] to-transparent"
initial={{ top: "-20%" }}
animate={{ top: "110%" }}
transition={{ duration: 9, repeat: Infinity, ease: "linear", repeatDelay: 2 }}
/>
)}
<div className="absolute left-1/2 top-[-12rem] h-[30rem] w-[60rem] -translate-x-1/2 rounded-full bg-brand/15 blur-[140px]" />
</div>
);
}
export function HeroAgency({
eyebrow = "Northline Studio, Denver CO",
headline = ["We build brands", "people *remember*."],
description = "Strategy, identity and conversion-first websites for ambitious local businesses and real estate teams. Senior people only. Launches in six weeks, not six months.",
primaryCta = { label: "Book a call", href: "#book" },
secondaryCta = { label: "See our work", href: "#work" },
availability = "2 spots left for Q1",
clientsLabel = "Trusted by teams at",
clients = [
"Summit Roofing",
"Mile High Dental",
"Alder & Vine",
"COMPASS RIDGE",
"Peak Realty",
"Ironwood",
"Redline Auto",
"Larimer Coffee",
],
className,
}: HeroAgencyProps) {
const reduce = useReducedMotion();
return (
<section
className={cn(
"relative isolate overflow-hidden bg-background text-foreground",
className,
)}
>
<AnimatedGrid />
<div className="mx-auto max-w-7xl px-5 pb-16 pt-20 sm:px-8 sm:pt-28 lg:pt-36">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: EASE }}
className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground"
>
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-card/70 px-3 py-1 font-medium backdrop-blur">
<span className="size-1.5 rounded-full bg-brand" aria-hidden />
{eyebrow}
</span>
<span className="hidden sm:inline">Brand, Web, Growth</span>
</motion.div>
<h1 className="mt-8 font-display text-[clamp(2.75rem,12vw,3.25rem)] font-semibold leading-[0.92] tracking-[-0.04em] sm:text-[5.5rem] lg:text-[8.5rem]">
{headline.map((line, i) => (
<span key={i} className="block overflow-hidden pb-[0.06em]">
<motion.span
className="block"
initial={reduce ? { opacity: 0 } : { y: "110%", rotate: 2 }}
animate={reduce ? { opacity: 1 } : { y: "0%", rotate: 0 }}
transition={{ duration: 1.1, delay: 0.1 + i * 0.12, ease: EASE }}
>
{renderLine(line)}
</motion.span>
</span>
))}
</h1>
<div className="mt-10 grid gap-10 lg:mt-14 lg:grid-cols-[1fr_auto] lg:items-end">
<motion.p
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4, ease: EASE }}
className="max-w-xl text-lg leading-relaxed text-muted-foreground text-pretty sm:text-xl"
>
{description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE }}
className="flex flex-col gap-4 sm:flex-row sm:items-center"
>
<div className="flex flex-col items-start gap-2.5">
<a
href={primaryCta.href}
className="group inline-flex h-14 items-center gap-3 rounded-full bg-primary pl-2 pr-6 text-[15px] font-semibold text-primary-foreground transition-transform hover:-translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<span className="flex size-10 items-center justify-center rounded-full bg-brand text-brand-foreground transition-transform duration-500 group-hover:rotate-[-8deg]">
<Calendar className="size-4" aria-hidden />
</span>
{primaryCta.label}
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" aria-hidden />
</a>
<span className="flex items-center gap-2 pl-2 text-sm text-muted-foreground">
<span className="relative flex size-2" aria-hidden>
<span className="absolute inline-flex size-full animate-ping-slow rounded-full bg-success/70" />
<span className="relative inline-flex size-2 rounded-full bg-success" />
</span>
{availability}
</span>
</div>
<a
href={secondaryCta.href}
className="group inline-flex h-14 items-center gap-1.5 self-start rounded-full px-5 text-[15px] font-semibold text-foreground underline-offset-4 transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{secondaryCta.label}
<ArrowUpRight className="size-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden />
</a>
</motion.div>
</div>
</div>
{/* Client marquee */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 0.8 }}
className="border-y border-border bg-card/40 backdrop-blur"
>
<div className="mx-auto flex max-w-7xl flex-col gap-4 px-5 py-6 sm:px-8 md:flex-row md:items-center md:gap-10">
<p className="shrink-0 text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
{clientsLabel}
</p>
<div
className="group relative flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]"
style={{ ["--marquee-gap" as string]: "3.5rem", ["--marquee-duration" as string]: "38s" }}
>
<ul
className="flex w-max shrink-0 animate-marquee items-center gap-[var(--marquee-gap)] group-hover:[animation-play-state:paused]"
>
{[...clients, ...clients].map((c, i) => (
<li
key={i}
aria-hidden={i >= clients.length}
className={cn(
"whitespace-nowrap text-xl text-foreground/45 transition-colors hover:text-foreground",
WORDMARK_STYLES[i % clients.length % WORDMARK_STYLES.length],
)}
>
{c}
</li>
))}
</ul>
</div>
</div>
</motion.div>
</section>
);
}
export default HeroAgency;app/page.tsx
import { HeroAgency } from "@/components/facade/hero-agency";
export default function Demo() {
return <HeroAgency />;
}Installation
$
npx shadcn@latest add https://facadeui.com/r/hero-agency.jsonUsage
import { HeroAgency } from "@/components/facade/hero-agency";
export default function Page() {
return <HeroAgency />;
}Props
HeroAgencyProps is exported, so you can extend or wrap it. Every prop has a default; <HeroAgency /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| eyebrow | string | |
| headline | string[] | Headline lines. Wrap a word in *asterisks* to render it in the brand accent italic. |
| description | string | |
| primaryCta | { label: string; href: string } | |
| secondaryCta | { label: string; href: string } | |
| availability | string | |
| clientsLabel | string | |
| clients | string[] | |
| className | string |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade hero, agency to this page and adapt the copy to our business.