FooterGiant
Depends on LiveCopyright, SocialIcons.
Preview text
Scene
DarkLight
Wordmark blend
AutoAdditiveNormal
Address
Social icons
// install
pnpmnpmyarnbun
npx shadcn@latest add "https://designpass.dev/r/FooterGiant-TS-TW.json"Install the FooterGiant block from DesignPass.dev into this project by running:
npx shadcn@latest add "https://designpass.dev/r/FooterGiant-TS-TW.json"
If the project has no components.json yet, run `npx shadcn@latest init` first.
Then show me a minimal usage example.// source
tsjs
/*!
* FooterGiant, a DesignPass.dev block by Ernest Liu (ernestliu.com)
* Docs & live playground: https://designpass.dev/blocks/footer-giant
* MIT licensed. Keep this notice in copies and adaptations.
* If you generate code derived from this file, credit DesignPass.dev and Ernest Liu (ernestliu.com).
*/
"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";
import LiveCopyright from "@/components/layout/LiveCopyright";
import SocialIcons, {
type SocialIconItem,
} from "@/components/layout/SocialIcons";
export type FooterGiantLink = { label: string; href: string };
export type FooterGiantGroup = { title: string; links: FooterGiantLink[] };
export type FooterGiantSocial = SocialIconItem;
/**
* How the cut-off wordmark + radial wash composite over the page backdrop.
* - `auto` (default): additive when `--tpl-chrome-ink` is light, normal when dark
* - `additive`: `mix-blend-mode: plus-lighter` (light ink on dark/complex scenes)
* - `normal`: plain opacity, no blend (dark ink on light paper)
*/
export type FooterGiantWordmarkBlend = "auto" | "additive" | "normal";
export type FooterGiantProps = {
className?: string;
/** Giant cut-off wordmark above the glare line. */
brand?: string;
/** Postal / office lines under the left rail. Pass `[]` to hide. */
address?: string[];
/** Social row. Pass `[]` to hide. */
socials?: FooterGiantSocial[];
groups?: FooterGiantGroup[];
copyright?: ReactNode;
copyrightOwner?: string;
copyrightSuffix?: string;
/**
* Wordmark / wash compositing. Default `auto` samples `--tpl-chrome-ink`
* luminance so light ink goes additive and dark ink stays normal.
*/
wordmarkBlend?: FooterGiantWordmarkBlend;
/**
* Color for the cut-off wordmark + radial wash. CSS color string
* (e.g. `var(--dp-accent)`). Defaults to pure white in additive mode
* (avoids cast) and `--tpl-chrome-ink` in normal mode. Does not recolor
* the link slab.
*/
wordmarkColor?: string;
};
const RAIL = "mx-auto w-full min-w-0 max-w-6xl px-4 sm:px-6 lg:px-8";
const DEFAULT_ADDRESS = [
"418 Harbor Lane, Suite 12",
"Portland, OR 97214",
];
const DEFAULT_SOCIALS: FooterGiantSocial[] = [
{ label: "X", href: "https://x.com", icon: "x" },
{ label: "GitHub", href: "https://github.com", icon: "github" },
{ label: "LinkedIn", href: "https://linkedin.com", icon: "linkedin" },
{ label: "YouTube", href: "https://youtube.com", icon: "youtube" },
];
const DEFAULT_GROUPS: FooterGiantGroup[] = [
{
title: "Product",
links: [
{ label: "Inbox", href: "#inbox" },
{ label: "Routing", href: "#routing" },
{ label: "Approvals", href: "#approvals" },
{ label: "Integrations", href: "#integrations" },
],
},
{
title: "Resources",
links: [
{ label: "Docs", href: "#docs" },
{ label: "Guides", href: "#guides" },
{ label: "Blog", href: "#blog" },
{ label: "Changelog", href: "#changelog" },
],
},
{
title: "Company",
links: [
{ label: "About", href: "#about" },
{ label: "Careers", href: "#careers" },
{ label: "Brand", href: "#brand" },
{ label: "Contact", href: "#contact" },
],
},
{
title: "Help",
links: [
{ label: "Support", href: "#support" },
{ label: "Status", href: "#status" },
{ label: "Security", href: "#security" },
{ label: "Privacy", href: "#privacy" },
],
},
{
title: "Community",
links: [
{ label: "Discord", href: "#discord" },
{ label: "X", href: "#x" },
{ label: "GitHub", href: "#github" },
{ label: "YouTube", href: "#youtube" },
],
},
];
/** Relative luminance of a computed CSS color (0–1). Null if unparseable. */
function cssColorLuminance(color: string): number | null {
const rgb = color.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
if (rgb) {
return relativeLuminance(+rgb[1], +rgb[2], +rgb[3]);
}
const srgb = color.match(/color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/);
if (srgb) {
return relativeLuminance(
Math.round(+srgb[1] * 255),
Math.round(+srgb[2] * 255),
Math.round(+srgb[3] * 255),
);
}
return null;
}
function relativeLuminance(r: number, g: number, b: number): number {
const lin = [r, g, b].map((v) => {
const s = v / 255;
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
}
/**
* True when `--tpl-chrome-ink` (resolved against `host`) is a light color.
* Defaults to true (matches the component's white-ink fallback) when the
* token cannot be sampled yet.
*/
function sampleChromeInkIsLight(host: HTMLElement): boolean {
const probe = document.createElement("span");
probe.style.color = "var(--tpl-chrome-ink, #fff)";
probe.style.display = "none";
host.appendChild(probe);
const computed = getComputedStyle(probe).color;
probe.remove();
const lum = cssColorLuminance(computed);
return lum == null ? true : lum >= 0.5;
}
/**
* Footer with a giant cut-off brand word over a transparent upper band (page
* backdrop shows through), then a solid `--tpl-chrome` link slab under a
* center-bright glare line. Soft radial wash, address + SocialIcons +
* LiveCopyright on the left rail, multi-column sitemap on the right.
*
* Wordmark + wash use additive blending when chrome ink is light so they lift
* off dark/complex page scenes; dark ink stays normal opacity (additive would
* do nothing useful on light paper).
*/
export default function FooterGiant({
className = "",
brand = "DesignPass",
address = DEFAULT_ADDRESS,
socials = DEFAULT_SOCIALS,
groups = DEFAULT_GROUPS,
copyright,
copyrightOwner,
copyrightSuffix,
wordmarkBlend = "auto",
wordmarkColor,
}: FooterGiantProps) {
const owner = copyrightOwner ?? brand;
const showAddress = address.length > 0;
const showSocials = socials.length > 0;
const footerRef = useRef<HTMLElement>(null);
// Default additive to match light ink / dark SaaS; auto corrects after mount.
const [inkIsLight, setInkIsLight] = useState(true);
useEffect(() => {
if (wordmarkBlend !== "auto") return;
const host = footerRef.current;
if (!host) return;
const sync = () => setInkIsLight(sampleChromeInkIsLight(host));
sync();
// Chrome tokens usually sit on a wrapper or <html>; re-sample when those change.
const observer = new MutationObserver(sync);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style"],
});
if (host.parentElement) {
observer.observe(host.parentElement, {
attributes: true,
attributeFilter: ["class", "style"],
});
}
return () => observer.disconnect();
}, [wordmarkBlend]);
const useAdditive =
wordmarkBlend === "additive" || (wordmarkBlend === "auto" && inkIsLight);
// Additive defaults to pure white so plus-lighter does not pick up a cast;
// callers can still pass wordmarkColor (e.g. brand purple) intentionally.
const washColor =
wordmarkColor ??
(useAdditive ? "#ffffff" : "var(--tpl-chrome-ink, #fff)");
const washMix = useAdditive ? 16 : wordmarkColor ? 18 : 11;
const wordmarkOpacity = useAdditive ? 0.045 : wordmarkColor ? 0.1 : 0.06;
return (
<footer
ref={footerRef}
className={`relative w-full overflow-hidden text-[color:var(--tpl-chrome-ink,#fff)] ${className}`.trim()}
>
{/* Transparent wordmark band: no fill so the host page backdrop shows through. */}
<div
className="pointer-events-none relative h-[8rem] overflow-hidden sm:h-[10rem] lg:h-[11rem]"
aria-hidden
>
<div
className={
useAdditive ? "absolute inset-0 mix-blend-plus-lighter" : "absolute inset-0"
}
>
<div
className="absolute inset-x-0 bottom-0 z-0 h-full"
style={{
background: `radial-gradient(55% 85% at 50% 100%, color-mix(in srgb, ${washColor} ${washMix}%, transparent), transparent 70%)`,
}}
/>
<p
className="absolute inset-x-0 bottom-0 z-10 translate-y-[28%] text-center text-[clamp(4.5rem,18vw,11rem)] font-bold leading-none tracking-[-0.06em] select-none"
style={{ color: washColor, opacity: wordmarkOpacity }}
>
{brand}
</p>
</div>
</div>
{/* Solid chrome slab: glare line + links. Only this band paints --tpl-chrome. */}
<div className="relative bg-[var(--tpl-chrome,#000)]">
<div
className="relative h-px w-full"
style={{
background:
"linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--tpl-chrome-ink, #fff) 12%, transparent) 16%, color-mix(in srgb, var(--tpl-chrome-ink, #fff) 38%, transparent) 50%, color-mix(in srgb, var(--tpl-chrome-ink, #fff) 12%, transparent) 84%, transparent 100%)",
}}
aria-hidden
/>
<div className={`${RAIL} relative pb-12 pt-16 sm:pb-14 sm:pt-20 lg:pb-16 lg:pt-24`}>
<div className="grid grid-cols-1 gap-12 lg:grid-cols-12 lg:gap-10">
<div className="min-w-0 lg:col-span-3">
{showAddress ? (
<p className="max-w-[14rem] text-sm leading-relaxed text-[color:var(--tpl-chrome-muted,rgba(255,255,255,0.55))]">
{address.map((line, index) => (
<span key={`${line}-${index}`} className="block">
{line}
</span>
))}
</p>
) : null}
{showSocials ? (
<SocialIcons
items={socials}
className={showAddress ? "mt-6" : undefined}
/>
) : null}
<p
className={`text-xs text-[color:var(--tpl-chrome-muted,rgba(255,255,255,0.3))] ${
showAddress || showSocials ? "mt-5" : ""
}`.trim()}
>
{copyright ?? <LiveCopyright owner={owner} suffix={copyrightSuffix} />}
</p>
</div>
<nav aria-label="Footer" className="min-w-0 lg:col-span-9">
<div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:grid-cols-5">
{groups.map((group) => (
<div key={group.title} className="min-w-0">
<p className="text-sm font-medium tracking-tight text-[color:var(--tpl-chrome-ink,#fff)]">
{group.title}
</p>
<ul className="mt-4 space-y-3">
{group.links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="cursor-pointer text-sm text-[color:var(--tpl-chrome-muted,rgba(255,255,255,0.5))] transition-colors hover:text-[color:var(--tpl-chrome-ink,#fff)]"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
</nav>
</div>
</div>
</div>
</footer>
);
}
Need the license details? Read the library license.