← alle entries

variable-font-hover

FC-0152Tekst & typografievariable-font-textprimitivev1.0.0

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.

zelfde component — andere tokens
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; }