All componentslayout
Footer Variants
Three layouts sharing the same link-group data shape.
Minimal
Links + newsletter
Mega
Installation
$ npx shadcn@latest add https://deepmoon.dev/r/footer-variants.jsonProps
FooterMinimalProps
| Prop | Type | Required |
|---|---|---|
| logo | React.ReactNode | No |
| copyright | string | Yes |
| links | { label: string; href: string }[] | No |
FooterLinksProps
| Prop | Type | Required |
|---|---|---|
| logo | React.ReactNode | No |
| groups | FooterLinkGroup[] | Yes |
| copyright | string | Yes |
| onSubscribe | (email: string) => void | No |
FooterMegaProps
| Prop | Type | Required |
|---|---|---|
| logo | React.ReactNode | No |
| description | string | No |
| groups | FooterLinkGroup[] | Yes |
| bottomLinks | { label: string; href: string }[] | No |
| copyright | string | Yes |
Code
footer-variants.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface FooterLinkGroup {
title: string;
links: { label: string; href: string }[];
}
export interface FooterMinimalProps extends React.ComponentPropsWithoutRef<"footer"> {
logo?: React.ReactNode;
copyright: string;
links?: { label: string; href: string }[];
}
export function FooterMinimal({ logo, copyright, links = [], className, ...props }: FooterMinimalProps) {
return (
<footer
className={cn(
"flex flex-col items-center justify-between gap-4 border-t border-border py-6 sm:flex-row",
className,
)}
{...props}
>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
{logo}
<span>{copyright}</span>
</div>
{links.length > 0 ? (
<nav aria-label="Footer" className="flex items-center gap-4 text-sm">
{links.map((link) => (
<a key={link.href} href={link.href} className="text-muted-foreground hover:text-foreground">
{link.label}
</a>
))}
</nav>
) : null}
</footer>
);
}
export interface FooterLinksProps extends React.ComponentPropsWithoutRef<"footer"> {
logo?: React.ReactNode;
groups: FooterLinkGroup[];
copyright: string;
onSubscribe?: (email: string) => void;
}
export function FooterLinks({ logo, groups, copyright, onSubscribe, className, ...props }: FooterLinksProps) {
const [email, setEmail] = React.useState("");
return (
<footer className={cn("border-t border-border py-12", className)} {...props}>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex flex-col gap-3 lg:col-span-1">
{logo}
<p className="text-sm text-muted-foreground">Stay up to date with product news.</p>
{onSubscribe ? (
<form
className="flex gap-2"
onSubmit={(event) => {
event.preventDefault();
if (email.trim()) {
onSubscribe(email.trim());
setEmail("");
}
}}
>
<label htmlFor="footer-newsletter-email" className="sr-only">
Email address
</label>
<input
id="footer-newsletter-email"
type="email"
required
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="you@example.com"
className="w-full min-w-0 rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
<button
type="submit"
className="shrink-0 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Subscribe
</button>
</form>
) : null}
</div>
{groups.map((group) => (
<div key={group.title} className="flex flex-col gap-3">
<p className="text-sm font-semibold text-foreground">{group.title}</p>
<ul role="list" className="flex list-none flex-col gap-2">
{group.links.map((link) => (
<li key={link.href}>
<a href={link.href} className="text-sm text-muted-foreground hover:text-foreground">
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<p className="mt-10 text-sm text-muted-foreground">{copyright}</p>
</footer>
);
}
export interface FooterMegaProps extends React.ComponentPropsWithoutRef<"footer"> {
logo?: React.ReactNode;
description?: string;
groups: FooterLinkGroup[];
bottomLinks?: { label: string; href: string }[];
copyright: string;
}
export function FooterMega({
logo,
description,
groups,
bottomLinks = [],
copyright,
className,
...props
}: FooterMegaProps) {
return (
<footer className={cn("border-t border-border bg-secondary/30 py-16", className)} {...props}>
<div className="grid grid-cols-1 gap-10 sm:grid-cols-3 lg:grid-cols-5">
<div className="flex flex-col gap-3 sm:col-span-3 lg:col-span-2">
{logo}
{description ? <p className="max-w-sm text-sm text-muted-foreground">{description}</p> : null}
</div>
{groups.map((group) => (
<div key={group.title} className="flex flex-col gap-3">
<p className="text-sm font-semibold text-foreground">{group.title}</p>
<ul role="list" className="flex list-none flex-col gap-2">
{group.links.map((link) => (
<li key={link.href}>
<a href={link.href} className="text-sm text-muted-foreground hover:text-foreground">
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-border pt-6 sm:flex-row">
<p className="text-sm text-muted-foreground">{copyright}</p>
{bottomLinks.length > 0 ? (
<nav aria-label="Legal" className="flex items-center gap-4 text-sm">
{bottomLinks.map((link) => (
<a key={link.href} href={link.href} className="text-muted-foreground hover:text-foreground">
{link.label}
</a>
))}
</nav>
) : null}
</div>
</footer>
);
}