Footer
Footer Logo
A calm footer: a large wordmark, a statement with an action beside three link columns, and the legal line.
Layout and motion from the Kalme template — see it in contextimport * as React from "react";
import Link from "next/link";
import { FlipButton } from "@/components/ui/flip-button";
import { UnderlineLink } from "@/components/ui/underline-link";
import { cn } from "@/lib/utils";
/*
* Footer Logo — a calm footer: the brand as a large wordmark, a line, a short statement with an action beside three
* link columns, another line, and the copyright with the legal links.
* Source: marcframe/kalme → components/site/Footer.tsx (+ design-system TextLink, Button).
*
* Layout kept from Framer: on the page background with a line on top. The wordmark, a line, then the main row 48px
* apart; the main row and the bottom row are split by a line, 64px apart (80px from 768px, 96px from 1024px).
* Main row: below 768px the statement and button stack (up to 350px) above a grid of two link columns with the third
* across the row under them; from 768px the statement and button share a row above the three columns; from 1024px the
* statement and button (350px) sit at the left and the columns at the right, 96px apart. Bottom row: links above the
* centred copyright on phones; copyright left, links right from 768px.
* Motion kept: every link underlines from the left on hover (underline-link; Kalme uses a no-bounce spring, the
* component a 0.2 bounce); the button rolls its label (flip-button). The column titles are h2 — a footer's parts are
* top-level sections of the page — inside the footer landmark.
*/
type LinkItem = { label: string; href: string };
export type FooterLogoProps = {
/** The wordmark at the top; `logo` (any node, e.g. an SVG) replaces the name. */
brand?: { name: string; href: string; logo?: React.ReactNode };
text?: string;
action?: LinkItem;
/** Two or three columns; the second goes across the row on phones. */
columns?: { title: string; links: LinkItem[] }[];
/** "{year}" becomes the current year. */
copyright?: string;
legal?: LinkItem[];
className?: string;
};
const defaults = {
brand: { name: "Kalme", href: "/" },
text: "A digital marketing studio for brands that want growth they can explain. Tell us where you want to be next year.",
action: { label: "Schedule a call", href: "/contact" },
columns: [
{
title: "Quick links",
links: [
{ label: "About", href: "/about" },
{ label: "Services", href: "/services" },
{ label: "Projects", href: "/projects" },
{ label: "Blog", href: "/blog" },
],
},
{
title: "Get in touch",
links: [
{ label: "hello@kalme.example", href: "mailto:hello@kalme.example" },
{ label: "+1 (555) 014 2290", href: "tel:+15550142290" },
],
},
{
title: "Follow us",
links: [
{ label: "Instagram", href: "https://instagram.com" },
{ label: "LinkedIn", href: "https://linkedin.com" },
{ label: "Dribbble", href: "https://dribbble.com" },
],
},
],
copyright: "© {year} Kalme. All rights reserved.",
legal: [
{ label: "Privacy policy", href: "/privacy" },
{ label: "Terms", href: "/terms" },
],
};
export function FooterLogo({
brand = defaults.brand,
text = defaults.text,
action = defaults.action,
columns = defaults.columns,
copyright = defaults.copyright,
legal = defaults.legal,
className,
}: FooterLogoProps) {
return (
<footer className={cn("border-t pt-(--section-y) pb-5 md:pb-6 lg:pb-7.5", className)}>
<div className="mx-auto flex max-w-(--container) flex-col gap-16 px-(--gutter) md:gap-20 lg:gap-24">
<div className="flex flex-col gap-12">
<Link
href={brand.href}
className="w-fit rounded-md font-heading text-display leading-none font-semibold tracking-tight outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
{brand.logo ?? brand.name}
</Link>
<hr className="border-border" />
<div className="flex flex-col gap-12 lg:flex-row lg:items-start lg:justify-between lg:gap-24">
<div className="flex max-w-87.5 flex-col items-start gap-7.5 md:max-w-none md:flex-row md:items-end md:justify-between lg:max-w-87.5 lg:flex-col lg:items-start lg:justify-start">
{text && <p className="text-pretty md:max-w-100 lg:max-w-none">{text}</p>}
{action && <FlipButton href={action.href}>{action.label}</FlipButton>}
</div>
{columns.length > 0 && (
<div className="grid grid-cols-2 gap-x-7.5 gap-y-12 md:flex md:justify-between lg:gap-24">
{columns.map((column, i) => (
<div
key={column.title}
className={cn("flex min-w-0 flex-col items-start gap-4", columns.length === 3 && i === 1 && "col-span-2 row-start-2")}
>
<h2 className="text-muted-foreground">{column.title}</h2>
<ul className="flex flex-col items-start gap-2.5">
{column.links.map((link) => (
<li key={link.href} className="flex">
<UnderlineLink href={link.href} arrow={false}>
<span className="text-base font-normal">{link.label}</span>
</UnderlineLink>
</li>
))}
</ul>
</div>
))}
</div>
)}
</div>
</div>
<div className="flex flex-col-reverse items-center gap-5 border-t pt-16 md:flex-row md:justify-between md:pt-20 lg:pt-24">
<p>{copyright.replace("{year}", String(new Date().getFullYear()))}</p>
{legal.length > 0 && (
<ul className="flex flex-wrap items-center justify-center gap-5">
{legal.map((link) => (
<li key={link.href} className="flex">
<UnderlineLink href={link.href} arrow={false}>
<span className="text-base font-normal">{link.label}</span>
</UnderlineLink>
</li>
))}
</ul>
)}
</div>
</div>
</footer>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/footer-logo.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/underline-link.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/footer-logo.tsx.import * as React from "react"; import Link from "next/link"; import { FlipButton } from "@/components/ui/flip-button"; import { UnderlineLink } from "@/components/ui/underline-link"; import { cn } from "@/lib/utils"; /* * Footer Logo — a calm footer: the brand as a large wordmark, a line, a short statement with an action beside three * link columns, another line, and the copyright with the legal links. * Source: marcframe/kalme → components/site/Footer.tsx (+ design-system TextLink, Button). * * Layout kept from Framer: on the page background with a line on top. The wordmark, a line, then the main row 48px * apart; the main row and the bottom row are split by a line, 64px apart (80px from 768px, 96px from 1024px). * Main row: below 768px the statement and button stack (up to 350px) above a grid of two link columns with the third * across the row under them; from 768px the statement and button share a row above the three columns; from 1024px the * statement and button (350px) sit at the left and the columns at the right, 96px apart. Bottom row: links above the * centred copyright on phones; copyright left, links right from 768px. * Motion kept: every link underlines from the left on hover (underline-link; Kalme uses a no-bounce spring, the * component a 0.2 bounce); the button rolls its label (flip-button). The column titles are h2 — a footer's parts are * top-level sections of the page — inside the footer landmark. */ type LinkItem = { label: string; href: string }; export type FooterLogoProps = { /** The wordmark at the top; `logo` (any node, e.g. an SVG) replaces the name. */ brand?: { name: string; href: string; logo?: React.ReactNode }; text?: string; action?: LinkItem; /** Two or three columns; the second goes across the row on phones. */ columns?: { title: string; links: LinkItem[] }[]; /** "{year}" becomes the current year. */ copyright?: string; legal?: LinkItem[]; className?: string; }; const defaults = { brand: { name: "Kalme", href: "/" }, text: "A digital marketing studio for brands that want growth they can explain. Tell us where you want to be next year.", action: { label: "Schedule a call", href: "/contact" }, columns: [ { title: "Quick links", links: [ { label: "About", href: "/about" }, { label: "Services", href: "/services" }, { label: "Projects", href: "/projects" }, { label: "Blog", href: "/blog" }, ], }, { title: "Get in touch", links: [ { label: "hello@kalme.example", href: "mailto:hello@kalme.example" }, { label: "+1 (555) 014 2290", href: "tel:+15550142290" }, ], }, { title: "Follow us", links: [ { label: "Instagram", href: "https://instagram.com" }, { label: "LinkedIn", href: "https://linkedin.com" }, { label: "Dribbble", href: "https://dribbble.com" }, ], }, ], copyright: "© {year} Kalme. All rights reserved.", legal: [ { label: "Privacy policy", href: "/privacy" }, { label: "Terms", href: "/terms" }, ], }; export function FooterLogo({ brand = defaults.brand, text = defaults.text, action = defaults.action, columns = defaults.columns, copyright = defaults.copyright, legal = defaults.legal, className, }: FooterLogoProps) { return ( <footer className={cn("border-t pt-(--section-y) pb-5 md:pb-6 lg:pb-7.5", className)}> <div className="mx-auto flex max-w-(--container) flex-col gap-16 px-(--gutter) md:gap-20 lg:gap-24"> <div className="flex flex-col gap-12"> <Link href={brand.href} className="w-fit rounded-md font-heading text-display leading-none font-semibold tracking-tight outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" > {brand.logo ?? brand.name} </Link> <hr className="border-border" /> <div className="flex flex-col gap-12 lg:flex-row lg:items-start lg:justify-between lg:gap-24"> <div className="flex max-w-87.5 flex-col items-start gap-7.5 md:max-w-none md:flex-row md:items-end md:justify-between lg:max-w-87.5 lg:flex-col lg:items-start lg:justify-start"> {text && <p className="text-pretty md:max-w-100 lg:max-w-none">{text}</p>} {action && <FlipButton href={action.href}>{action.label}</FlipButton>} </div> {columns.length > 0 && ( <div className="grid grid-cols-2 gap-x-7.5 gap-y-12 md:flex md:justify-between lg:gap-24"> {columns.map((column, i) => ( <div key={column.title} className={cn("flex min-w-0 flex-col items-start gap-4", columns.length === 3 && i === 1 && "col-span-2 row-start-2")} > <h2 className="text-muted-foreground">{column.title}</h2> <ul className="flex flex-col items-start gap-2.5"> {column.links.map((link) => ( <li key={link.href} className="flex"> <UnderlineLink href={link.href} arrow={false}> <span className="text-base font-normal">{link.label}</span> </UnderlineLink> </li> ))} </ul> </div> ))} </div> )} </div> </div> <div className="flex flex-col-reverse items-center gap-5 border-t pt-16 md:flex-row md:justify-between md:pt-20 lg:pt-24"> <p>{copyright.replace("{year}", String(new Date().getFullYear()))}</p> {legal.length > 0 && ( <ul className="flex flex-wrap items-center justify-center gap-5"> {legal.map((link) => ( <li key={link.href} className="flex"> <UnderlineLink href={link.href} arrow={false}> <span className="text-base font-normal">{link.label}</span> </UnderlineLink> </li> ))} </ul> )} </div> </div> </footer> ); }
Usage
import { FooterLogo } from "@/components/footer-logo";
<FooterLogo
brand={{ name: "Kalme", href: "/" }}
text="A digital marketing studio for brands that want growth they can explain."
action={{ label: "Schedule a call", href: "/contact" }}
columns={[
{ title: "Quick links", links: [{ label: "About", href: "/about" }] },
{ title: "Get in touch", links: [{ label: "hello@example.com", href: "mailto:hello@example.com" }] },
{ title: "Follow us", links: [{ label: "Instagram", href: "https://instagram.com" }] },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| brand | { name, href, logo? } | Kalme | The large wordmark at the top; logo (any node, e.g. an SVG) replaces the name. |
| text / action | string / { label, href } | — | The statement and the button beside the columns. |
| columns | { title, links }[] | 3 columns | Two or three; with three, the second goes across the row on phones. Titles are h2. |
| copyright | string | © {year} Kalme… | "{year}" becomes the current year. |
| legal | { label, href }[] | — | Links at the right of the bottom row (above the copyright on phones). |
| className | string | — | Classes for the <footer>. |
- On the page background with a line on top. Below 768px the statement and button stack above two link columns with the third under them; from 768px they share a row above the columns; from 1024px they sit at the left, the columns at the right.
- Every link underlines from the left on hover (underline-link); the button rolls its 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 FooterLogo (Next.js). - <footer className="border-t pt-(--section-y) pb-5 md:pb-6 lg:pb-7.5">, container mx-auto max-w-(--container) px-(--gutter), column gap-16 / md:gap-20 / lg:gap-24. - The brand as a link in font-heading text-display semibold (leading-none), an <hr>, then the main row (gap-12): a statement and a button (label rolls up on hover) — stacked up to 350px on phones, one row from 768px, a 350px column at the left from 1024px — and the link columns (grid of 2 on phones with the second column across the row below, a spaced row from 768px, gap-24 from 1024px): a muted h2 title, then links (gap-2.5) whose 1px underline grows from the left on hover. - Bottom row (border-t, pt-16 / 20 / 24): the copyright with the current year and the legal links — links above the centred copyright on phones, copyright left and links right from 768px. - Only shadcn variables and block tokens.











