39 / Blocks
Signature Footer
A complete closing section with room for the useful links first and one oversized wordmark at the end.
"use client"
import { DemoVariants } from "@/components/demos/demo-variants"
import {
SignatureFooter,
type FooterColumn,
} from "mischief-ui/signature-footer"
const columns: FooterColumn[] = [
{
label: "Product",
links: [
{ label: "Pricing", href: "#" },
{ label: "Changelog", href: "#" },
{ label: "Dashboard", href: "#" },
],
},
{
label: "Free tools",
links: [
{ label: "Link shortener", href: "#" },
{ label: "QR generator", href: "#" },
{ label: "UTM builder", href: "#" },
],
},
{
label: "Company",
links: [
{ label: "Docs", href: "#", external: true },
{ label: "Brand", href: "#" },
{ label: "Contact", href: "#" },
],
},
]
const related: FooterColumn = {
label: "Other products",
links: [
{ label: "Maillayer", href: "#" },
{ label: "Post Scheduler", href: "#" },
{ label: "Traffic Source", href: "#" },
{ label: "Meta Tags", href: "#" },
{ label: "Blog CMS", href: "#" },
],
}
const frame =
"w-full rounded-[var(--radius)] [&_[data-slot=signature-footer-inner]]:px-6 [&_[data-slot=signature-footer-inner]]:pt-8 [&_[data-slot=signature-footer-wordmark]]:pt-4 [&_[data-slot=signature-footer-wordmark]]:text-[clamp(3rem,9vw,7rem)]"
function Status() {
return (
<span className="inline-flex items-center gap-2">
<span aria-hidden="true" className="bg-accent size-1.5 rounded-full" />
All systems operational
</span>
)
}
export function SignatureFooterDemo() {
return (
<DemoVariants
label="Footer"
variants={[
{
id: "directory",
label: "Directory",
render: () => (
<SignatureFooter
className={frame}
heading={<strong className="text-xl">Northstar</strong>}
description="Short links and file sharing with real-time analytics."
columns={columns}
related={related}
brand={<span>© 2026 Northstar</span>}
legal={
<span className="flex gap-5">
<a className="no-underline" href="#">
Terms
</a>
<a className="no-underline" href="#">
Privacy
</a>
</span>
}
status={<Status />}
wordmark="northstar"
/>
),
},
{
id: "light",
label: "On a light ground",
render: () => (
<SignatureFooter
className={`${frame} bg-card text-card-foreground border-border border`}
heading={<strong className="text-xl">Northstar</strong>}
description="Every shade is mixed from the text colour, so nothing has to be restyled."
columns={columns.slice(0, 2)}
related={related}
brand={<span>© 2026 Northstar</span>}
status={<Status />}
wordmark="northstar"
/>
),
},
{
id: "statement",
label: "Statement",
render: () => (
<SignatureFooter
className={frame}
eyebrow="One last useful thought"
heading="Make the ending memorable."
description="Keep the links practical. Let the wordmark do the rest."
brand={<strong>Northstar</strong>}
meta={<span>Made with care.</span>}
wordmark="Northstar"
/>
),
},
]}
/>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/signature-footerimport { SignatureFooter } from "mischief-ui/signature-footer"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
import * as React from "react" import { cn } from "@/lib/utils"import { FOOTER_LABEL, FooterColumns, type FooterColumn, type FooterLink, type FooterLinkRenderer,} from "@/registry/default/footer-columns/footer-columns"import { FooterRow } from "@/registry/default/footer-row/footer-row"import { FooterWordmark } from "@/registry/default/footer-wordmark/footer-wordmark" export type { FooterColumn, FooterLink, FooterLinkRenderer }Usage
export function Footer() {
return (
<SignatureFooter
heading={<Logo />}
description="Short links and file sharing with real-time analytics."
columns={[
{ label: "Product", links: [{ label: "Pricing", href: "/pricing" }] },
{ label: "Company", links: [{ label: "Docs", href: docsUrl, external: true }] },
]}
related={{
label: "Other products",
links: otherProducts,
}}
renderLink={({ href, label }) => <Link href={href}>{label}</Link>}
brand={<span>© 2026 Northstar</span>}
legal={<LegalLinks />}
status={<SystemStatus />}
wordmark="northstar"
/>
)
}Links, and who renders them
Pass columns and the footer lays out the labelled groups and styles the links itself, so a directory of thirty links is a data structure rather than thirty lines of markup. Pass navigation instead when the shape is unusual enough that you would rather build it.
Links are plain anchors unless you say otherwise. renderLink hands each one back to you, which is how a framework's own link component gets used without the footer knowing anything about it.
renderLink={({ href, label, external }) =>
external ? (
<a href={href} target="_blank" rel="noreferrer noopener">{label}</a>
) : (
<Link href={href}>{label}</Link>
)
}related is the same shape as a column but laid out as a wrapping row above the closing line, set off by a dashed rule. It is for the links that are not part of this product -- a sister site, the rest of a portfolio -- and its label is yours to name.
Dark or light
The default is the page's foreground colour as a ground, which reads as a dark slab under a light site. Every shade inside is mixed from the footer's own text colour rather than a theme token, so setting a different background and text colour on the element is all it takes to move it to a light ground.
<SignatureFooter
className="bg-card text-card-foreground border-border border-t"
...
/>The wordmark
The oversized word across the bottom is drawn at a fraction of the footer's own colour and clipped by the edge of the page. It is hidden from assistive technology and unselectable, because it is a texture rather than a heading -- a screen reader announcing an enormous brand name at the end of every page is noise.
Keep it to one short word. It scales with the viewport and is set to never wrap, so anything long is simply cut off rather than reflowed, and the name you actually want read belongs in brand or meta.
Filling it in
Everything except the heading and the wordmark is optional, and each slot takes whatever you give it. There is no link list baked in, no newsletter form, and no social row -- pass your own navigation and it is laid out with the rest.
- eyebrow and heading carry the line you want people to leave with.
- action is the single thing you want them to do next, not three things.
- navigation takes your own list markup, so the grouping is yours.
- brand and meta hold the small print along the bottom edge.
This is a server component: it holds no state and no effects, so it can render on the server and ship no JavaScript. Import it from its own entry to keep it that way.
API
wordmarkstringThe oversized closing brand name. The only required prop.columnsFooterColumn[]Labelled link columns. Wins over navigation.relatedFooterColumnA wrapping row of links set apart above the closing row.renderLink(link: FooterLink) => ReactNodeRenders every link, for your framework's link component.headingReactNodeA line to lead with, or a logo. Optional.eyebrowReactNodeA short label above the heading.descriptionReactNodeSupporting copy, held to about 36 characters a line.socialReactNodeA row of icon links under the description.actionReactNodeA primary link or button.navigationReactNodeYour own markup, when the columns do not fit.brand, metaReactNode, ReactNodeOpen the closing row: ownership and small print.legal, statusReactNode, ReactNodeClose it: terms in the middle, a status on the right.classNamestringClasses for the footer element. Set the ground here.Accessibility
A semantic footer element, and a real heading when you give it one rather than an empty one when you do not. A link marked external opens in a new tab, carries rel=noreferrer noopener, and says so in its accessible name -- led by a comma, because a leading space is dropped when that name is computed. Column labels are plain text rather than headings, so a long directory does not litter the page outline. The oversized wordmark is decoration and hidden from assistive technology.