Variable Type
Hovered character pushes a variable font's weight and width to peak, neighbors ripple outward with quadratic falloff
variable-fonthoverripplefont-variation-settingscss
Preview
Source Code
"use client";
import { useId } from "react";
interface VariableTypeProps {
text?: string;
maxWeight?: number;
baseWeight?: number;
maxWidth?: number;
baseWidth?: number;
duration?: number;
radius?: number;
className?: string;
}
export default function VariableType({
text = "Hover me",
maxWeight = 900,
baseWeight = 400,
maxWidth = 125,
baseWidth = 100,
duration = 400,
radius = 3,
className = "",
}: VariableTypeProps) {
const rawId = useId();
const cls = `vt-${rawId.replace(/[^a-zA-Z0-9]/g, "")}`;
const stops = Array.from({ length: radius }, (_, i) => {
const d = i + 1;
const t = 1 - (d / (radius + 1)) ** 2;
return {
w: Math.round(baseWeight + (maxWeight - baseWeight) * t),
wd: Math.round((baseWidth + (maxWidth - baseWidth) * t) * 100) / 100,
};
});
const siblingRules = stops
.map((s, idx) => {
const d = idx + 1;
const forward = `.${cls} .vtc:hover` + " + .vtc".repeat(d);
const inner = "+ .vtc ".repeat(d - 1) + "+ .vtc:hover";
const backward = `.${cls} .vtc:has(${inner})`;
return `${forward}, ${backward} { font-variation-settings: "wght" ${s.w}, "wdth" ${s.wd}; }`;
})
.join("\n");
const css = `
.${cls} { display: inline-block; }
.${cls} .vtc {
display: inline-block;
font-variation-settings: "wght" ${baseWeight}, "wdth" ${baseWidth};
transition: font-variation-settings ${duration}ms cubic-bezier(0.34, 1.56, 0.64, 1);
will-change: font-variation-settings;
}
.${cls} .vtc:hover { font-variation-settings: "wght" ${maxWeight}, "wdth" ${maxWidth}; }
${siblingRules}
@media (prefers-reduced-motion: reduce) {
.${cls} .vtc { transition: none; }
.${cls} .vtc:hover,
.${cls} .vtc:has(+ .vtc:hover),
.${cls} .vtc:hover + .vtc { font-variation-settings: "wght" ${baseWeight}, "wdth" ${baseWidth}; }
}
`;
const chars = Array.from(text);
return (
<span className={`${cls} ${className}`} aria-label={text}>
<style>{css}</style>
{chars.map((c, i) => (
<span key={i} className="vtc" aria-hidden="true">
{c === " " ? " " : c}
</span>
))}
</span>
);
}