Text animations

Pulsing Text

A component that applies a subtle, rhythmic pulse animation to its children.

Default

The default component applies a subtle scale animation.

Limited Time Offer!

Variants

The component has two visual variants: scale (default) and opacity.

Scale Variant

Default

Opacity Variant

variant="opacity"

Dynamic Control

You can dynamically control the animation's duration and intensity using props. This example uses sliders to demonstrate the effect in real-time.

Dynamic Pulse

Use Cases & Composition

Use the iterationCount prop for finite animations, like notifications. Use the asChild prop to apply the pulse effect to other components like Button or Card.

New Alert
Featured Plan
The best value for your team.

Unlock all features.

Installation

CLI

Installation via the CLI is coming soon. For now, please follow the manual installation instructions below.

Manual

Install the following dependencies:

npm install @radix-ui/react-slot class-variance-authority

Then, copy and paste the following code into your project:

import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

const pulsingTextVariants = cva('inline-block origin-center', {
  variants: {
    variant: {
      scale: 'animate-pulse-scale',
      opacity: 'animate-pulse-opacity',
    },
  },
  defaultVariants: {
    variant: 'scale',
  },
});

type PulsingTextCssProperties = {
  '--pulse-duration'?: string;
  '--pulse-intensity-scale'?: number;
  '--pulse-intensity-opacity'?: number;
  '--pulse-delay'?: string;
  '--pulse-iteration-count'?: number | 'infinite';
};

type CombinedCssProperties = React.CSSProperties & PulsingTextCssProperties;

export interface PulsingTextProps
  extends React.HTMLAttributes<HTMLSpanElement>,
    VariantProps<typeof pulsingTextVariants> {
  /**
   * If true, the component will merge its props and behavior onto its immediate child.
   * This is useful for applying the pulse effect to other components or elements.
   * @default false
   */
  asChild?: boolean;
  /**
   * The duration of one full pulse cycle in seconds.
   * @default 2
   */
  duration?: number;
  /**
   * A normalized value from 0 to 1 that controls the strength of the pulse effect.
   * @default 0.5
   */
  intensity?: number;
  /**
   * The delay before the animation starts, in seconds.
   * @default 0
   */
  delay?: number;
  /**
   * The number of times the animation should repeat.
   * Use 'infinite' for a never-ending loop.
   * @default 'infinite'
   */
  iterationCount?: number | 'infinite';
  /**
   * Allows passing a standard style object, fully typed to include the animation's
   * controlling CSS Custom Properties.
   */
  style?: CombinedCssProperties;
}

/**
 * A dynamic, performant server component that applies a subtle, rhythmic pulse animation.
 * It is controlled via props that are translated to CSS Custom Properties, allowing for
 * a wide range of visual effects without client-side JavaScript.
 */
const PulsingText = React.forwardRef<HTMLSpanElement, PulsingTextProps>(
  (
    {
      className,
      style,
      asChild = false,
      variant = 'scale',
      duration,
      intensity,
      delay,
      iterationCount,
      ...props
    },
    ref
  ) => {
    const Comp = asChild ? Slot : 'span';

    // WHY: Clamp the intensity to a 0-1 range to ensure predictable and safe animation values.
    const clampedIntensity =
      intensity === undefined ? undefined : Math.max(0, Math.min(1, intensity));

    const dynamicStyles: CombinedCssProperties = {
      ...style,
    };

    if (duration !== undefined) {
      dynamicStyles['--pulse-duration'] = `${duration}s`;
    }
    if (clampedIntensity !== undefined) {
      if (variant === 'scale') {
        dynamicStyles['--pulse-intensity-scale'] = 1 + clampedIntensity * 0.1;
      } else {
        dynamicStyles['--pulse-intensity-opacity'] =
          1 - clampedIntensity * 0.25;
      }
    }
    if (delay !== undefined) {
      dynamicStyles['--pulse-delay'] = `${delay}s`;
    }
    if (iterationCount !== undefined) {
      dynamicStyles['--pulse-iteration-count'] = iterationCount;
    }

    return (
      <Comp
        className={cn(pulsingTextVariants({ variant, className }))}
        style={dynamicStyles}
        ref={ref}
        {...props}
      />
    );
  }
);
PulsingText.displayName = 'PulsingText';

export { PulsingText };

Usage

Import the component and use it to wrap text or other components.

import { Button } from '@/components/ui/button';
import { PulsingText } from '@/components/ui/pulsing-text';

export default function Example() {
  return (
    <div className='flex flex-col items-center gap-8'>
      {/* Default usage */}
      <PulsingText className='text-xl font-semibold'>
        Limited Time Offer
      </PulsingText>

      {/* Customized usage on a button */}
      <PulsingText asChild variant='opacity' duration={1.5} intensity={0.8}>
        <Button>Get Started Now</Button>
      </PulsingText>
    </div>
  );
}

Props

PropTypeDefaultDescription
asChildbooleanfalseIf true, the component will merge its props and behavior onto its immediate child.
durationnumber2The duration of one full pulse cycle in seconds.
intensitynumber0.5A normalized value from 0 to 1 that controls the strength of the pulse effect.
delaynumber0The delay before the animation starts, in seconds.
iterationCountnumber | 'infinite''infinite'The number of times the animation should repeat.
variant'scale' | 'opacity''scale'The type of pulse animation to apply.
styleReact.CSSPropertiesAllows passing a standard style object, including the animation's controlling CSS Custom Properties.