Footer
Footer Columns
Brand and contact, large page links, social rows that fill on hover, a very large closing link and the copyright.
Layout and motion from the Nitip template — see it in contextimport Link from "next/link";
import { FillLink } from "@/components/ui/fill-link";
import { UnderlineLink } from "@/components/ui/underline-link";
import { cn } from "@/lib/utils";
/*
* Footer Columns — brand and contact, large page links, social rows, a very large closing link and the copyright.
* Source: marcframe/nitip → components/site/Footer.tsx (+ design-system BigButton, SocialMediaLink, BigFooter).
*
* Layout kept from Framer: a line across the top. Brand + contact stack on phones, sit side by side (bottom-aligned)
* from 768px and become a 300px column at the left from 1024px. The links area (up to 720px, at the right from
* 1024px) puts the page links beside the social rows (300px) from 768px, then the closing link under them; a line and
* the copyright close it. Top padding follows --section-y; the bottom is tighter, as on Framer.
* Motion kept: page links grow an underline and turn their arrow on hover (underline-link — its arrow turns from ↘ to
* → where Nitip's big button turns from ↗; both end pointing right); social rows fill and invert, their content nudges
* inward and the arrow turns (fill-link); the closing link grows a 2px underline. Dropped: Nitip's "More details"
* cursor label.
* The group titles are h2 — a footer's parts are top-level sections of the page — inside the footer landmark.
*/
export type FooterColumnsProps = {
brand?: { name: string; href: string; logo?: React.ReactNode };
description?: string;
email?: string;
phone?: string;
links?: { label: string; href: string }[];
socials?: { label: string; href: string; meta?: string }[];
/** Small note above the very large closing link. */
closing?: { note: string; label: string; href: string };
/** Printed after "© {year} ". */
copyright?: string;
labels?: { contact: string; social: string };
className?: string;
};
const defaults = {
brand: { name: "Northstar", href: "/" },
description: "A digital marketing and social media studio helping ambitious brands be seen, remembered and chosen.",
email: "hello@northstar.example",
phone: "+1 (555) 014 2290",
links: [
{ label: "Home", href: "/" },
{ label: "Services", href: "/services" },
{ label: "Projects", href: "/projects" },
{ label: "About", href: "/about" },
{ label: "Blog", href: "/blog" },
{ label: "Careers", href: "/careers" },
],
socials: [
{ label: "Instagram", href: "https://instagram.com" },
{ label: "LinkedIn", href: "https://linkedin.com" },
{ label: "YouTube", href: "https://youtube.com" },
{ label: "X", href: "https://x.com" },
],
closing: { note: "We would love to hear from you.", label: "Let's work together.", href: "/contact" },
copyright: "Northstar. All rights reserved.",
labels: { contact: "Contact", social: "Follow us" },
};
const textLink = "transition-colors hover:text-foreground/80 hover:underline hover:underline-offset-4";
export function FooterColumns({
brand = defaults.brand,
description = defaults.description,
email = defaults.email,
phone = defaults.phone,
links = defaults.links,
socials = defaults.socials,
closing = defaults.closing,
copyright = defaults.copyright,
labels = defaults.labels,
className,
}: FooterColumnsProps) {
return (
<footer className={cn("border-t border-border pt-(--section-y) pb-6 md:pb-8", className)}>
<div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter)">
<div className="flex flex-col gap-12 lg:flex-row lg:items-start lg:justify-between">
<div className="flex flex-col gap-8 md:flex-row md:items-end md:justify-between lg:w-75 lg:flex-col lg:items-start">
<div className="flex flex-col gap-5 md:w-88 lg:w-full">
<Link href={brand.href} className="self-start font-heading text-xl font-semibold tracking-tight">
{brand.logo ?? brand.name}
</Link>
{description && <p className="text-muted-foreground">{description}</p>}
</div>
{(email || phone) && (
<div className="flex flex-col gap-2.5 whitespace-nowrap">
<h2 className="font-medium">{labels.contact}</h2>
<ul className="flex flex-col gap-2.5 text-muted-foreground">
{email && (
<li>
<a href={`mailto:${email}`} className={textLink}>
{email}
</a>
</li>
)}
{phone && (
<li>
<a href={`tel:${phone.replace(/[^\d+]/g, "")}`} className={textLink}>
{phone}
</a>
</li>
)}
</ul>
</div>
)}
</div>
<div className="flex flex-col gap-12 md:max-w-180 lg:flex-1 lg:gap-24">
<div className="flex flex-col gap-12 md:flex-row md:items-start md:justify-between">
<nav aria-label="Footer" className="flex flex-col items-start gap-1">
{links.map((link) => (
<UnderlineLink key={link.href} href={link.href} className="gap-2 [&>svg]:size-6">
<span className="font-heading text-h3 font-semibold tracking-tight">{link.label}</span>
</UnderlineLink>
))}
</nav>
{socials.length > 0 && (
<div className="flex flex-col gap-2.5 md:w-75">
<h2 className="font-medium">{labels.social}</h2>
<ul className="border-t border-border">
{socials.map((social) => (
<li key={social.href}>
<FillLink href={social.href} meta={social.meta}>
{social.label}
</FillLink>
</li>
))}
</ul>
</div>
)}
</div>
{closing && (
<div className="flex flex-col gap-2.5">
<p className="text-muted-foreground">{closing.note}</p>
<UnderlineLink href={closing.href} arrow={false} thickness={2} className="self-start whitespace-normal">
<span className="font-heading text-display font-semibold tracking-tight">{closing.label}</span>
</UnderlineLink>
</div>
)}
</div>
</div>
<hr className="border-border" />
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} {copyright}
</p>
</div>
</footer>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/footer-columns.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/fill-link.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-columns.tsx.import Link from "next/link"; import { FillLink } from "@/components/ui/fill-link"; import { UnderlineLink } from "@/components/ui/underline-link"; import { cn } from "@/lib/utils"; /* * Footer Columns — brand and contact, large page links, social rows, a very large closing link and the copyright. * Source: marcframe/nitip → components/site/Footer.tsx (+ design-system BigButton, SocialMediaLink, BigFooter). * * Layout kept from Framer: a line across the top. Brand + contact stack on phones, sit side by side (bottom-aligned) * from 768px and become a 300px column at the left from 1024px. The links area (up to 720px, at the right from * 1024px) puts the page links beside the social rows (300px) from 768px, then the closing link under them; a line and * the copyright close it. Top padding follows --section-y; the bottom is tighter, as on Framer. * Motion kept: page links grow an underline and turn their arrow on hover (underline-link — its arrow turns from ↘ to * → where Nitip's big button turns from ↗; both end pointing right); social rows fill and invert, their content nudges * inward and the arrow turns (fill-link); the closing link grows a 2px underline. Dropped: Nitip's "More details" * cursor label. * The group titles are h2 — a footer's parts are top-level sections of the page — inside the footer landmark. */ export type FooterColumnsProps = { brand?: { name: string; href: string; logo?: React.ReactNode }; description?: string; email?: string; phone?: string; links?: { label: string; href: string }[]; socials?: { label: string; href: string; meta?: string }[]; /** Small note above the very large closing link. */ closing?: { note: string; label: string; href: string }; /** Printed after "© {year} ". */ copyright?: string; labels?: { contact: string; social: string }; className?: string; }; const defaults = { brand: { name: "Northstar", href: "/" }, description: "A digital marketing and social media studio helping ambitious brands be seen, remembered and chosen.", email: "hello@northstar.example", phone: "+1 (555) 014 2290", links: [ { label: "Home", href: "/" }, { label: "Services", href: "/services" }, { label: "Projects", href: "/projects" }, { label: "About", href: "/about" }, { label: "Blog", href: "/blog" }, { label: "Careers", href: "/careers" }, ], socials: [ { label: "Instagram", href: "https://instagram.com" }, { label: "LinkedIn", href: "https://linkedin.com" }, { label: "YouTube", href: "https://youtube.com" }, { label: "X", href: "https://x.com" }, ], closing: { note: "We would love to hear from you.", label: "Let's work together.", href: "/contact" }, copyright: "Northstar. All rights reserved.", labels: { contact: "Contact", social: "Follow us" }, }; const textLink = "transition-colors hover:text-foreground/80 hover:underline hover:underline-offset-4"; export function FooterColumns({ brand = defaults.brand, description = defaults.description, email = defaults.email, phone = defaults.phone, links = defaults.links, socials = defaults.socials, closing = defaults.closing, copyright = defaults.copyright, labels = defaults.labels, className, }: FooterColumnsProps) { return ( <footer className={cn("border-t border-border pt-(--section-y) pb-6 md:pb-8", className)}> <div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter)"> <div className="flex flex-col gap-12 lg:flex-row lg:items-start lg:justify-between"> <div className="flex flex-col gap-8 md:flex-row md:items-end md:justify-between lg:w-75 lg:flex-col lg:items-start"> <div className="flex flex-col gap-5 md:w-88 lg:w-full"> <Link href={brand.href} className="self-start font-heading text-xl font-semibold tracking-tight"> {brand.logo ?? brand.name} </Link> {description && <p className="text-muted-foreground">{description}</p>} </div> {(email || phone) && ( <div className="flex flex-col gap-2.5 whitespace-nowrap"> <h2 className="font-medium">{labels.contact}</h2> <ul className="flex flex-col gap-2.5 text-muted-foreground"> {email && ( <li> <a href={`mailto:${email}`} className={textLink}> {email} </a> </li> )} {phone && ( <li> <a href={`tel:${phone.replace(/[^\d+]/g, "")}`} className={textLink}> {phone} </a> </li> )} </ul> </div> )} </div> <div className="flex flex-col gap-12 md:max-w-180 lg:flex-1 lg:gap-24"> <div className="flex flex-col gap-12 md:flex-row md:items-start md:justify-between"> <nav aria-label="Footer" className="flex flex-col items-start gap-1"> {links.map((link) => ( <UnderlineLink key={link.href} href={link.href} className="gap-2 [&>svg]:size-6"> <span className="font-heading text-h3 font-semibold tracking-tight">{link.label}</span> </UnderlineLink> ))} </nav> {socials.length > 0 && ( <div className="flex flex-col gap-2.5 md:w-75"> <h2 className="font-medium">{labels.social}</h2> <ul className="border-t border-border"> {socials.map((social) => ( <li key={social.href}> <FillLink href={social.href} meta={social.meta}> {social.label} </FillLink> </li> ))} </ul> </div> )} </div> {closing && ( <div className="flex flex-col gap-2.5"> <p className="text-muted-foreground">{closing.note}</p> <UnderlineLink href={closing.href} arrow={false} thickness={2} className="self-start whitespace-normal"> <span className="font-heading text-display font-semibold tracking-tight">{closing.label}</span> </UnderlineLink> </div> )} </div> </div> <hr className="border-border" /> <p className="text-sm text-muted-foreground"> © {new Date().getFullYear()} {copyright} </p> </div> </footer> ); }
Usage
import { FooterColumns } from "@/components/footer-columns";
<FooterColumns
brand={{ name: "Northstar", href: "/" }}
links={[{ label: "Home", href: "/" }, { label: "Services", href: "/services" }]}
socials={[{ label: "Instagram", href: "https://instagram.com/northstar" }]}
closing={{ note: "We would love to hear from you.", label: "Let's work together.", href: "/contact" }}
copyright="Northstar. All rights reserved."
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| brand | { name, href, logo? } | — | Brand at the top left; logo (any node) replaces the name. |
| description | string | — | Short text under the brand. |
| email / phone | string | — | mailto and tel links under Contact. |
| links | { label, href }[] | 6 links | Large page links in the footer navigation. |
| socials | { label, href, meta? }[] | 4 networks | Rows under Follow us; external links open in a new tab. |
| closing | { note, label, href } | — | Small note above a very large link. |
| copyright | string | — | Printed after "© {current year} ". |
| labels | { contact, social } | — | Group titles (h2). |
| className | string | — | Classes for the <footer>. |
- Brand + contact stack on phones, sit side by side from 768px and become a 300px column at the left from 1024px; the links area (up to 720px) puts the page links beside the social rows from 768px.
- Page links grow an underline and turn their arrow on hover (underline-link); social rows fill and invert with their content nudging inward (fill-link); the closing link grows a 2px underline.
- The group titles are h2 inside the footer landmark; the navigation is labelled Footer.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS + Motion block called FooterColumns (Next.js).
- <footer className="border-t border-border pt-(--section-y) pb-6 md:pb-8">, container mx-auto max-w-(--container) px-(--gutter), column gap-12.
- Top: stacked; from 1024px a row. Left (300px from 1024px): brand (font-heading text-xl semibold) and a muted description; "Contact" (h2,
font-medium) with mailto and tel links — beside the brand from 768px (items-end), under it from 1024px.
- Right (max 720px, flex-1 from 1024px): a <nav aria-label="Footer"> of large links (font-heading text-h3 semibold) whose 1px underline grows
from the left on hover and whose arrow turns to point right (spring bounce 0.2, 0.4s); beside it from 768px a 300px "Follow us" (h2) list
of full-width rows (border-b, py-3) that fill with the foreground color and invert on hover, label and arrow nudging 12px inward.
Under both: a muted note and a very large link (font-heading text-display) with a 2px underline that grows on hover.
- An <hr> and "© {year} …" in text-sm muted.
- Only shadcn variables and block tokens.










