← alle entries

background-paths

FC-0031hero-backgroundblockv1.0.0

Hero met 72 trage, dunne lijnen die als stromen over het vlak trekken; de titel komt letter voor letter omhoog, de actie eronder is een slot. Lijnen kleuren met currentColor, dus het blok herskint mee.

zelfde component — andere tokens

Stromenvanlicht

blocks/background-paths/background-paths.tsx
/**
 * BackgroundPaths — een hero met 72 dunne, trage lijnen die als stromen
 * over het vlak trekken; de titel komt letter voor letter omhoog, de actie
 * eronder is een slot. De lijnen kleuren met de tekstkleur (currentColor),
 * dus het blok herskint mee zonder één letterlijke kleur.
 *
 * Bewust anders dan de bron: de duur per lijn was Math.random() (hydratie-
 * verschil server/client); nu deterministisch uit de index. De knop uit de
 * bron (shadcn Button met radix-slot) is vervallen — het blok kiest geen knop,
 * het project levert er een via `action` (Button, PressDepth, of een link).
 * Reduced motion: lijnen staan stil op 40% zichtbaar, titel staat er direct.
 */

"use client";

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

interface PadenProps {
  richting: 1 | -1;
  reduced: boolean;
}

function Paden({ richting, reduced }: PadenProps) {
  const paden = Array.from({ length: 36 }, (_, i) => ({
    id: i,
    d: `M-${380 - i * 5 * richting} -${189 + i * 6}C-${380 - i * 5 * richting} -${189 + i * 6} -${312 - i * 5 * richting} ${216 - i * 6} ${152 - i * 5 * richting} ${343 - i * 6}C${616 - i * 5 * richting} ${470 - i * 6} ${684 - i * 5 * richting} ${875 - i * 6} ${684 - i * 5 * richting} ${875 - i * 6}`,
    breedte: 0.5 + i * 0.03,
    opacity: 0.1 + i * 0.03,
    duur: 20 + ((i * 7) % 10),
  }));
  return (
    <svg className="fp-paths__svg" viewBox="0 0 696 316" fill="none" aria-hidden="true" focusable="false">
      {paden.map((p) => (
        <motion.path
          key={p.id}
          d={p.d}
          stroke="currentColor"
          strokeWidth={p.breedte}
          strokeOpacity={p.opacity}
          initial={reduced ? { pathLength: 1, opacity: 0.4 } : { pathLength: 0.3, opacity: 0.6 }}
          animate={reduced ? { pathLength: 1, opacity: 0.4 } : { pathLength: 1, opacity: [0.3, 0.6, 0.3], pathOffset: [0, 1, 0] }}
          transition={reduced ? { duration: 0 } : { duration: p.duur, repeat: Infinity, ease: "linear" }}
        />
      ))}
    </svg>
  );
}

export interface BackgroundPathsProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
  title: string;
  /** De actie onder de titel — het project levert de knop. */
  action?: React.ReactNode;
  /** Hoogte van het vlak, structureel (default min(100svh, 40rem)). */
  height?: string;
}

export function BackgroundPaths({ title, action, height = "min(100svh, 40rem)", className, style, ...rest }: BackgroundPathsProps) {
  const reduced = useReducedMotion() ?? false;
  const veer = useSpringTransition("smooth", { mass: "content" });
  const woorden = title.split(" ");

  return (
    <section {...rest} className={["fp-paths", className].filter(Boolean).join(" ")} style={{ ...style, minHeight: height }}>
      <div className="fp-paths__achter" aria-hidden="true">
        <Paden richting={1} reduced={reduced} />
        <Paden richting={-1} reduced={reduced} />
      </div>
      <motion.div
        className="fp-paths__voor"
        initial={reduced ? false : { opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ ...veer, delay: 0.1 }}
      >
        <h1 className="fp-paths__titel">
          {woorden.map((woord, w) => (
            <span key={w} className="fp-paths__woord">
              {Array.from(woord).map((letter, l) => (
                <motion.span
                  key={`${w}-${l}`}
                  className="fp-paths__letter"
                  initial={reduced ? false : { y: 100, opacity: 0 }}
                  animate={{ y: 0, opacity: 1 }}
                  transition={{ ...veer, delay: w * 0.1 + l * 0.03 }}
                >
                  {letter}
                </motion.span>
              ))}
            </span>
          ))}
        </h1>
        {action && <div className="fp-paths__actie">{action}</div>}
      </motion.div>
    </section>
  );
}
blocks/background-paths/background-paths.css
/**
 * BackgroundPaths — hero met stromende lijnen. Importeren in de app-root
 * (de registry-site doet dat in app/globals.css).
 */

.fp-paths {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  width: 100%;
  background: var(--background); color: var(--foreground);
}
.fp-paths__achter { position: absolute; inset: 0; pointer-events: none; }
.fp-paths__svg { position: absolute; inset: 0; width: 100%; height: 100%; }

.fp-paths__voor {
  position: relative; z-index: 1;
  width: 100%; max-width: 56rem; margin: 0 auto;
  padding: 0 1.5rem; text-align: center;
}
.fp-paths__titel {
  margin: 0 0 2rem;
  font: inherit; font-weight: 700; letter-spacing: -0.04em; line-height: 1;
  font-size: clamp(2.75rem, 8vw, 6rem);
}
.fp-paths__woord { display: inline-block; margin-right: 0.25em; }
.fp-paths__woord:last-child { margin-right: 0; }
/* De letters komen van onderen; de kop knipt ze niet af (overflow blijft zichtbaar). */
.fp-paths__letter { display: inline-block; }
.fp-paths__actie { display: inline-flex; }