← alle entries

grid-footer

FC-0140Footersfooter-gridblockv1.0.0

Rustige, brede voettekst: links het ronde logo met een korte tekst; rechts vier kolommen in twee paren met dunne koppen en ruim gespatieerde links; onderaan één gecentreerde copyright-regel. Elk deel komt na het vorige omhoog zodra de footer in beeld komt.

zelfde component — andere tokens

Vier kolommen in twee paren; elk deel komt na het vorige omhoog zodra de footer in beeld komt.

blocks/footers/grid-footer.tsx
/**
 * GridFooter — een rustige, brede voettekst: links het ronde logo met een
 * korte tekst; rechts vier kolommen in twee paren met dunne koppen en ruim
 * gespatieerde links; onderaan één gecentreerde copyright-regel. Elk deel
 * komt na het vorige omhoog zodra de footer in beeld komt (scroll: trigger).
 *
 * Geoogst uit 21st.dev Ruixen Footer 03 (ruixen.ui): next/image met twee
 * logo's (licht/donker) → één `logo`-slot (het project kiest per skin),
 * next/link → linkComponent, vier vaste kolommen → `columns` (2 of 4 — twee
 * paren), text-gray-500 → --muted-foreground, de stagger-delays van 0.1–0.5 s
 * blijven maar lopen op de smooth-token.
 */

"use client";

import type * as React from "react";
import { motion, useReducedMotion } from "framer-motion";
import { useSpringTransition } from "../../primitives/motion";
import type { FooterColumn } from "./footer-types";

export interface GridFooterProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
  logo: React.ReactNode;
  description?: React.ReactNode;
  /** Vier kolommen: ze staan in twee paren. Twee of drie mag ook. */
  columns: readonly FooterColumn[];
  copyright?: React.ReactNode;
  linkComponent?: React.ElementType;
}

function Onthul({ delay, className, children }: { delay: number; className?: string; children: React.ReactNode }) {
  const reduced = useReducedMotion() ?? false;
  const veer = useSpringTransition("smooth", { mass: "content", delay });
  return (
    <motion.div className={className} initial={reduced ? false : { opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, amount: 0.2 }} transition={veer}>
      {children}
    </motion.div>
  );
}

export function GridFooter({ logo, description, columns, copyright, linkComponent: Link = "a", className, ...rest }: GridFooterProps) {
  const paren: FooterColumn[][] = [];
  for (let i = 0; i < columns.length; i += 2) paren.push(columns.slice(i, i + 2));
  return (
    <footer {...rest} className={["fp-gfoot", className].filter(Boolean).join(" ")}>
      <div className="fp-gfoot__raster">
        <Onthul delay={0} className="fp-gfoot__merk">
          <span className="fp-gfoot__logo">{logo}</span>
          {description && <p className="fp-ft-tekst">{description}</p>}
        </Onthul>
        <div className="fp-gfoot__paren">
          {paren.map((paar, pi) => (
            <div key={pi} className="fp-gfoot__paar">
              {paar.map((k, ki) => (
                <Onthul key={k.title} delay={0.1 + (pi * 2 + ki) * 0.1}>
                  <h3 className="fp-gfoot__kolomkop">{k.title}</h3>
                  <ul className="fp-ft-lijst">
                    {k.links.map((l) => (
                      <li key={l.label}><Link href={l.href} className="fp-ft-link">{l.icon}{l.label}</Link></li>
                    ))}
                  </ul>
                </Onthul>
              ))}
            </div>
          ))}
        </div>
      </div>
      {copyright && (
        <Onthul delay={0.5} className="fp-gfoot__copy">
          <p style={{ margin: 0 }}>{copyright}</p>
        </Onthul>
      )}
    </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); }