Regel tekst waarvan de letters bij hover één voor één van de ene font-variation-instelling naar de andere veren (gewicht, schuinte — welke assen het lettertype heeft), met stagger vanaf de eerste, laatste of middelste letter. Twee regels met omgekeerde instellingen geven de kruisende hero-kop van de bron. Vereist een variabel lettertype in het project.
Bouw sneller.Lever met vertrouwen.
Kom op een regel: de letters veren één voor één van dun naar vet (en omgekeerd op de tweede regel). Werkt met een variabel lettertype.
primitives/tekst/variable-font-hover.tsx
/**
* VariableFontHover — een regel tekst waarvan de letters bij hover één voor
* één van de ene font-variation-instelling naar de andere veren (gewicht,
* schuinte, optische maat — welke assen het lettertype van het project ook
* heeft), met een stagger vanaf de eerste of de laatste letter. Twee regels
* met omgekeerde instellingen geven de 'kruisende' hero-kop van de bron.
*
* Geoogst uit 21st.dev Variable Font Hover Hero (cnippet-dev, naar fancy-
* components): de niet-meegeleverde VariableFontHover → eigen implementatie
* op framer (fontVariationSettings is een string met getallen, framer
* interpoleert die), spring duration 0.5 → smooth-token, text-5xl →
* --text-display, text-muted-foreground → `muted`-prop. Vereist een
* variabel lettertype in het project; anders staat de tekst gewoon.
*/
"use client";
import { useState } from "react";
import type * as React from "react";
import { motion, useReducedMotion } from "framer-motion";
import { useSpringTransition } from "../motion";
export interface VariableFontHoverProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
label: string;
/** Rust-instelling, bv. "'wght' 100, 'slnt' 0". */
from?: string;
/** Hover-instelling, bv. "'wght' 900, 'slnt' -10". */
to?: string;
/** Vertraging per letter in seconden. Default 0.02. */
stagger?: number;
staggerFrom?: "first" | "last" | "center";
/** Gedempte kleur (--muted-foreground) i.p.v. --foreground. */
muted?: boolean;
/** Typerol. Default display. */
size?: "display" | "h2" | "h3";
}
export function VariableFontHover({ label, from = "'wght' 100, 'slnt' 0", to = "'wght' 900, 'slnt' -10", stagger = 0.02, staggerFrom = "first", muted = false, size = "display", className, ...rest }: VariableFontHoverProps) {
const [hover, setHover] = useState(false);
const reduced = useReducedMotion() ?? false;
const veer = useSpringTransition("smooth");
const letters = label.split("");
const n = letters.length;
const vertraging = (i: number) => {
if (staggerFrom === "last") return (n - 1 - i) * stagger;
if (staggerFrom === "center") return Math.abs(i - (n - 1) / 2) * stagger;
return i * stagger;
};
return (
<span
{...rest}
className={["fp-vfh", `fp-vfh--${size}`, muted ? "fp-vfh--muted" : "", className].filter(Boolean).join(" ")}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
onFocus={() => setHover(true)}
onBlur={() => setHover(false)}
tabIndex={0}
>
<span className="fp-vfh__sr">{label}</span>
<span aria-hidden="true">
{letters.map((l, i) => (
<motion.span
key={i}
className="fp-vfh__letter"
initial={false}
animate={{ fontVariationSettings: hover ? to : from }}
transition={reduced ? { duration: 0 } : { ...veer, delay: vertraging(i) }}
>
{l === " " ? " " : l}
</motion.span>
))}
</span>
</span>
);
}
primitives/tekst/tekst.css
/**
* Tekst II — importeren in de app-root (registry-site: app/globals.css).
* Token-contract 2.
*/
/* ── variable-font-hover ────────────────────────────────────────────────── */
.fp-vfh { display: inline-block; cursor: default; color: var(--foreground); letter-spacing: -0.02em; line-height: 1.05; }
.fp-vfh:focus-visible { outline: 2px solid var(--ring); outline-offset: 4px; border-radius: var(--radius-sm); }
.fp-vfh--display { font-size: var(--text-display); }
.fp-vfh--h2 { font-size: var(--text-h2); }
.fp-vfh--h3 { font-size: var(--text-h3); }
.fp-vfh--muted { color: var(--muted-foreground); }
.fp-vfh__letter { display: inline-block; }
.fp-vfh__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }