Docs
Icon Text Substitution

Icon Text Substitution

A modern component that replaces words with icons or visual elements in text content.

Installation

Copy and paste the following code into your project.

"use client";
 
import React, { useRef, useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
 
export interface IconTextSubstitutionProps
  extends React.HTMLAttributes<HTMLDivElement> {
  text: string;
  substitutions: {
    word: string;
    icon: React.ReactNode;
    animationStyle?: "bounce" | "pulse" | "rotate" | "shake" | "none";
    backgroundStyle?: "solid" | "gradient" | "glow" | "none";
    backgroundColor?: string;
    scale?: number;
    verticalOffset?: number;
  }[];
  textColor?: string;
  fontSize?: string;
  fontWeight?: string;
  enableHoverEffects?: boolean;
  enableAnimations?: boolean;
  animationSpeed?: number;
  inline?: boolean;
}
 
export function IconTextSubstitution({
  text,
  substitutions = [],
  textColor = "text-gray-200",
  fontSize = "text-2xl",
  fontWeight = "font-semibold",
  enableHoverEffects = true,
  enableAnimations = true,
  animationSpeed = 1,
  inline = false,
  className,
  ...props
}: IconTextSubstitutionProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [segments, setSegments] = useState<
    Array<{
      type: "text" | "icon";
      content: string | number;
      substitution?: any;
    }>
  >([]);
 
  useEffect(() => {
    const processedSegments: Array<{
      type: "text" | "icon";
      content: string | number;
      substitution?: any;
    }> = [];
    let remainingText = text;
    let lastIndex = 0;
 
    const sortedSubstitutions = [...substitutions].sort((a, b) => {
      const indexA = text.toLowerCase().indexOf(a.word.toLowerCase());
      const indexB = text.toLowerCase().indexOf(b.word.toLowerCase());
      return indexA - indexB;
    });
 
    sortedSubstitutions.forEach((sub) => {
      const wordIndex = remainingText
        .toLowerCase()
        .indexOf(sub.word.toLowerCase());
      if (wordIndex !== -1) {
        if (wordIndex > 0) {
          processedSegments.push({
            type: "text",
            content: remainingText.substring(0, wordIndex),
          });
        }
 
        processedSegments.push({
          type: "icon",
          content: processedSegments.length,
          substitution: sub,
        });
 
        remainingText = remainingText.substring(wordIndex + sub.word.length);
      }
    });
 
    if (remainingText) {
      processedSegments.push({
        type: "text",
        content: remainingText,
      });
    }
 
    setSegments(processedSegments);
  }, [text, substitutions]);
 
  const getAnimationVariant = (style: string = "none", speed: number = 1) => {
    const duration = 2 / speed;
 
    switch (style) {
      case "bounce":
        return {
          animate: {
            y: ["-5%", "0%", "-5%"],
            transition: {
              duration,
              repeat: Infinity,
              ease: "easeInOut",
            },
          },
        };
      case "pulse":
        return {
          animate: {
            scale: [1, 1.05, 1],
            opacity: [1, 0.8, 1],
            transition: {
              duration,
              repeat: Infinity,
              ease: "easeInOut",
            },
          },
        };
      case "rotate":
        return {
          animate: {
            rotate: [0, 10, 0, -10, 0],
            transition: {
              duration,
              repeat: Infinity,
              ease: "easeInOut",
            },
          },
        };
      case "shake":
        return {
          animate: {
            x: [0, 3, -3, 3, 0],
            transition: {
              duration: duration / 2,
              repeat: Infinity,
              ease: "easeInOut",
            },
          },
        };
      default:
        return {};
    }
  };
 
  const getBackgroundStyle = (style: string = "none", color?: string) => {
    switch (style) {
      case "solid":
        return {
          backgroundColor: color || "rgba(255, 255, 255, 0.1)",
          borderRadius: "0.5rem",
          padding: "0.25rem 0.5rem",
        };
      case "gradient":
        return {
          background: color
            ? `linear-gradient(135deg, ${color}, rgba(0,0,0,0.3))`
            : "linear-gradient(135deg, rgba(255,255,255,0.2), rgba(0,0,0,0.1))",
          borderRadius: "0.5rem",
          padding: "0.25rem 0.5rem",
        };
      case "glow":
        return {
          backgroundColor: "transparent",
          borderRadius: "0.5rem",
          padding: "0.25rem 0.5rem",
          boxShadow: color
            ? `0 0 10px ${color}`
            : "0 0 10px rgba(255, 255, 255, 0.5)",
        };
      default:
        return {};
    }
  };
 
  return (
    <div
      ref={containerRef}
      className={cn(
        inline ? "inline-flex items-center" : "flex flex-wrap items-center",
        inline ? "gap-0" : "gap-1",
        textColor,
        fontSize,
        fontWeight,
        className,
      )}
      {...props}
    >
      <AnimatePresence>
        {segments.map((segment, index) => {
          if (segment.type === "text") {
            return (
              <span key={`text-${index}`} className="inline-block">
                {segment.content}
              </span>
            );
          } else {
            const {
              icon,
              animationStyle,
              backgroundStyle,
              backgroundColor,
              scale = 1,
              verticalOffset = 0,
            } = segment.substitution;
            const animationVariant = getAnimationVariant(
              animationStyle,
              animationSpeed,
            );
            const bgStyle = getBackgroundStyle(
              backgroundStyle,
              backgroundColor,
            );
 
            return (
              <motion.div
                key={`icon-${segment.content}`}
                className={cn(
                  "inline-flex items-center justify-center",
                  enableHoverEffects && "transition-transform hover:scale-110",
                )}
                style={{
                  ...bgStyle,
                  transform: `scale(${scale})`,
                  position: "relative",
                  top: `${verticalOffset}px`,
                  margin: inline ? "0 1px" : undefined,
                }}
                whileHover={enableHoverEffects ? { scale: scale * 1.1 } : {}}
                {...(enableAnimations && animationStyle !== "none"
                  ? animationVariant
                  : {})}
              >
                {icon}
              </motion.div>
            );
          }
        })}
      </AnimatePresence>
    </div>
  );
}

Update the import paths to match your project setup.

import IconTextSubstitution from "@/components/ui/icon-text-substitution";

Usage

import IconTextSubstitution from "@/components/ui/icon-text-substitution";
 
export default function Example() {
  return (
    <IconTextSubstitution
      text="Travel to your favorite places this summer!"
      substitutions={[
        {
          word: "Travel",
          icon: <img src="/images/passport.png" alt="Passport" />,
          animationStyle: "pulse",
        },
        {
          word: "summer",
          icon: <img src="/images/sun.png" alt="Sun" />,
          animationStyle: "rotate",
        },
      ]}
    />
  );
}

Props

PropTypeDescriptionDefault
textstringText content to be displayed with icon substitutionsRequired
substitutionsSubstitutionItem[]Array of substitution objects defining replacements[]
textColorstringText color for non-substituted texttext-gray-200
fontSizestringFont size for the text (CSS class)text-2xl
fontWeightstringFont weight for the text (CSS class)font-semibold
enableHoverEffectsbooleanWhether to enable hover effects on iconstrue
enableAnimationsbooleanWhether to enable animations for iconstrue
animationSpeednumberAnimation speed multiplier (1 = normal speed)1
inlinebooleanWhether to use inline style for text flowfalse

SubstitutionItem Properties

PropertyTypeDescriptionDefault
wordstringThe word or phrase to be replacedRequired
iconReact.ReactNodeThe icon element to substituteRequired
animationStyle"bounce" | "pulse" | "rotate" | "shake" | "none"Animation style for the icon"none"
backgroundStyle"solid" | "gradient" | "glow" | "none"Background style for the icon"none"
backgroundColorstringCustom background color (CSS color value)-
scalenumberScale factor for the icon (1 = normal size)1
verticalOffsetnumberVertical alignment adjustment (in pixels)0

Examples

Text with Icon Substitutions

<div className="flex flex-col items-center justify-center space-y-8">
  <IconTextSubstitution
    text="Travel to your ❤️ places, this ☀️, book your ➡️ now!"
    fontSize="text-3xl md:text-4xl"
    fontWeight="font-bold"
    textColor="text-gray-300"
    enableHoverEffects={true}
    enableAnimations={true}
    inline={true}
    substitutions={[
      {
        word: "❤️",
        icon: (
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="currentColor"
            className="h-8 w-8 text-red-500"
          >
            <path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
          </svg>
        ),
        animationStyle: "pulse",
        backgroundStyle: "none",
        verticalOffset: -2,
      },
      {
        word: "☀️",
        icon: (
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="currentColor"
            className="h-10 w-10 text-yellow-400"
          >
            <path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z" />
          </svg>
        ),
        animationStyle: "rotate",
        backgroundStyle: "none",
        verticalOffset: -2,
      },
      {
        word: "➡️",
        icon: (
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="currentColor"
            className="h-10 w-10 text-sky-400"
          >
            <path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" />
          </svg>
        ),
        animationStyle: "shake",
        backgroundStyle: "none",
        verticalOffset: -2,
      },
    ]}
  />
</div>

Feature List with Icons

<div className="flex flex-col items-center justify-center space-y-8">
  <div className="w-full max-w-2xl space-y-4">
    <IconTextSubstitution
      text="Secure cloud storage for all your files"
      fontSize="text-xl"
      fontWeight="font-medium"
      textColor="text-gray-300"
      enableHoverEffects={true}
      enableAnimations={true}
      inline={true}
      substitutions={[
        {
          word: "Secure",
          icon: (
            <span className="mr-1 inline-flex items-center">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                className="mr-1 h-5 w-5 text-green-400"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
                />
              </svg>
              <span className="font-medium text-green-400">Secure</span>
            </span>
          ),
          animationStyle: "pulse",
          backgroundStyle: "none",
        },
        {
          word: "cloud",
          icon: (
            <span className="mx-1 inline-flex items-center">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                className="mr-1 h-5 w-5 text-blue-400"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
                />
              </svg>
              <span className="font-medium text-blue-400">cloud</span>
            </span>
          ),
          animationStyle: "pulse",
          backgroundStyle: "none",
          verticalOffset: -1,
        },
      ]}
    />
  </div>
</div>