svg-text

PreviousNext
Docs
reuiui

Preview

Loading preview…
registry/default/ui/svg-text.tsx
'use client';

import * as React from 'react';
import { ElementType, ReactNode, useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/utils';

export interface SvgTextProps {
  /**
   * The SVG content to display inside the text
   */
  svg: ReactNode;
  /**
   * The content to display (will have the SVG "inside" it)
   */
  children: ReactNode;
  /**
   * Additional className for the container
   */
  className?: string;
  /**
   * Font size for the text mask (in viewport width units or CSS units)
   * @default "20vw"
   */
  fontSize?: string | number;
  /**
   * Font weight for the text mask
   * @default "bold"
   */
  fontWeight?: string | number;
  /**
   * The element type to render for the container
   * @default "div"
   */
  as?: ElementType;
}

/**
 * SvgText displays content with an SVG background fill effect.
 * The SVG is masked by the content, creating a dynamic text look.
 */
export function SvgText({
  svg,
  children,
  className = '',
  fontSize = '20vw',
  fontWeight = 'bold',
  as: Component = 'div',
}: SvgTextProps) {
  const textRef = useRef<HTMLDivElement>(null);
  const [textDimensions, setTextDimensions] = useState({ width: 0, height: 0 });
  const content = React.Children.toArray(children).join('');
  const maskId = React.useId();

  useEffect(() => {
    if (!textRef.current) return;

    const updateDimensions = () => {
      const rect = textRef.current?.getBoundingClientRect();
      if (rect) {
        setTextDimensions({
          width: Math.max(rect.width, 200),
          height: Math.max(rect.height, 100),
        });
      }
    };

    // Initial measurement
    updateDimensions();

    // Use ResizeObserver for better performance
    const resizeObserver = new ResizeObserver(updateDimensions);
    resizeObserver.observe(textRef.current);

    return () => resizeObserver.disconnect();
  }, [content, fontSize, fontWeight]);

  return (
    <Component className={cn('relative inline-block', className)}>
      {/* Hidden text for measuring */}
      <div
        ref={textRef}
        className="opacity-0 absolute pointer-events-none font-bold whitespace-nowrap"
        style={{
          fontSize: typeof fontSize === 'number' ? `${fontSize}px` : fontSize,
          fontWeight,
          fontFamily: 'system-ui, -apple-system, sans-serif',
        }}
      >
        {content}
      </div>

      {/* SVG with text mask */}
      <svg
        className="block"
        width={textDimensions.width}
        height={textDimensions.height}
        viewBox={`0 0 ${textDimensions.width} ${textDimensions.height}`}
        style={{
          fontSize: typeof fontSize === 'number' ? `${fontSize}px` : fontSize,
          fontWeight,
          fontFamily: 'system-ui, -apple-system, sans-serif',
        }}
      >
        <defs>
          <mask id={maskId}>
            <rect width="100%" height="100%" fill="black" />
            <text
              x="50%"
              y="50%"
              textAnchor="middle"
              dominantBaseline="central"
              fill="white"
              style={{
                fontSize: typeof fontSize === 'number' ? `${fontSize}px` : fontSize,
                fontWeight,
                fontFamily: 'system-ui, -apple-system, sans-serif',
              }}
            >
              {content}
            </text>
          </mask>
        </defs>

        {/* Background SVG with proper scaling */}
        <g mask={`url(#${maskId})`}>
          <foreignObject
            width="100%"
            height="100%"
            style={{
              overflow: 'visible',
            }}
          >
            <div
              style={{
                width: `${textDimensions.width}px`,
                height: `${textDimensions.height}px`,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
              }}
            >
              <div
                style={{
                  width: '400px',
                  height: '200px',
                  transform: `scale(${Math.max(textDimensions.width / 400, textDimensions.height / 200)})`,
                  transformOrigin: 'center',
                }}
              >
                {svg}
              </div>
            </div>
          </foreignObject>
        </g>
      </svg>

      {/* Screen reader text */}
      <span className="sr-only">{content}</span>
    </Component>
  );
}

Installation

npx shadcn@latest add @reui/svg-text

Usage

import { SvgText } from "@/components/ui/svg-text"
<SvgText />