Hero
Hero Showreel
A short line beside a display heading, a text block with a corner mark and two buttons, and a small showreel of your work.
Layout and motion from the Kalme template — see it in contextimport { FlipButton } from "@/components/ui/flip-button";
import { Showreel } from "@/components/ui/showreel";
import { cn } from "@/lib/utils";
/*
* Hero Showreel — a short line beside a display heading, then a text block with a corner mark and two buttons, and a
* small showreel of your work that cuts to the next image every two seconds, with its caption.
* Source: marcframe/kalme → components/sections/HomeHeroSection.tsx (+ design-system Showreel, Button).
*
* Layout kept from Framer: below 1024px the line sits above the heading; from 1024px it takes a 150px column beside
* the heading (up to 750px), 48px apart. Under them, 32px apart (48px from 768px): on phones the showreel first, full
* width (5:3), and the text under it; from 768px the text at the left and the showreel (250px wide, 300px from 1024px)
* at the right, aligned to the top (to the bottom from 1024px, where the text is indented by 200px). The text block:
* a 14px corner mark (two 2px lines, top and right), then the text (up to 325px) and the two buttons.
* Motion kept: the showreel cuts to the next image every 2s with no transition, starting over after the last
* (showreel); it holds the first image with reduced motion. The buttons roll their label (flip-button).
* Dropped: Kalme's dot on the buttons. The showreel images are decorative.
*/
export type HeroShowreelProps = {
/** Heading level: h1 when the hero opens the page. */
as?: "h1" | "h2";
/** Short line beside (or above) the heading. */
label?: string;
title?: string;
text?: string;
/** Filled and outlined buttons. */
actions?: { label: string; href: string }[];
/** Images the showreel cycles through (decorative). */
images?: string[];
/** Two short lines under the showreel. */
caption?: [string, string?];
/** Milliseconds each image stays. */
interval?: number;
className?: string;
};
const photo = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=1200&q=80`;
const defaults = {
label: "Digital marketing that drives growth",
title: "Lift your brand with work people remember",
text: "From creative campaigns to data-led strategy, we help you reach, engage and convert — wherever your customers are.",
actions: [
{ label: "Start a project", href: "/contact" },
{ label: "Our case studies", href: "/projects" },
],
images: [
photo("photo-1504674900247-0877df9cc836"),
photo("photo-1534438327276-14e5300c3a48"),
photo("photo-1441986300917-64674bd600d8"),
photo("photo-1523275335684-37898b6baf30"),
],
caption: ["Studio showreel", "(Clients — 2018/2026)"] as [string, string?],
};
export function HeroShowreel({
as: Heading = "h1",
label = defaults.label,
title = defaults.title,
text = defaults.text,
actions = defaults.actions,
images = defaults.images,
caption = defaults.caption,
interval = 2000,
className,
}: HeroShowreelProps) {
const [primary, secondary] = actions;
return (
<section className={cn("py-(--section-y)", className)}>
<div className="mx-auto flex max-w-(--container) flex-col gap-8 px-(--gutter) md:gap-12">
<div className="flex flex-col items-start gap-2.5 lg:flex-row lg:gap-12">
{label && <p className="text-muted-foreground lg:w-37.5 lg:shrink-0 lg:pt-4">{label}</p>}
<Heading className="font-heading text-display font-semibold tracking-tight text-balance md:max-w-175 lg:w-187.5 lg:max-w-none">
{title}
</Heading>
</div>
<div className="flex flex-col gap-12 md:flex-row md:items-start md:justify-between lg:items-end">
<div className="order-2 flex items-start gap-2.5 md:order-1 lg:pl-50">
<span aria-hidden className="shrink-0 pt-1.5 pr-4">
<span className="block size-3.5 border-t-2 border-r-2 border-foreground" />
</span>
<div className="flex max-w-81.25 flex-col items-start gap-7.5">
{text && <p className="text-lead text-pretty">{text}</p>}
{actions.length > 0 && (
<div className="flex flex-wrap items-center gap-2.5">
{primary && <FlipButton href={primary.href}>{primary.label}</FlipButton>}
{secondary && (
<FlipButton href={secondary.href} variant="outline">
{secondary.label}
</FlipButton>
)}
</div>
)}
</div>
</div>
{images.length > 0 && (
<div className="order-1 flex w-full flex-col items-start gap-1.5 md:order-2 md:w-62.5 lg:w-75">
<Showreel
interval={interval}
className="aspect-[5/3] w-full rounded-xl"
items={images.map((src) => (
<img key={src} src={src} alt="" className="object-cover" />
))}
/>
{caption[0] && (
<div className="text-sm text-muted-foreground">
<p>{caption[0]}</p>
{caption[1] && <p>{caption[1]}</p>}
</div>
)}
</div>
)}
</div>
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/hero-showreel.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/flip-button.json https://framebrick.co/r/showreel.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/hero-showreel.tsx.import { FlipButton } from "@/components/ui/flip-button"; import { Showreel } from "@/components/ui/showreel"; import { cn } from "@/lib/utils"; /* * Hero Showreel — a short line beside a display heading, then a text block with a corner mark and two buttons, and a * small showreel of your work that cuts to the next image every two seconds, with its caption. * Source: marcframe/kalme → components/sections/HomeHeroSection.tsx (+ design-system Showreel, Button). * * Layout kept from Framer: below 1024px the line sits above the heading; from 1024px it takes a 150px column beside * the heading (up to 750px), 48px apart. Under them, 32px apart (48px from 768px): on phones the showreel first, full * width (5:3), and the text under it; from 768px the text at the left and the showreel (250px wide, 300px from 1024px) * at the right, aligned to the top (to the bottom from 1024px, where the text is indented by 200px). The text block: * a 14px corner mark (two 2px lines, top and right), then the text (up to 325px) and the two buttons. * Motion kept: the showreel cuts to the next image every 2s with no transition, starting over after the last * (showreel); it holds the first image with reduced motion. The buttons roll their label (flip-button). * Dropped: Kalme's dot on the buttons. The showreel images are decorative. */ export type HeroShowreelProps = { /** Heading level: h1 when the hero opens the page. */ as?: "h1" | "h2"; /** Short line beside (or above) the heading. */ label?: string; title?: string; text?: string; /** Filled and outlined buttons. */ actions?: { label: string; href: string }[]; /** Images the showreel cycles through (decorative). */ images?: string[]; /** Two short lines under the showreel. */ caption?: [string, string?]; /** Milliseconds each image stays. */ interval?: number; className?: string; }; const photo = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=1200&q=80`; const defaults = { label: "Digital marketing that drives growth", title: "Lift your brand with work people remember", text: "From creative campaigns to data-led strategy, we help you reach, engage and convert — wherever your customers are.", actions: [ { label: "Start a project", href: "/contact" }, { label: "Our case studies", href: "/projects" }, ], images: [ photo("photo-1504674900247-0877df9cc836"), photo("photo-1534438327276-14e5300c3a48"), photo("photo-1441986300917-64674bd600d8"), photo("photo-1523275335684-37898b6baf30"), ], caption: ["Studio showreel", "(Clients — 2018/2026)"] as [string, string?], }; export function HeroShowreel({ as: Heading = "h1", label = defaults.label, title = defaults.title, text = defaults.text, actions = defaults.actions, images = defaults.images, caption = defaults.caption, interval = 2000, className, }: HeroShowreelProps) { const [primary, secondary] = actions; return ( <section className={cn("py-(--section-y)", className)}> <div className="mx-auto flex max-w-(--container) flex-col gap-8 px-(--gutter) md:gap-12"> <div className="flex flex-col items-start gap-2.5 lg:flex-row lg:gap-12"> {label && <p className="text-muted-foreground lg:w-37.5 lg:shrink-0 lg:pt-4">{label}</p>} <Heading className="font-heading text-display font-semibold tracking-tight text-balance md:max-w-175 lg:w-187.5 lg:max-w-none"> {title} </Heading> </div> <div className="flex flex-col gap-12 md:flex-row md:items-start md:justify-between lg:items-end"> <div className="order-2 flex items-start gap-2.5 md:order-1 lg:pl-50"> <span aria-hidden className="shrink-0 pt-1.5 pr-4"> <span className="block size-3.5 border-t-2 border-r-2 border-foreground" /> </span> <div className="flex max-w-81.25 flex-col items-start gap-7.5"> {text && <p className="text-lead text-pretty">{text}</p>} {actions.length > 0 && ( <div className="flex flex-wrap items-center gap-2.5"> {primary && <FlipButton href={primary.href}>{primary.label}</FlipButton>} {secondary && ( <FlipButton href={secondary.href} variant="outline"> {secondary.label} </FlipButton> )} </div> )} </div> </div> {images.length > 0 && ( <div className="order-1 flex w-full flex-col items-start gap-1.5 md:order-2 md:w-62.5 lg:w-75"> <Showreel interval={interval} className="aspect-[5/3] w-full rounded-xl" items={images.map((src) => ( <img key={src} src={src} alt="" className="object-cover" /> ))} /> {caption[0] && ( <div className="text-sm text-muted-foreground"> <p>{caption[0]}</p> {caption[1] && <p>{caption[1]}</p>} </div> )} </div> )} </div> </div> </section> ); }
Usage
import { HeroShowreel } from "@/components/hero-showreel";
<HeroShowreel
label="Digital marketing that drives growth"
title="Lift your brand with work people remember"
actions={[
{ label: "Start a project", href: "/contact" },
{ label: "Our case studies", href: "/projects" },
]}
images={["/work/one.jpg", "/work/two.jpg", "/work/three.jpg", "/work/four.jpg"]}
caption={["Studio showreel", "(Clients — 2018/2026)"]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| as | "h1" | "h2" | "h1" | Heading level — h1 when the hero opens the page. |
| label / title / text | string | — | The short line (a 150px column beside the heading from 1024px), the display heading and the text under the corner mark. |
| actions | { label, href }[] | — | Up to two buttons: filled, then outlined. |
| images | string[] | 4 photos | Frames of the showreel (decorative, 5:3). |
| caption | [string, string?] | — | Two short lines under the showreel. |
| interval | number | 2000 | Milliseconds each frame stays. |
| className | string | — | Classes for the <section>. |
- On phones the showreel comes first, full width, and the text under it; from 768px the text sits at the left and the showreel (250px, 300px from 1024px) at the right; from 1024px the text is indented 200px and both align to the bottom.
- The showreel cuts to the next frame every 2s with no transition and holds the first with reduced motion (showreel). The buttons roll their label (flip-button).
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS block called HeroShowreel (Next.js). - <section className="py-(--section-y)">, container mx-auto max-w-(--container) px-(--gutter), column gap-8 / md:gap-12. - Title row: a muted line above the h1 (font-heading text-display semibold); from 1024px the line is a 150px column beside the heading (750px, pt-4 on the line, gap-12). - Details: phones — a 5:3 showreel (rounded-xl) full width, then the text; from 768px a row (justify-between, items-start; items-end from 1024px): the text block (lg: pl-50) and the showreel (250px / 300px wide) with two muted text-sm caption lines under it. - Text block: a 14px corner mark (border-t-2 border-r-2 border-foreground, pt-1.5 pr-4 around it), then the text (text-lead, max 325px) and two buttons (filled, outline) whose label rolls up on hover (spring bounce 0, 0.4s). - Showreel: frames stacked, the next one shown every 2s with no transition (the others invisible); the first only with prefers-reduced-motion. - Only shadcn variables and block tokens; plain <img>.











