import { type CSSProperties, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
interface FitTextProps {
text: string;
className?: string;
style?: CSSProperties;
minScale?: number;
maxScale?: number;
}
export function FitText({
text,
className,
style,
minScale = 0.1,
maxScale = Number.POSITIVE_INFINITY,
}: FitTextProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const textRef = useRef<HTMLSpanElement | null>(null);
const resizeFrameRef = useRef<number | null>(null);
const [scale, setScale] = useState(1);
const recalculate = useCallback(() => {
const container = containerRef.current;
const textElement = textRef.current;
if (!container || !textElement) {
return;
}
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const textWidth = textElement.offsetWidth;
const textHeight = textElement.offsetHeight;
if (containerWidth <= 0 || containerHeight <= 0 || textWidth <= 0 || textHeight <= 0) {
return;
}
const nextScale = Math.max(
minScale,
Math.min(maxScale, Math.min(containerWidth / textWidth, containerHeight / textHeight)),
);
if (!Number.isFinite(nextScale)) {
return;
}
setScale((currentScale) => (Math.abs(currentScale - nextScale) < 0.001 ? currentScale : nextScale));
}, [maxScale, minScale]);
useLayoutEffect(() => {
recalculate();
}, [recalculate, text]);
useEffect(() => {
const container = containerRef.current;
const textElement = textRef.current;
if (!container) {
return;
}
const handleResize = () => {
if (resizeFrameRef.current !== null) {
cancelAnimationFrame(resizeFrameRef.current);
}
// Batch resize work into the next animation frame to avoid layout thrashing.
resizeFrameRef.current = requestAnimationFrame(recalculate);
};
const observer = new ResizeObserver(handleResize);
observer.observe(container);
if (textElement) {
observer.observe(textElement);
}
recalculate();
return () => {
observer.disconnect();
if (resizeFrameRef.current !== null) {
cancelAnimationFrame(resizeFrameRef.current);
}
};
}, [recalculate, text]);
return (
<div
ref={containerRef}
className={className}
style={{
position: "relative",
width: "100%",
height: "100%",
overflow: "hidden",
...style,
}}
>
<span
ref={textRef}
style={{
position: "absolute",
top: "50%",
left: "50%",
whiteSpace: "nowrap",
lineHeight: 1,
display: "inline-block",
transform: `translate(-50%, -50%) scale(${scale})`,
transformOrigin: "center center",
willChange: "transform",
}}
>
{text}
</span>
</div>
);
}
export default FitText;
1012300cookie-checkReact FitText component