Kleine zakelijke voettekst: merk (logo + naam) links en ronde sociale knoppen rechts; onder een lijn de hoofdlinks en juridische links rechts uitgelijnd, copyright met een tweede regel links op dezelfde hoogte. Statisch.
Merk links, sociale knoppen rechts; onder de lijn de links rechts uitgelijnd en de copyright links.
blocks/footers/compact-footer.tsx
/**
* CompactFooter — de kleine, zakelijke voettekst: merk (logo + naam) links en
* ronde sociale knoppen rechts; daaronder een lijn, dan de hoofdlinks en de
* juridische links rechts uitgelijnd, met copyright (en een tweede regel)
* links op dezelfde hoogte. Statisch; geen beweging behalve de hover.
*
* Geoogst uit 21st.dev Footer (nevsky118): shadcn Button (secondary/icon,
* asChild) → eigen ronde knop uit de gedeelde footer-stylesheet, lucide
* Hexagon/Twitter/Github uit de demo → slots, `/`-link achter het merk →
* `homeHref`, Tailwind-raster (lg:col-[4/11]) → één grid met twee rijen.
*/
import type * as React from "react";
import type { FooterLink, FooterSocial } from "./footer-types";
export interface CompactFooterProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
logo?: React.ReactNode;
brandName: string;
/** Waar het merk heen linkt. Default "/". */
homeHref?: string;
socials?: readonly FooterSocial[];
links: readonly FooterLink[];
legal?: readonly FooterLink[];
copyright: React.ReactNode;
/** Tweede regel onder de copyright (bv. 'Alle rechten voorbehouden'). */
license?: React.ReactNode;
linkComponent?: React.ElementType;
}
export function CompactFooter({ logo, brandName, homeHref = "/", socials = [], links, legal = [], copyright, license, linkComponent: Link = "a", className, ...rest }: CompactFooterProps) {
return (
<footer {...rest} className={["fp-kfoot", className].filter(Boolean).join(" ")}>
<div className="fp-kfoot__boven">
<Link href={homeHref} className="fp-ft-merk" aria-label={brandName}>
{logo}
<span>{brandName}</span>
</Link>
{socials.length > 0 && (
<ul className="fp-ft-social">
{socials.map((s) => (
<li key={s.label}>
<a href={s.href} target="_blank" rel="noopener noreferrer" aria-label={s.label} className="fp-ft-social__knop">{s.icon}</a>
</li>
))}
</ul>
)}
</div>
<div className="fp-kfoot__onder">
<nav aria-label="Voettekst">
<ul className="fp-kfoot__links">
{links.map((l) => (
<li key={l.label}><Link href={l.href} className="fp-kfoot__hoofdlink">{l.label}</Link></li>
))}
</ul>
</nav>
{legal.length > 0 && (
<ul className="fp-kfoot__links">
{legal.map((l) => (
<li key={l.label}><Link href={l.href} className="fp-kfoot__legallink">{l.label}</Link></li>
))}
</ul>
)}
<div className="fp-kfoot__copy">
<p>{copyright}</p>
{license && <p>{license}</p>}
</div>
</div>
</footer>
);
}
blocks/footers/footer-types.ts
/**
* Gedeelde datavormen van de footer-entries (sticky, centered, compact,
* columns, grid, newsletter). Eén vocabulaire zodat een project van de ene
* footer naar de andere kan wisselen zonder zijn data te hertalen.
*/
import type * as React from "react";
export interface FooterLink {
label: string;
href: string;
/** Optioneel icoon vóór het label (van het project). */
icon?: React.ReactNode;
}
export interface FooterColumn {
title: string;
links: readonly FooterLink[];
}
export interface FooterSocial {
label: string;
href: string;
/** Het icoon komt van het project (21st Icons, eigen SVG). */
icon: React.ReactNode;
}
blocks/footers/footers.css
/**
* Footers — importeren in de app-root (registry-site: app/globals.css).
* Token-contract 2: maten uit --text-*, --space-*, --radius-*, --border-width.
* Gedeeld: .fp-ft-* (link, kop, sociale knop, onderbalk); per entry een eigen blok.
*/
/* ── Gedeelde onderdelen ─────────────────────────────────────────────────── */
.fp-ft-link {
display: inline-flex; align-items: center; gap: var(--space-2xs);
font-size: var(--text-body-sm); color: var(--muted-foreground); text-decoration: none;
transition: color var(--motion-instant-duration) var(--motion-instant);
}
.fp-ft-link:hover { color: var(--foreground); }
.fp-ft-link:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: var(--radius-sm); }
.fp-ft-kop { margin: 0 0 var(--space-md); font: inherit; font-size: var(--text-label); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--foreground); }
.fp-ft-lijst { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-xs); }
.fp-ft-social { list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: var(--space-xs); }
.fp-ft-social__knop {
display: inline-flex; align-items: center; justify-content: center;
width: 2.25rem; height: 2.25rem; border-radius: 50%;
border: var(--border-width) solid var(--border); background: var(--card); color: var(--foreground);
transition: background var(--motion-instant-duration) var(--motion-instant), border-color var(--motion-instant-duration) var(--motion-instant);
}
.fp-ft-social__knop:hover { background: var(--muted); border-color: var(--muted-foreground); }
.fp-ft-social__knop:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.fp-ft-social__knop > * { width: 1rem; height: 1rem; }
.fp-ft-onder {
display: flex; flex-direction: column; gap: var(--space-xs);
padding-top: var(--space-lg); border-top: var(--border-width) solid var(--border);
font-size: var(--text-body-sm); color: var(--muted-foreground);
}
@media (min-width: 48rem) { .fp-ft-onder { flex-direction: row; align-items: center; justify-content: space-between; } }
.fp-ft-onder p { margin: 0; }
.fp-ft-legal { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-md); }
.fp-ft-merk { display: flex; align-items: center; gap: var(--space-xs); color: var(--foreground); text-decoration: none; font-size: var(--text-h3); font-weight: 700; letter-spacing: -0.01em; }
.fp-ft-merk > svg, .fp-ft-merk > img { width: 2rem; height: 2rem; }
.fp-ft-tekst { margin: 0; max-width: 32ch; font-size: var(--text-body-sm); line-height: 1.6; color: var(--muted-foreground); }
/* ── sticky-footer: vast onder de pagina, de inhoud schuift eroverheen ─── */
.fp-sfoot { position: relative; width: 100%; height: var(--fp-sfoot-h, 40rem); clip-path: inset(0); color: var(--foreground); }
.fp-sfoot__vast { position: fixed; bottom: 0; left: 0; width: 100%; height: var(--fp-sfoot-h, 40rem); }
.fp-sfoot__vel {
position: relative; display: flex; flex-direction: column; justify-content: space-between; gap: var(--space-lg);
box-sizing: border-box; width: 100%; height: 100%; overflow: auto;
padding: var(--space-xl) var(--space-md); background: var(--background);
border-top: var(--border-width) solid var(--border);
}
@media (min-width: 48rem) { .fp-sfoot__vel { padding: var(--space-xl) var(--space-2xl); } }
/* Drie zachte lichtbanen in de tekstkleur, schuin over het vlak — de 'gloed' van de bron. */
.fp-sfoot__gloed { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.fp-sfoot__gloed i { position: absolute; top: 0; left: 0; display: block; border-radius: 50%; }
.fp-sfoot__gloed i:nth-child(1) { width: 35rem; height: 80rem; transform: translateY(-55%) rotate(-45deg); background: radial-gradient(68% 68% at 55% 31%, color-mix(in srgb, var(--foreground) 6%, transparent) 0, color-mix(in srgb, var(--foreground) 2%, transparent) 50%, transparent 80%); }
.fp-sfoot__gloed i:nth-child(2) { width: 15rem; height: 80rem; transform: translate(5%, -50%) rotate(-45deg); background: radial-gradient(50% 50% at 50% 50%, color-mix(in srgb, var(--foreground) 4%, transparent) 0, transparent 80%); }
.fp-sfoot__gloed i:nth-child(3) { width: 15rem; height: 80rem; transform: translateY(-55%) rotate(-45deg); background: radial-gradient(50% 50% at 50% 50%, color-mix(in srgb, var(--foreground) 4%, transparent) 0, transparent 80%); }
.fp-sfoot__rijen { position: relative; z-index: 1; display: flex; flex-direction: column; gap: var(--space-xl); margin-top: var(--space-lg); }
@media (min-width: 48rem) { .fp-sfoot__rijen { flex-direction: row; gap: var(--space-xl); } }
.fp-sfoot__merk { display: grid; gap: var(--space-md); width: 100%; max-width: 24rem; }
.fp-sfoot__kolom { width: 100%; }
.fp-sfoot__onder { position: relative; z-index: 1; }
/* ── centered-footer: logo, links op een rij, sociale iconen, arceerband ── */
.fp-cfoot { width: 100%; padding: var(--space-xl) 0 var(--space-lg); overflow: hidden; color: var(--foreground); }
.fp-cfoot__midden { display: flex; flex-direction: column; align-items: center; gap: var(--space-xl); margin-bottom: var(--space-xl); padding: 0 var(--space-md); }
.fp-cfoot__logo { display: flex; justify-content: center; }
.fp-cfoot__logo > * { height: 2.5rem; width: auto; }
.fp-cfoot__nav { position: relative; z-index: 1; display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-md) var(--space-lg); }
.fp-cfoot__nav-link { position: relative; display: inline-block; padding: var(--space-2xs) var(--space-xs); font-size: var(--text-body); font-weight: 500; color: var(--foreground); text-decoration: none; }
.fp-cfoot__nav-link:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: var(--radius-sm); }
.fp-cfoot__nav-tekst { position: relative; z-index: 1; }
.fp-cfoot__nav-vlak { position: absolute; inset: 0; z-index: 0; border-radius: var(--radius-sm); background: var(--muted); transform-origin: center; }
/* De arceerband: dunne schuine lijnen in de tekstkleur, op lage dekking. */
.fp-cfoot__band {
width: 100%; height: 3rem; opacity: 0.1;
border-top: var(--border-width) solid var(--foreground); border-bottom: var(--border-width) solid var(--foreground);
background-image: repeating-linear-gradient(315deg, currentColor 0, currentColor 1px, transparent 0, transparent 50%);
background-size: 10px 10px;
}
.fp-cfoot__copy { margin: var(--space-lg) 0 0; padding: 0 var(--space-md); text-align: center; font-size: var(--text-body-sm); color: var(--muted-foreground); }
/* ── compact-footer: merk + sociale knoppen, lijn, links rechts, copyright links ── */
.fp-kfoot { width: 100%; padding: var(--space-2xl) var(--space-md) var(--space-lg); color: var(--foreground); }
@media (min-width: 64rem) { .fp-kfoot { padding: var(--space-2xl) var(--space-xl) var(--space-lg); } }
.fp-kfoot__boven { display: flex; flex-direction: column; gap: var(--space-lg); }
@media (min-width: 48rem) { .fp-kfoot__boven { flex-direction: row; align-items: flex-start; justify-content: space-between; } }
.fp-kfoot__onder { display: grid; gap: var(--space-md); margin-top: var(--space-lg); padding-top: var(--space-lg); border-top: var(--border-width) solid var(--border); }
@media (min-width: 64rem) { .fp-kfoot__onder { grid-template-columns: 3fr 7fr; grid-template-rows: auto auto; padding-top: var(--space-xl); } }
.fp-kfoot__links { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-md); }
@media (min-width: 64rem) { .fp-kfoot__links { grid-column: 2; justify-content: flex-end; } }
.fp-kfoot__hoofdlink { font-size: var(--text-body-sm); color: var(--foreground); text-decoration: none; text-underline-offset: 0.25em; }
.fp-kfoot__hoofdlink:hover { text-decoration: underline; }
.fp-kfoot__legallink { font-size: var(--text-body-sm); color: var(--muted-foreground); text-decoration: none; text-underline-offset: 0.25em; }
.fp-kfoot__legallink:hover { text-decoration: underline; }
.fp-kfoot__copy { font-size: var(--text-body-sm); line-height: 1.6; color: var(--muted-foreground); white-space: nowrap; }
@media (min-width: 64rem) { .fp-kfoot__copy { grid-column: 1; grid-row: 1 / 3; } }
.fp-kfoot__copy p { margin: 0; }
/* ── columns-footer: merk + tekst + socials links, drie kolommen rechts, onderbalk ── */
.fp-zfoot { width: 100%; max-width: 72rem; margin: 0 auto; padding: var(--space-2xl) var(--space-md); box-sizing: border-box; color: var(--foreground); }
.fp-zfoot__boven { display: flex; flex-direction: column; gap: var(--space-xl); }
@media (min-width: 64rem) { .fp-zfoot__boven { flex-direction: row; align-items: flex-start; } }
.fp-zfoot__merk { display: grid; gap: var(--space-lg); width: 100%; }
.fp-zfoot__kolommen { display: grid; gap: var(--space-lg); width: 100%; }
@media (min-width: 48rem) { .fp-zfoot__kolommen { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .fp-zfoot__kolommen { gap: var(--space-2xl); } }
.fp-zfoot__kolomkop { margin: 0 0 var(--space-md); font: inherit; font-size: var(--text-body); font-weight: 700; }
.fp-zfoot__onder { margin-top: var(--space-xl); }
.fp-zfoot__onder .fp-ft-onder { padding: var(--space-lg) 0; font-size: var(--text-label); font-weight: 500; }
.fp-zfoot__onder .fp-ft-link:hover { color: var(--primary); }
.fp-zfoot .fp-ft-social__knop { border: 0; background: transparent; width: auto; height: auto; }
.fp-zfoot .fp-ft-social__knop > * { width: 1.25rem; height: 1.25rem; }
.fp-zfoot .fp-ft-social__knop:hover { color: var(--primary); background: transparent; }
/* ── grid-footer: merk in de eerste kolom, vier kolommen in twee paren, centrale copyright ── */
.fp-gfoot {
position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 100%; max-width: 72rem; margin: 0 auto; box-sizing: border-box;
padding: var(--space-2xl) var(--space-md) var(--space-lg);
border-top: var(--border-width) solid var(--border); color: var(--foreground);
}
@media (min-width: 64rem) { .fp-gfoot { padding: var(--space-2xl) var(--space-xl) var(--space-lg); } }
.fp-gfoot__raster { display: grid; gap: var(--space-xl); width: 100%; }
@media (min-width: 80rem) { .fp-gfoot__raster { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.fp-gfoot__merk { display: grid; gap: var(--space-md); align-content: start; max-width: 14rem; }
.fp-gfoot__logo { display: inline-flex; width: 2.5rem; height: 2.5rem; border-radius: 50%; overflow: hidden; }
.fp-gfoot__logo > * { width: 100%; height: 100%; }
.fp-gfoot__paren { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-xl); }
@media (min-width: 80rem) { .fp-gfoot__paren { grid-column: span 2; } }
.fp-gfoot__paar { display: grid; gap: var(--space-xl); }
@media (min-width: 48rem) { .fp-gfoot__paar { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.fp-gfoot__kolomkop { margin: 0 0 var(--space-md); font: inherit; font-size: var(--text-body); font-weight: 400; color: var(--foreground); }
.fp-gfoot .fp-ft-lijst { gap: var(--space-md); }
.fp-gfoot__copy { width: 100%; margin: var(--space-2xl) 0 0; text-align: center; font-size: var(--text-body-sm); color: var(--muted-foreground); }
/* ── newsletter-footer: inschrijven, snelle links, contact, volgen; onderbalk ── */
.fp-nfoot { position: relative; width: 100%; border-top: var(--border-width) solid var(--border); background: var(--background); color: var(--foreground); }
.fp-nfoot__binnen { max-width: 72rem; margin: 0 auto; padding: var(--space-xl) var(--space-md); box-sizing: border-box; }
@media (min-width: 64rem) { .fp-nfoot__binnen { padding: var(--space-xl) var(--space-xl); } }
.fp-nfoot__raster { display: grid; gap: var(--space-xl); }
@media (min-width: 48rem) { .fp-nfoot__raster { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .fp-nfoot__raster { grid-template-columns: 1.4fr repeat(3, minmax(0, 1fr)); } }
.fp-nfoot__kolom { position: relative; }
.fp-nfoot__titel { margin: 0 0 var(--space-md); font: inherit; font-size: var(--text-h2); line-height: 1.1; font-weight: 700; letter-spacing: -0.02em; }
.fp-nfoot__intro { margin: 0 0 var(--space-lg); font-size: var(--text-body-sm); color: var(--muted-foreground); }
.fp-nfoot__form { position: relative; }
/* De verzendknop zit ín het veld (trailing-slot): rond, in de primaire kleur, met pijl; klaar = vinkje. */
.fp-nfoot__verzend {
display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; padding: 0;
border: 0; border-radius: 50%; background: var(--primary); color: var(--primary-foreground); cursor: pointer;
transition: transform var(--motion-instant-duration) var(--motion-instant), opacity var(--motion-instant-duration) var(--motion-instant);
}
.fp-nfoot__verzend:hover { transform: scale(1.06); }
.fp-nfoot__verzend:active { transform: scale(0.96); }
.fp-nfoot__verzend:disabled { cursor: default; opacity: 0.7; transform: none; }
.fp-nfoot__verzend:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.fp-nfoot__verzend > svg { width: 1rem; height: 1rem; }
.fp-nfoot__form .fp-input--met-trailing { padding-right: 3rem; }
/* De zachte gloed rechtsboven het inschrijfblok, in de primaire kleur. */
.fp-nfoot__gloed { position: absolute; top: -1rem; right: -1rem; width: 6rem; height: 6rem; border-radius: 50%; background: color-mix(in srgb, var(--primary) 10%, transparent); filter: blur(2rem); pointer-events: none; }
.fp-nfoot__kop { margin: 0 0 var(--space-md); font: inherit; font-size: var(--text-h3); font-weight: 600; }
.fp-nfoot__nav { display: grid; gap: var(--space-xs); }
.fp-nfoot__nav .fp-ft-link { color: var(--foreground); }
.fp-nfoot__nav .fp-ft-link:hover { color: var(--primary); }
.fp-nfoot__adres { margin: 0; display: grid; gap: var(--space-xs); font-style: normal; font-size: var(--text-body-sm); color: var(--foreground); }
.fp-nfoot__adres p { margin: 0; }
.fp-nfoot__extra { margin-top: var(--space-lg); }
.fp-nfoot__onder { margin-top: var(--space-xl); text-align: center; }
.fp-nfoot__onder .fp-ft-link { color: var(--foreground); }
.fp-nfoot__onder .fp-ft-link:hover { color: var(--primary); }