{"id":10123,"date":"2026-05-01T11:01:17","date_gmt":"2026-05-01T10:01:17","guid":{"rendered":"https:\/\/solidt.eu\/site\/?p=10123"},"modified":"2026-05-01T11:01:19","modified_gmt":"2026-05-01T10:01:19","slug":"react-fittext-component","status":"publish","type":"post","link":"https:\/\/solidt.eu\/site\/react-fittext-component\/","title":{"rendered":"React FitText component"},"content":{"rendered":"\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"typescript\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { type CSSProperties, useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\ninterface FitTextProps {\n    text: string;\n    className?: string;\n    style?: CSSProperties;\n    minScale?: number;\n    maxScale?: number;\n}\n\nexport function FitText({\n    text,\n    className,\n    style,\n    minScale = 0.1,\n    maxScale = Number.POSITIVE_INFINITY,\n}: FitTextProps) {\n    const containerRef = useRef&lt;HTMLDivElement | null>(null);\n    const textRef = useRef&lt;HTMLSpanElement | null>(null);\n    const resizeFrameRef = useRef&lt;number | null>(null);\n    const [scale, setScale] = useState(1);\n\n    const recalculate = useCallback(() => {\n        const container = containerRef.current;\n        const textElement = textRef.current;\n\n        if (!container || !textElement) {\n            return;\n        }\n\n        const containerWidth = container.clientWidth;\n        const containerHeight = container.clientHeight;\n        const textWidth = textElement.offsetWidth;\n        const textHeight = textElement.offsetHeight;\n\n        if (containerWidth &lt;= 0 || containerHeight &lt;= 0 || textWidth &lt;= 0 || textHeight &lt;= 0) {\n            return;\n        }\n\n        const nextScale = Math.max(\n            minScale,\n            Math.min(maxScale, Math.min(containerWidth \/ textWidth, containerHeight \/ textHeight)),\n        );\n\n        if (!Number.isFinite(nextScale)) {\n            return;\n        }\n\n        setScale((currentScale) => (Math.abs(currentScale - nextScale) &lt; 0.001 ? currentScale : nextScale));\n    }, [maxScale, minScale]);\n\n    useLayoutEffect(() => {\n        recalculate();\n    }, [recalculate, text]);\n\n    useEffect(() => {\n        const container = containerRef.current;\n        const textElement = textRef.current;\n\n        if (!container) {\n            return;\n        }\n\n        const handleResize = () => {\n            if (resizeFrameRef.current !== null) {\n                cancelAnimationFrame(resizeFrameRef.current);\n            }\n\n            \/\/ Batch resize work into the next animation frame to avoid layout thrashing.\n            resizeFrameRef.current = requestAnimationFrame(recalculate);\n        };\n\n        const observer = new ResizeObserver(handleResize);\n        observer.observe(container);\n\n        if (textElement) {\n            observer.observe(textElement);\n        }\n\n        recalculate();\n\n        return () => {\n            observer.disconnect();\n\n            if (resizeFrameRef.current !== null) {\n                cancelAnimationFrame(resizeFrameRef.current);\n            }\n        };\n    }, [recalculate, text]);\n\n    return (\n        &lt;div\n            ref={containerRef}\n            className={className}\n            style={{\n                position: \"relative\",\n                width: \"100%\",\n                height: \"100%\",\n                overflow: \"hidden\",\n                ...style,\n            }}\n        >\n            &lt;span\n                ref={textRef}\n                style={{\n                    position: \"absolute\",\n                    top: \"50%\",\n                    left: \"50%\",\n                    whiteSpace: \"nowrap\",\n                    lineHeight: 1,\n                    display: \"inline-block\",\n                    transform: `translate(-50%, -50%) scale(${scale})`,\n                    transformOrigin: \"center center\",\n                    willChange: \"transform\",\n                }}\n            >\n                {text}\n            &lt;\/span>\n        &lt;\/div>\n    );\n}\n\nexport default FitText;\n<\/pre>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-10123","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/10123","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/comments?post=10123"}],"version-history":[{"count":1,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/10123\/revisions"}],"predecessor-version":[{"id":10124,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/10123\/revisions\/10124"}],"wp:attachment":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/media?parent=10123"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/categories?post=10123"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/tags?post=10123"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}