CTA
CTA Marquee
A large invitation and a button above an endless strip of photos running edge to edge.
Layout and motion from the Nitip template — see it in contextimport { Marquee } from "@/components/ui/marquee";
import { SpringButton } from "@/components/ui/spring-button";
import { cn } from "@/lib/utils";
/*
* CTA Marquee — a large invitation and a button above an endless strip of square photos running edge to edge.
* Source: marcframe/nitip → components/sections/CtaSection.tsx (+ design-system Ticker).
*
* Layout kept from Framer: the heading and button sit in a column up to 56rem, left-aligned below 1024px and centered
* (text still left-aligned) from 1024px; the strip runs the full width of the page under it, clipped at the edges.
* Tiles are 400px square with 30px between them from 768px (256px on phones, where 400px would fill the screen).
* Motion kept (marquee): the strip runs left at 30px/s and stands still with reduced motion. The strip is decorative,
* so it is hidden from assistive tech.
*/
export type CtaMarqueeProps = {
title?: string;
action?: { label: string; href: string };
/** Square photos for the strip (decorative). */
images?: string[];
/** Pixels per second. */
speed?: number;
className?: string;
};
const photo = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=800&h=800&q=80`;
const defaults = {
title: "Ready to turn your brand's attention into real growth?",
action: { label: "Start a project", href: "/contact" },
images: [
photo("photo-1504674900247-0877df9cc836"),
photo("photo-1534438327276-14e5300c3a48"),
photo("photo-1441986300917-64674bd600d8"),
photo("photo-1523275335684-37898b6baf30"),
photo("photo-1505740420928-5e560c06d30e"),
photo("photo-1485955900006-10f4d324d411"),
],
};
export function CtaMarquee({
title = defaults.title,
action = defaults.action,
images = defaults.images,
speed = 30,
className,
}: CtaMarqueeProps) {
return (
<section className={cn("overflow-hidden py-(--section-y)", className)}>
<div className="mx-auto flex max-w-(--container) flex-col px-(--gutter)">
<div className="flex w-full max-w-4xl flex-col items-start gap-12 lg:self-center">
<h2 className="font-heading text-h1 font-semibold tracking-tight text-balance">{title}</h2>
{action && (
<SpringButton href={action.href} size="lg" arrow={false}>
{action.label}
</SpringButton>
)}
</div>
</div>
<div aria-hidden className="mt-16 md:mt-20 lg:mt-24">
<Marquee speed={speed} gap={30} repeat={2}>
{images.map((src) => (
<img key={src} src={src} alt="" draggable={false} className="size-64 rounded-lg bg-muted object-cover md:size-100" />
))}
</Marquee>
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/cta-marquee.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/marquee.json https://framebrick.co/r/spring-button.json2. Add the block tokens to
app/globals.cssonce (theming).npx shadcn@latest add https://framebrick.co/r/blocks/tokens.json3. Copy the block into
components/cta-marquee.tsx.import { Marquee } from "@/components/ui/marquee"; import { SpringButton } from "@/components/ui/spring-button"; import { cn } from "@/lib/utils"; /* * CTA Marquee — a large invitation and a button above an endless strip of square photos running edge to edge. * Source: marcframe/nitip → components/sections/CtaSection.tsx (+ design-system Ticker). * * Layout kept from Framer: the heading and button sit in a column up to 56rem, left-aligned below 1024px and centered * (text still left-aligned) from 1024px; the strip runs the full width of the page under it, clipped at the edges. * Tiles are 400px square with 30px between them from 768px (256px on phones, where 400px would fill the screen). * Motion kept (marquee): the strip runs left at 30px/s and stands still with reduced motion. The strip is decorative, * so it is hidden from assistive tech. */ export type CtaMarqueeProps = { title?: string; action?: { label: string; href: string }; /** Square photos for the strip (decorative). */ images?: string[]; /** Pixels per second. */ speed?: number; className?: string; }; const photo = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=800&h=800&q=80`; const defaults = { title: "Ready to turn your brand's attention into real growth?", action: { label: "Start a project", href: "/contact" }, images: [ photo("photo-1504674900247-0877df9cc836"), photo("photo-1534438327276-14e5300c3a48"), photo("photo-1441986300917-64674bd600d8"), photo("photo-1523275335684-37898b6baf30"), photo("photo-1505740420928-5e560c06d30e"), photo("photo-1485955900006-10f4d324d411"), ], }; export function CtaMarquee({ title = defaults.title, action = defaults.action, images = defaults.images, speed = 30, className, }: CtaMarqueeProps) { return ( <section className={cn("overflow-hidden py-(--section-y)", className)}> <div className="mx-auto flex max-w-(--container) flex-col px-(--gutter)"> <div className="flex w-full max-w-4xl flex-col items-start gap-12 lg:self-center"> <h2 className="font-heading text-h1 font-semibold tracking-tight text-balance">{title}</h2> {action && ( <SpringButton href={action.href} size="lg" arrow={false}> {action.label} </SpringButton> )} </div> </div> <div aria-hidden className="mt-16 md:mt-20 lg:mt-24"> <Marquee speed={speed} gap={30} repeat={2}> {images.map((src) => ( <img key={src} src={src} alt="" draggable={false} className="size-64 rounded-lg bg-muted object-cover md:size-100" /> ))} </Marquee> </div> </section> ); }
Usage
import { CtaMarquee } from "@/components/cta-marquee";
<CtaMarquee
title="Ready to grow?"
action={{ label: "Start a project", href: "/contact" }}
images={["/work/1.jpg", "/work/2.jpg", "/work/3.jpg"]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | — | Section heading (h2), set large. |
| action | { label, href } | Start a project | Button under the heading. |
| images | string[] | 6 photos | Square photos for the strip (decorative, hidden from assistive tech). |
| speed | number | 30 | Pixels per second. |
| className | string | — | Classes for the <section>. |
- The heading column (up to 56rem) is left-aligned below 1024px and centered from 1024px; the strip runs the full page width under it.
- Tiles are 400px square, 30px apart, from 768px — 256px on phones. The strip runs left at 30px/s and stands still with reduced motion (marquee).
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS block called CtaMarquee (Next.js). - <section className="overflow-hidden py-(--section-y)">. In a container (mx-auto max-w-(--container) px-(--gutter)) a column up to max-w-4xl, centered from 1024px (text left-aligned): an h2 font-heading text-h1 semibold tracking-tight and a large shadcn-style button, gap-12. - Under it (mt-16 / md:mt-20 / lg:mt-24), outside the container, an aria-hidden strip of square photos (256px, 400px from 768px, rounded-lg, 30px apart) that scrolls left forever at 30px per second (Web Animations or Motion, duration from the width), still with prefers-reduced-motion. - Only shadcn variables and block tokens.











