A night footer that ends on a giant wordmark. Point at a link column, or Tab into it, and the Echo Reel at the foot turns to that column's name and inks it in, then turns back to the brand a beat after you leave. With a newsletter field, three link columns, the legal line and a status light.
registry/blocks/echo-reel-footer.tsx
"use client";
import * as React from "react";
import { EchoReel } from "@/components/ui/echo-reel";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the footer yours: the
* palette, the columns, the newsletter and the legal line.
* The giant wordmark at the foot is an Echo Reel: point at or
* Tab into a column and it turns to that column's name, then
* back to yours a beat after you leave.
* ─────────────────────────────────────────────────────────── */
/** A night band in both schemes, with the violet as the ink. */
const PALETTE = { paper: "#07070c", ink: "#f5f5f7", accent: "#9b83ff", lime: "#c6ff3d" };
const BRAND = { name: "Loupe", line: "Studio product photos from one phone shot.", href: "#" };
const COLUMNS = [
{
title: "Product",
links: [
{ label: "Cutouts", href: "#cutouts" },
{ label: "Scenes", href: "#scenes" },
{ label: "Colorways", href: "#colorways" },
{ label: "Pricing", href: "#pricing" },
],
},
{
title: "Company",
links: [
{ label: "About", href: "#about" },
{ label: "Careers", href: "#careers" },
{ label: "Press kit", href: "#press" },
],
},
{
title: "Help",
links: [
{ label: "Guides", href: "#guides" },
{ label: "Changelog", href: "#changelog" },
{ label: "Status", href: "#status" },
{ label: "Contact", href: "#contact" },
],
},
];
const NEWSLETTER = { label: "One email a month with what we shipped.", placeholder: "you@shop.com", button: "Subscribe", done: "Subscribed" };
const LEGAL = { owner: "© 2026 Loupe Labs, Inc.", links: [{ label: "Privacy", href: "#privacy" }, { label: "Terms", href: "#terms" }] };
const STATUS = "All systems normal";
/** How long the wordmark waits before turning back to the brand. */
const RETURN_MS = 250;
/* ── Section ─────────────────────────────────────────────── */
const WORDS = [BRAND.name, ...COLUMNS.map((column) => column.title)];
export function EchoReelFooter() {
const [word, setWord] = React.useState(0);
const [subscribed, setSubscribed] = React.useState(false);
const back = React.useRef<number | undefined>(undefined);
const show = (index: number) => {
window.clearTimeout(back.current);
setWord(index);
};
// A beat before turning back, so moving from one column to the next turns the reel once.
const release = () => {
window.clearTimeout(back.current);
back.current = window.setTimeout(() => setWord(0), RETURN_MS);
};
React.useEffect(() => () => window.clearTimeout(back.current), []);
return (
<footer
style={{ "--paper": PALETTE.paper, "--ink": PALETTE.ink, "--accent": PALETTE.accent, "--lime": PALETTE.lime } as React.CSSProperties}
className="relative overflow-hidden bg-(--paper) text-(--ink)"
>
<div className="mx-auto max-w-6xl px-6 pt-16 sm:pt-20">
<div className="grid gap-12 lg:grid-cols-[minmax(0,1fr)_auto] lg:gap-20">
<div className="max-w-sm">
<a
href={BRAND.href}
className="inline-flex items-center gap-2 rounded-md text-[17px] font-semibold tracking-[-0.03em] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent)"
>
<LoupeMark className="size-6" />
{BRAND.name}
</a>
<p className="mt-3 text-[15px] leading-relaxed opacity-60">{BRAND.line}</p>
<form
className="mt-7"
onSubmit={(e) => {
e.preventDefault();
setSubscribed(true);
}}
>
<label htmlFor="loupe-newsletter" className="text-[13px] font-medium opacity-80">
{NEWSLETTER.label}
</label>
<div className="mt-2.5 flex items-center gap-1.5 rounded-full bg-white/[0.06] p-1.5 ring-1 ring-white/10 focus-within:ring-(--accent)">
<input
id="loupe-newsletter"
type="email"
required
placeholder={NEWSLETTER.placeholder}
className="h-9 min-w-0 flex-1 bg-transparent px-3 text-sm outline-none placeholder:text-white/35"
/>
<button
type="submit"
className="h-9 shrink-0 rounded-full bg-(--accent) px-4 text-sm font-semibold text-(--paper) transition-[opacity,scale] duration-150 hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.97]"
>
{subscribed ? NEWSLETTER.done : NEWSLETTER.button}
</button>
</div>
</form>
</div>
<nav aria-label="Footer" className="grid grid-cols-2 gap-x-12 gap-y-10 sm:grid-cols-3">
{COLUMNS.map((column, i) => (
<div
key={column.title}
onPointerEnter={() => show(i + 1)}
onPointerLeave={release}
onFocus={() => show(i + 1)}
onBlur={release}
>
<p className="text-[13px] font-semibold text-(--accent)">{column.title}</p>
<ul className="mt-4 space-y-2.5">
{column.links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="-mx-1 rounded px-1 py-0.5 text-[14px] opacity-65 transition-opacity duration-150 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-(--accent)"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</nav>
</div>
<div className="mt-14 flex flex-col gap-4 border-t border-white/10 pt-6 text-[13px] sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 opacity-55">
<span>{LEGAL.owner}</span>
{LEGAL.links.map((link) => (
<a key={link.label} href={link.href} className="rounded hover:opacity-100 focus-visible:outline-2 focus-visible:outline-(--accent)">
{link.label}
</a>
))}
</div>
<p className="flex items-center gap-2 opacity-70">
<span aria-hidden className="size-2 rounded-full bg-(--lime) shadow-[0_0_0_3px_rgb(198_255_61/0.15)]" />
{STATUS}
</p>
</div>
</div>
{/* The wordmark sits on the foot of the page; its lower proof runs off the edge. */}
<div className="mx-auto mt-6 -mb-[13%] max-w-5xl px-2 sm:px-6 lg:-mb-[12%]">
<EchoReel words={WORDS} value={word} onValueChange={(index) => setWord(index)} interval={false} className="text-(--accent)" />
</div>
</footer>
);
}
/* ── Mark ────────────────────────────────────────────────── */
function LoupeMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden className={className}>
<circle cx="10.5" cy="10.5" r="6.5" stroke="currentColor" strokeWidth="2.6" />
<path d="M15.5 15.5 20 20" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" />
<circle cx="10.5" cy="10.5" r="2.6" fill="var(--accent)" />
</svg>
);
}
Live preview and the components it's built with: https://keyframeui.com/blocks/footers/echo-reel-footer