undefined
Component Library

React Bits
Library

A curated collection of interactive React components and effects. Each component includes live demos, complete code examples, and implementation notes for easy integration into your projects.

Live Demos
Complete Code
Copy & Paste
2+
Components
100%
Interactive
0
Dependencies
Text Pressure Component

Text Pressure

Interactive text animation that responds to cursor movement. Each character dynamically changes weight, width, and opacity based on proximity to the cursor.

Live Demo
Move your cursor over the text below

PRESSURE

Weight

Font weight: 100-900

Width

Character width: 75-125

Opacity

Range: 0.3-1.0

Implementation Code

/* HTML Structure */
<div id="textPressureContainer" class="relative w-full h-80 flex items-center justify-center">
  <h1 id="textPressureTitle" class="text-6xl font-bold text-white uppercase tracking-wider cursor-default select-none">
    PRESSURE
  </h1>
</div>

/* CSS Styles */
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
  
  .text-pressure-title {
    font-family: 'Inter', sans-serif;
    font-variation-settings: 'wght' 400, 'wdth' 100;
    transition: all 0.1s ease-out;
    text-shadow: 0 0 30px rgba(255, 255, 255, 0.3);
  }
  
  .text-pressure-title span {
    display: inline-block;
    transition: all 0.1s ease-out;
    font-variation-settings: inherit;
  }
</style>

/* JavaScript Implementation */
<script>
(function() {
  const container = document.getElementById('textPressureContainer');
  const title = document.getElementById('textPressureTitle');
  
  if (!container || !title) return;
  
  const text = title.textContent;
  const chars = text.split('');
  
  // Clear and rebuild with spans
  title.innerHTML = '';
  const spans = [];
  
  chars.forEach((char, i) => {
    const span = document.createElement('span');
    span.textContent = char;
    span.style.display = 'inline-block';
    spans.push(span);
    title.appendChild(span);
  });
  
  const mouse = { x: 0, y: 0 };
  const cursor = { x: 0, y: 0 };
  
  // Initialize cursor position
  const containerRect = container.getBoundingClientRect();
  mouse.x = cursor.x = containerRect.left + containerRect.width / 2;
  mouse.y = cursor.y = containerRect.top + containerRect.height / 2;
  
  // Distance calculation
  const dist = (a, b) => {
    const dx = b.x - a.x;
    const dy = b.y - a.y;
    return Math.sqrt(dx * dx + dy * dy);
  };
  
  // Mouse/touch event handlers
  const handleMouseMove = (e) => {
    cursor.x = e.clientX;
    cursor.y = e.clientY;
  };
  
  const handleTouchMove = (e) => {
    const touch = e.touches[0];
    cursor.x = touch.clientX;
    cursor.y = touch.clientY;
  };
  
  window.addEventListener('mousemove', handleMouseMove);
  window.addEventListener('touchmove', handleTouchMove, { passive: false });
  
  // Animation loop
  let rafId;
  const animate = () => {
    // Smooth cursor following
    mouse.x += (cursor.x - mouse.x) / 15;
    mouse.y += (cursor.y - mouse.y) / 15;
    
    const titleRect = title.getBoundingClientRect();
    const maxDist = titleRect.width / 2;
    
    spans.forEach((span) => {
      const rect = span.getBoundingClientRect();
      const charCenter = {
        x: rect.x + rect.width / 2,
        y: rect.y + rect.height / 2,
      };
      
      const d = dist(mouse, charCenter);
      
      const getAttr = (distance, minVal, maxVal) => {
        const val = maxVal - Math.abs((maxVal * distance) / maxDist);
        return Math.max(minVal, val + minVal);
      };
      
      const wdth = Math.floor(getAttr(d, 75, 125));
      const wght = Math.floor(getAttr(d, 100, 900));
      const alphaVal = getAttr(d, 0.3, 1).toFixed(2);
      
      span.style.opacity = alphaVal;
      span.style.fontVariationSettings = `'wght' ${wght}, 'wdth' ${wdth}`;
    });
    
    rafId = requestAnimationFrame(animate);
  };
  
  animate();
  
  // Cleanup function
  const cleanup = () => {
    if (rafId) {
      cancelAnimationFrame(rafId);
    }
    window.removeEventListener('mousemove', handleMouseMove);
    window.removeEventListener('touchmove', handleTouchMove);
  };
  
  window.textPressureCleanup = cleanup;
})();
</script>

Customization Options

  • Text: Change the text content in the h1 element
  • Font: Replace 'Inter' with any variable font
  • Colors: Modify text color and text-shadow
  • Ranges: Adjust min/max values in getAttr function
  • Speed: Change the division value (15) for cursor follow speed

Implementation Tips

  • Use variable fonts for best results
  • Ensure proper cleanup to prevent memory leaks
  • Test on mobile devices for touch events
  • Consider performance on low-end devices
  • Add fallbacks for browsers without variable font support
Blur Text Component

Blur Text

Animated text that reveals with a blur-to-clear effect. Perfect for creating smooth, eye-catching text animations that trigger when scrolled into view.

Live Demo
Scroll down to see the animations trigger
Animation by Words - From Top

Isn't this so cool?!

Animation by Characters - From Bottom

Character by Character

Custom Styling - Words

Beautiful blur animations made simple

Intersection Observer

Triggers when scrolled into view

Staggered Timing

Customizable delay between elements

Direction Control

Animate from top or bottom

Flexible Splitting

Animate by words or characters

Implementation Code

/* HTML Structure */
<div class="blur-text-container" data-delay="150" data-animate-by="words" data-direction="top">
  <p class="text-4xl font-bold text-gray-800">
    Isn't this so cool?!
  </p>
</div>

/* CSS Styles */
<style>
  .blur-text-container {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
  }
  
  .blur-text-container p {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    margin: 0;
  }
  
  .blur-text-segment {
    display: inline-block;
    transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
    will-change: transform, filter, opacity;
  }
  
  .blur-text-segment.from-top {
    filter: blur(10px);
    opacity: 0;
    transform: translateY(-20px);
  }
  
  .blur-text-segment.from-bottom {
    filter: blur(10px);
    opacity: 0;
    transform: translateY(20px);
  }
  
  .blur-text-segment.animate-in {
    filter: blur(0px);
    opacity: 1;
    transform: translateY(0);
  }
</style>

/* JavaScript Implementation */
<script>
class BlurText {
  constructor(container) {
    this.container = container;
    this.delay = parseInt(container.dataset.delay) || 150;
    this.animateBy = container.dataset.animateBy || 'words';
    this.direction = container.dataset.direction || 'top';
    this.hasAnimated = false;
    
    this.init();
  }
  
  init() {
    const textElement = this.container.querySelector('p');
    if (!textElement) return;
    
    const text = textElement.textContent;
    const segments = this.animateBy === 'words' ? text.split(' ') : text.split('');
    
    // Clear and rebuild with spans
    textElement.innerHTML = '';
    this.segments = [];
    
    segments.forEach((segment, index) => {
      const span = document.createElement('span');
      span.className = `blur-text-segment from-${this.direction}`;
      span.textContent = segment;
      
      if (this.animateBy === 'words' && index < segments.length - 1) {
        span.textContent += ' ';
      }
      
      this.segments.push(span);
      textElement.appendChild(span);
    });
    
    this.setupIntersectionObserver();
  }
  
  setupIntersectionObserver() {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting && !this.hasAnimated) {
            this.animate();
            this.hasAnimated = true;
          }
        });
      },
      { threshold: 0.1, rootMargin: '0px' }
    );
    
    observer.observe(this.container);
    this.observer = observer;
  }
  
  animate() {
    this.segments.forEach((segment, index) => {
      setTimeout(() => {
        segment.classList.add('animate-in');
      }, index * this.delay);
    });
  }
  
  reset() {
    this.hasAnimated = false;
    this.segments.forEach(segment => {
      segment.classList.remove('animate-in');
    });
    if (this.observer) {
      this.observer.disconnect();
      this.setupIntersectionObserver();
    }
  }
  
  destroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }
}

// Initialize all blur text components
document.addEventListener('DOMContentLoaded', () => {
  const containers = document.querySelectorAll('.blur-text-container');
  window.blurTextInstances = [];
  
  containers.forEach(container => {
    const instance = new BlurText(container);
    window.blurTextInstances.push(instance);
  });
});

// Reset function
function resetBlurAnimations() {
  if (window.blurTextInstances) {
    window.blurTextInstances.forEach(instance => {
      instance.reset();
    });
  }
}
</script>

Configuration Options

  • data-delay: Delay between elements (ms)
  • data-animate-by: "words" or "characters"
  • data-direction: "top" or "bottom"
  • Threshold: Intersection observer threshold
  • Animation: CSS transition duration and easing

Best Practices

  • Use shorter delays for character animations
  • Test on different screen sizes
  • Consider reduced motion preferences
  • Use appropriate intersection thresholds
  • Clean up observers when components unmount
Circular Text Component

Circular Text

Animated circular text component with customizable rotation speed and hover effects. Perfect for logos, decorative elements, and interactive text displays.

Live Demo
Hover over the circular text to see different effects
Speed Up on Hover
REACT* BITS* COMPONENTS*
Slow Down on Hover
DESIGN* DEVELOP* DEPLOY*
Pause on Hover
HOVER* TO* PAUSE*
Speed Control

Customizable rotation speed

Hover Effects

Multiple hover interactions

Responsive

Mobile-friendly design

Customizable

Easy styling and theming

Implementation Code

/* HTML Structure */
<div class="circular-text-container" data-text="REACT*BITS*COMPONENTS*" data-speed="20" data-hover="speedUp">
  <div class="circular-text">
    <!-- Letters will be generated dynamically -->
  </div>
</div>

/* CSS Styles */
<style>
  .circular-text-container {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 200px;
    height: 200px;
    margin: 0 auto;
  }
  
  .circular-text {
    position: relative;
    width: 200px;
    height: 200px;
    border-radius: 50%;
    transform-origin: 50% 50%;
    animation: rotate 20s linear infinite;
    cursor: pointer;
    transition: animation-duration 0.5s ease;
  }
  
  .circular-text span {
    position: absolute;
    font-size: 18px;
    font-weight: bold;
    color: #059669;
    transform-origin: 0 0;
    transition: all 0.3s ease;
    width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .circular-text:hover span {
    color: #047857;
    transform: scale(1.1);
  }
  
  @keyframes rotate {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
  
  @keyframes rotate-fast {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
  
  @keyframes rotate-slow {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
  
  .speed-up {
    animation: rotate-fast 5s linear infinite;
  }
  
  .slow-down {
    animation: rotate-slow 40s linear infinite;
  }
  
  .paused {
    animation-play-state: paused;
  }
</style>

/* JavaScript Implementation */
<script>
class CircularText {
  constructor(container) {
    this.container = container;
    this.textElement = container.querySelector('.circular-text');
    this.text = container.dataset.text || 'CIRCULAR*TEXT*';
    this.speed = parseInt(container.dataset.speed) || 20;
    this.hoverEffect = container.dataset.hover || 'speedUp';
    this.originalAnimationDuration = this.speed;
    
    this.init();
  }
  
  init() {
    this.createLetters();
    this.setupAnimation();
    this.bindEvents();
  }
  
  createLetters() {
    const letters = Array.from(this.text);
    this.textElement.innerHTML = '';
    
    const radius = 80; // Distance from center to letters
    const centerX = 100; // Center of the 200px container
    const centerY = 100; // Center of the 200px container

    letters.forEach((letter, index) => {
      const span = document.createElement('span');
      span.textContent = letter;
      
      // Calculate angle for each letter
      const angle = (360 / letters.length) * index;
      const radian = (angle * Math.PI) / 180;
      
      // Calculate position on circumference
      const x = centerX + radius * Math.cos(radian);
      const y = centerY + radius * Math.sin(radian);
      
      // Position the letter and rotate it to face outward
      span.style.left = `${x}px`;
      span.style.top = `${y}px`;
      span.style.transform = `translate(-50%, -50%) rotate(${angle + 90}deg)`;
      
      this.textElement.appendChild(span);
    });
  }
  
  setupAnimation() {
    this.textElement.style.animationDuration = `${this.speed}s`;
  }
  
  bindEvents() {
    this.textElement.addEventListener('mouseenter', () => {
      this.handleHover();
    });
    
    this.textElement.addEventListener('mouseleave', () => {
      this.resetAnimation();
    });
  }
  
  handleHover() {
    this.textElement.classList.remove('speed-up', 'slow-down', 'paused');
    
    switch (this.hoverEffect) {
      case 'speedUp':
        this.textElement.classList.add('speed-up');
        break;
      case 'slowDown':
        this.textElement.classList.add('slow-down');
        break;
      case 'pause':
        this.textElement.classList.add('paused');
        break;
      case 'goBonkers':
        this.textElement.style.animationDuration = '0.5s';
        this.textElement.style.transform = 'scale(0.8)';
        break;
    }
  }
  
  resetAnimation() {
    this.textElement.classList.remove('speed-up', 'slow-down', 'paused');
    this.textElement.style.animationDuration = `${this.speed}s`;
    this.textElement.style.transform = 'scale(1)';
  }
  
  updateText(newText) {
    this.text = newText;
    this.createLetters();
  }
  
  updateSpeed(newSpeed) {
    this.speed = newSpeed;
    this.setupAnimation();
  }
  
  destroy() {
    this.textElement.removeEventListener('mouseenter', this.handleHover);
    this.textElement.removeEventListener('mouseleave', this.resetAnimation);
  }
}

// Initialize all circular text components
document.addEventListener('DOMContentLoaded', () => {
  const containers = document.querySelectorAll('.circular-text-container');
  window.circularTextInstances = [];
  
  containers.forEach(container => {
    const instance = new CircularText(container);
    window.circularTextInstances.push(instance);
  });
});
</script>

Configuration Options

  • data-text: Text to display in circle
  • data-speed: Rotation speed in seconds
  • data-hover: Hover effect (speedUp, slowDown, pause, goBonkers)
  • Size: Customize width/height via CSS
  • Colors: Modify text color and hover states

Best Practices

  • Use asterisks (*) or dots (•) as separators
  • Keep text concise for better readability
  • Test on different screen sizes
  • Consider reduced motion preferences
  • Ensure sufficient contrast for accessibility
Shiny Text Component

Shiny Text

Add a glossy, shimmering effect to your text with customizable speed and intensity. Perfect for highlighting important content and creating eye-catching headings.

Live Demo
Watch the shimmer effect move across the text
Fast Speed (2s)

Lightning Fast Shine!

Medium Speed (4s)

Perfect Balance

Slow Speed (8s)

Elegant & Smooth

Disabled (No Animation)

Static Text

Speed Control

Adjustable animation speed

Toggle State

Enable/disable animation

Customizable

Easy color and style changes

Lightweight

Pure CSS animations

Implementation Code

/* HTML Structure */
<div class="shiny-text" data-speed="3" data-disabled="false">
  Just some shiny text!
</div>

/* CSS Styles */
<style>
  .shiny-text {
    color: #b5b5b5a4;
    background: linear-gradient(
      120deg,
      rgba(255, 255, 255, 0) 40%,
      rgba(255, 255, 255, 0.8) 50%,
      rgba(255, 255, 255, 0) 60%
    );
    background-size: 200% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    display: inline-block;
    animation: shine 5s linear infinite;
  }

  @keyframes shine {
    0% {
      background-position: 100%;
    }
    100% {
      background-position: -100%;
    }
  }

  .shiny-text.disabled {
    animation: none;
  }
</style>

/* JavaScript Implementation */
<script>
class ShinyText {
  constructor(element) {
    this.element = element;
    this.text = element.textContent;
    this.speed = parseInt(element.dataset.speed) || 5;
    this.disabled = element.dataset.disabled === 'true';
    
    this.init();
  }
  
  init() {
    this.setupAnimation();
    this.applyState();
  }
  
  setupAnimation() {
    this.element.style.animationDuration = `${this.speed}s`;
  }
  
  applyState() {
    if (this.disabled) {
      this.element.classList.add('disabled');
    } else {
      this.element.classList.remove('disabled');
    }
  }
  
  updateSpeed(newSpeed) {
    this.speed = newSpeed;
    this.element.style.animationDuration = `${this.speed}s`;
  }
  
  enable() {
    this.disabled = false;
    this.element.classList.remove('disabled');
  }
  
  disable() {
    this.disabled = true;
    this.element.classList.add('disabled');
  }
  
  toggle() {
    if (this.disabled) {
      this.enable();
    } else {
      this.disable();
    }
  }
  
  updateText(newText) {
    this.text = newText;
    this.element.textContent = newText;
  }
  
  destroy() {
    this.element.classList.remove('shiny-text', 'disabled');
    this.element.style.animationDuration = '';
  }
}

// Initialize all shiny text components
document.addEventListener('DOMContentLoaded', () => {
  const elements = document.querySelectorAll('.shiny-text');
  window.shinyTextInstances = [];
  
  elements.forEach(element => {
    const instance = new ShinyText(element);
    window.shinyTextInstances.push(instance);
  });
});

// Helper function to create shiny text programmatically
function createShinyText(text, speed = 5, disabled = false) {
  const element = document.createElement('span');
  element.className = 'shiny-text';
  element.textContent = text;
  element.dataset.speed = speed;
  element.dataset.disabled = disabled;
  
  const instance = new ShinyText(element);
  window.shinyTextInstances.push(instance);
  
  return { element, instance };
}
</script>

Configuration Options

  • data-speed: Animation speed in seconds
  • data-disabled: Enable/disable animation
  • color: Change base text color
  • gradient: Customize shine effect colors
  • background-size: Control shine width

Best Practices

  • Use for headlines and important text
  • Avoid on small text for better readability
  • Consider reduced motion preferences
  • Test contrast ratios for accessibility
  • Use sparingly to maintain impact
Split Text Component

Split Text

Animate text by splitting it into individual characters, words, or lines with customizable stagger effects. Perfect for creating engaging text reveals and scroll-triggered animations.

Live Demo
Scroll to see the text animation effects
Split by Characters

Hello, Amazing World!

Split by Words

Beautiful Text Animation

Split by Lines

Multi-line Text
Animation Effect
Is Really Cool

Instant Animation (No Scroll)

Immediate Effect!

Split Types

Characters, words, or lines

Stagger Control

Customizable timing delays

Scroll Trigger

Animate on scroll into view

Smooth Effects

CSS-based animations

Implementation Code

/* HTML Structure */
<div class="split-text-container" data-split="chars" data-delay="50" data-duration="0.6" data-trigger="scroll">
  <h3 class="text-4xl font-bold text-gray-800">
    Hello, Amazing World!
  </h3>
</div>

/* CSS Styles */
<style>
  .split-text-container {
    overflow: hidden;
  }
  
  .split-text-container .split-char,
  .split-text-container .split-word,
  .split-text-container .split-line {
    display: inline-block;
    opacity: 0;
    transform: translateY(40px);
    transition: all 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  
  .split-text-container .split-char.animate,
  .split-text-container .split-word.animate,
  .split-text-container .split-line.animate {
    opacity: 1;
    transform: translateY(0);
  }
  
  .split-text-container .split-line {
    display: block;
    width: 100%;
  }
  
  .split-text-container .split-word {
    margin-right: 0.25em;
  }
  
  .split-text-container .split-char {
    margin-right: 0;
  }
</style>

/* JavaScript Implementation */
<script>
class SplitText {
  constructor(container) {
    this.container = container;
    this.textElement = container.querySelector('h3') || container.querySelector('p') || container.querySelector('span');
    this.text = this.textElement.textContent;
    this.splitType = container.dataset.split || 'chars';
    this.delay = parseInt(container.dataset.delay) || 50;
    this.duration = parseFloat(container.dataset.duration) || 0.6;
    this.trigger = container.dataset.trigger || 'scroll';
    this.hasAnimated = false;
    
    this.init();
  }
  
  init() {
    this.splitText();
    this.setupTrigger();
  }
  
  splitText() {
    const text = this.text;
    this.textElement.innerHTML = '';
    
    switch (this.splitType) {
      case 'chars':
        this.splitIntoChars(text);
        break;
      case 'words':
        this.splitIntoWords(text);
        break;
      case 'lines':
        this.splitIntoLines(text);
        break;
      default:
        this.splitIntoChars(text);
    }
  }
  
  splitIntoChars(text) {
    const chars = Array.from(text);
    chars.forEach((char, index) => {
      const span = document.createElement('span');
      span.textContent = char === ' ' ? '\u00A0' : char;
      span.className = 'split-char';
      span.style.transitionDelay = `${index * this.delay}ms`;
      span.style.transitionDuration = `${this.duration}s`;
      this.textElement.appendChild(span);
    });
  }
  
  splitIntoWords(text) {
    const words = text.split(' ');
    words.forEach((word, index) => {
      const span = document.createElement('span');
      span.textContent = word;
      span.className = 'split-word';
      span.style.transitionDelay = `${index * this.delay}ms`;
      span.style.transitionDuration = `${this.duration}s`;
      this.textElement.appendChild(span);
    });
  }
  
  splitIntoLines(text) {
    const lines = text.split(/\r?\n/);
    lines.forEach((line, index) => {
      const span = document.createElement('span');
      span.textContent = line;
      span.className = 'split-line';
      span.style.transitionDelay = `${index * this.delay}ms`;
      span.style.transitionDuration = `${this.duration}s`;
      this.textElement.appendChild(span);
    });
  }
  
  setupTrigger() {
    if (this.trigger === 'instant') {
      this.animate();
    } else {
      this.setupIntersectionObserver();
    }
  }
  
  setupIntersectionObserver() {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting && !this.hasAnimated) {
            this.animate();
            this.hasAnimated = true;
          }
        });
      },
      { threshold: 0.1, rootMargin: '0px' }
    );
    
    observer.observe(this.container);
    this.observer = observer;
  }
  
  animate() {
    const elements = this.textElement.querySelectorAll('.split-char, .split-word, .split-line');
    elements.forEach(element => {
      element.classList.add('animate');
    });
  }
  
  reset() {
    this.hasAnimated = false;
    const elements = this.textElement.querySelectorAll('.split-char, .split-word, .split-line');
    elements.forEach(element => {
      element.classList.remove('animate');
    });
    if (this.observer) {
      this.observer.disconnect();
      this.setupIntersectionObserver();
    }
  }
  
  destroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
    this.textElement.innerHTML = this.text;
  }
}

// Initialize all split text components
document.addEventListener('DOMContentLoaded', () => {
  const containers = document.querySelectorAll('.split-text-container');
  window.splitTextInstances = [];
  
  containers.forEach(container => {
    const instance = new SplitText(container);
    window.splitTextInstances.push(instance);
  });
});

// Reset function for testing
function resetSplitAnimations() {
  if (window.splitTextInstances) {
    window.splitTextInstances.forEach(instance => {
      instance.reset();
    });
  }
}
</script>

Configuration Options

  • data-split: Split type (chars, words, lines)
  • data-delay: Stagger delay in milliseconds
  • data-duration: Animation duration in seconds
  • data-trigger: Trigger type (scroll, instant)
  • CSS transitions: Customize easing and effects

Best Practices

  • Use shorter delays for character splits
  • Test on different screen sizes
  • Consider reduced motion preferences
  • Use appropriate intersection thresholds
  • Keep text concise for better effect
Curved Loop Component

Curved Loop

Create mesmerizing curved text animations that flow along customizable paths. Perfect for hero sections, artistic displays, and interactive text experiences.

Live Demo
Click and drag to interact with the curved text
Basic Curved Loop
Shallow Curve - Fast Speed
Deep Curve - Slow Speed
Non-Interactive Mode
Interactive

Drag to control animation

Curved Path

Customizable curve intensity

Speed Control

Adjustable animation speed

Direction

Left or right movement

Implementation Code

/* HTML Structure */
<div class="curved-loop-container" data-text="Welcome to React Bits ✦ " data-speed="2" data-curve="300" data-direction="left" data-interactive="true">
  <svg class="curved-loop-svg" viewBox="0 0 1440 200">
    <defs>
      <path id="curve-1" d="M-100,100 Q720,400 1540,100" fill="none" stroke="transparent"/>
    </defs>
    <text font-weight="bold" fill="#10b981" class="curved-text">
      <textPath href="#curve-1"></textPath>
    </text>
  </svg>
</div>

/* CSS Styles */
<style>
  .curved-loop-container {
    min-height: 200px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    cursor: grab;
  }
  
  .curved-loop-container.dragging {
    cursor: grabbing;
  }
  
  .curved-loop-container.non-interactive {
    cursor: default;
  }
  
  .curved-loop-svg {
    user-select: none;
    width: 100%;
    height: 200px;
    overflow: visible;
    display: block;
    font-size: 2rem;
    font-weight: 700;
    letter-spacing: 2px;
    text-transform: uppercase;
    line-height: 1;
  }
  
  .curved-text {
    font-family: inherit;
  }
</style>

/* JavaScript Implementation */
<script>
class CurvedLoop {
  constructor(container) {
    this.container = container;
    this.text = container.dataset.text || 'Curved Text ✦ ';
    this.speed = parseFloat(container.dataset.speed) || 2;
    this.curve = parseInt(container.dataset.curve) || 300;
    this.direction = container.dataset.direction || 'left';
    this.interactive = container.dataset.interactive !== 'false';
    
    this.textPath = container.querySelector('textPath');
    this.path = container.querySelector('path');
    this.svg = container.querySelector('svg');
    
    this.isDragging = false;
    this.lastMouseX = 0;
    this.velocity = 0;
    this.currentOffset = 0;
    this.animationId = null;
    this.spacing = 0;
    this.pathLength = 0;
    
    this.init();
  }
  
  init() {
    this.setupPath();
    this.setupText();
    this.setupInteraction();
    this.startAnimation();
  }
  
  setupPath() {
    const pathD = `M-100,100 Q720,${100 + this.curve} 1540,100`;
    this.path.setAttribute('d', pathD);
    this.pathLength = this.path.getTotalLength();
  }
  
  setupText() {
    // Create multiple text spans to create seamless loop
    const textLength = this.text.length;
    const svgWidth = 1440;
    const charWidth = 20; // Approximate character width
    this.spacing = charWidth;
    
    const repeats = Math.ceil(this.pathLength / (textLength * charWidth)) + 3;
    let textContent = '';
    
    for (let i = 0; i < repeats; i++) {
      textContent += this.text;
    }
    
    this.textPath.textContent = textContent;
    
    // Set cursor style
    if (this.interactive) {
      this.container.classList.add('interactive');
    } else {
      this.container.classList.add('non-interactive');
    }
  }
  
  setupInteraction() {
    if (!this.interactive) return;
    
    this.container.addEventListener('mousedown', this.onMouseDown.bind(this));
    this.container.addEventListener('mousemove', this.onMouseMove.bind(this));
    this.container.addEventListener('mouseup', this.onMouseUp.bind(this));
    this.container.addEventListener('mouseleave', this.onMouseUp.bind(this));
    
    // Touch events
    this.container.addEventListener('touchstart', this.onTouchStart.bind(this));
    this.container.addEventListener('touchmove', this.onTouchMove.bind(this));
    this.container.addEventListener('touchend', this.onTouchEnd.bind(this));
  }
  
  onMouseDown(e) {
    this.isDragging = true;
    this.lastMouseX = e.clientX;
    this.velocity = 0;
    this.container.classList.add('dragging');
  }
  
  onMouseMove(e) {
    if (!this.isDragging) return;
    
    const deltaX = e.clientX - this.lastMouseX;
    this.lastMouseX = e.clientX;
    this.velocity = deltaX;
    
    this.currentOffset += deltaX;
    this.updateTextPosition();
  }
  
  onMouseUp() {
    this.isDragging = false;
    this.container.classList.remove('dragging');
    
    // Set direction based on drag velocity
    if (Math.abs(this.velocity) > 2) {
      this.direction = this.velocity > 0 ? 'right' : 'left';
    }
  }
  
  onTouchStart(e) {
    this.onMouseDown({ clientX: e.touches[0].clientX });
  }
  
  onTouchMove(e) {
    e.preventDefault();
    this.onMouseMove({ clientX: e.touches[0].clientX });
  }
  
  onTouchEnd() {
    this.onMouseUp();
  }
  
  updateTextPosition() {
    const offset = this.currentOffset % (this.text.length * this.spacing);
    this.textPath.setAttribute('startOffset', `${offset}px`);
  }
  
  startAnimation() {
    const animate = () => {
      if (!this.isDragging) {
        const delta = this.direction === 'right' ? this.speed : -this.speed;
        this.currentOffset += delta;
        this.updateTextPosition();
      }
      
      this.animationId = requestAnimationFrame(animate);
    };
    
    animate();
  }
  
  destroy() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId);
    }
    
    // Remove event listeners
    this.container.removeEventListener('mousedown', this.onMouseDown);
    this.container.removeEventListener('mousemove', this.onMouseMove);
    this.container.removeEventListener('mouseup', this.onMouseUp);
    this.container.removeEventListener('mouseleave', this.onMouseUp);
    this.container.removeEventListener('touchstart', this.onTouchStart);
    this.container.removeEventListener('touchmove', this.onTouchMove);
    this.container.removeEventListener('touchend', this.onTouchEnd);
  }
}

// Initialize all curved loop components
document.addEventListener('DOMContentLoaded', () => {
  const containers = document.querySelectorAll('.curved-loop-container');
  window.curvedLoopInstances = [];
  
  containers.forEach(container => {
    const instance = new CurvedLoop(container);
    window.curvedLoopInstances.push(instance);
  });
});
</script>

Configuration Options

  • data-text: Text content to display
  • data-speed: Animation speed (1-10)
  • data-curve: Curve intensity (100-500)
  • data-direction: Direction (left/right)
  • data-interactive: Enable drag (true/false)

Best Practices

  • Use symbols (✦) for better visual separation
  • Keep text concise for better readability
  • Test on mobile devices for touch interaction
  • Use appropriate curve values for your design
  • Consider accessibility for motion-sensitive users
Gradient Text Component

Gradient Text

Create stunning animated gradient text effects with customizable colors, speed, and optional borders. Perfect for headlines, call-to-actions, and brand statements.

Live Demo
Watch the animated gradient effects in action
Classic Blue-Green Gradient
Add a splash of color!
Rainbow Spectrum
Rainbow Magic
Sunset Colors with Border
Sunset Vibes
Fast Purple-Pink Animation
Lightning Fast!
Cyberpunk Style with Border
CYBER PUNK
Custom Colors

Any color combination

Animation Speed

Adjustable animation timing

Optional Border

Animated gradient borders

CSS-Based

Pure CSS animations

Implementation Code

/* React Component Implementation */
import React from 'react';

interface GradientTextProps {
  children: React.ReactNode;
  className?: string;
  colors?: string[];
  animationSpeed?: number;
  showBorder?: boolean;
}

const GradientText: React.FC<GradientTextProps> = ({
  children,
  className = '',
  colors = ['#40ffaa', '#4079ff', '#40ffaa', '#4079ff', '#40ffaa'],
  animationSpeed = 8,
  showBorder = false,
}) => {
  const gradientColors = colors.join(', ');
  const gradientStyle = `linear-gradient(90deg, ${gradientColors})`;
  
  const containerClasses = [
    'gradient-text-container',
    showBorder ? 'has-border' : '',
    className
  ].filter(Boolean).join(' ');

  return (
    <div 
      className={containerClasses}
      style={{
        ...(showBorder ? {
          background: gradientStyle,
          backgroundSize: '300% 100%',
          animationDuration: `${animationSpeed}s`
        } : {})
      }}
    >
      <div
        className="text-content"
        style={{
          background: gradientStyle,
          backgroundSize: '300% 100%',
          animationDuration: `${animationSpeed}s`
        }}
      >
        {children}
      </div>
    </div>
  );
};

export default GradientText;

/* Usage Examples */
// Basic usage
<GradientText>
  Add a splash of color!
</GradientText>

// Custom colors and speed
<GradientText 
  colors={['#ff0080', '#ff8c00', '#40ff00', '#00ff80', '#0080ff', '#8000ff']}
  animationSpeed={4}
>
  Rainbow Magic
</GradientText>

// With border and custom styling
<GradientText 
  colors={['#ff6b6b', '#ffa726', '#ffeb3b', '#ffa726', '#ff6b6b']}
  animationSpeed={2}
  showBorder={true}
  className="text-4xl font-bold px-6 py-3"
>
  Sunset Vibes
</GradientText>

// Fast animation
<GradientText 
  colors={['#e91e63', '#9c27b0', '#673ab7', '#9c27b0', '#e91e63']}
  animationSpeed={1}
>
  Lightning Fast!
</GradientText>

// Cyberpunk style with border
<GradientText 
  colors={['#00f5ff', '#ff00ff', '#00ff41', '#ff00ff', '#00f5ff']}
  animationSpeed={2.5}
  showBorder={true}
  className="text-4xl font-bold px-8 py-4"
>
  CYBER PUNK
</GradientText>

/* Required CSS Styles */
<style>
  .gradient-text-container {
    position: relative;
    margin: 0 auto;
    display: flex;
    max-width: fit-content;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    border-radius: 1.25rem;
    font-weight: 500;
    transition: all 0.3s ease;
    overflow: hidden;
    cursor: pointer;
  }

  .gradient-text-container.has-border {
    background: linear-gradient(90deg, var(--gradient-colors, #40ffaa, #4079ff, #40ffaa, #4079ff, #40ffaa));
    background-size: 300% 100%;
    animation: gradient-border linear infinite;
    padding: 3px;
  }

  .gradient-text-container.has-border::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border-radius: calc(1.25rem - 3px);
    width: calc(100% - 6px);
    height: calc(100% - 6px);
    background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
    z-index: 1;
  }

  .text-content {
    display: inline-block;
    position: relative;
    z-index: 2;
    background: linear-gradient(90deg, var(--gradient-colors, #40ffaa, #4079ff, #40ffaa, #4079ff, #40ffaa));
    background-size: 300% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    animation: gradient-text linear infinite;
    font-weight: bold;
  }

  /* Specific gradient definitions for each demo */
  .gradient-text-container[data-colors*="#40ffaa"] .text-content {
    background: linear-gradient(90deg, #40ffaa, #4079ff, #40ffaa, #4079ff, #40ffaa);
    background-size: 300% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  .gradient-text-container[data-colors*="#ff0080"] .text-content {
    background: linear-gradient(90deg, #ff0080, #ff8c00, #40ff00, #00ff80, #0080ff, #8000ff, #ff0080);
    background-size: 300% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  .gradient-text-container[data-colors*="#ff6b6b"] .text-content {
    background: linear-gradient(90deg, #ff6b6b, #ffa726, #ffeb3b, #ffa726, #ff6b6b);
    background-size: 300% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  .gradient-text-container[data-colors*="#e91e63"] .text-content {
    background: linear-gradient(90deg, #e91e63, #9c27b0, #673ab7, #9c27b0, #e91e63);
    background-size: 300% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  .gradient-text-container[data-colors*="#00f5ff"] .text-content {
    background: linear-gradient(90deg, #00f5ff, #ff00ff, #00ff41, #ff00ff, #00f5ff);
    background-size: 300% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  /* Apply same gradients to borders */
  .gradient-text-container.has-border[data-colors*="#40ffaa"] {
    background: linear-gradient(90deg, #40ffaa, #4079ff, #40ffaa, #4079ff, #40ffaa);
    background-size: 300% 100%;
  }

  .gradient-text-container.has-border[data-colors*="#ff6b6b"] {
    background: linear-gradient(90deg, #ff6b6b, #ffa726, #ffeb3b, #ffa726, #ff6b6b);
    background-size: 300% 100%;
  }

  .gradient-text-container.has-border[data-colors*="#00f5ff"] {
    background: linear-gradient(90deg, #00f5ff, #ff00ff, #00ff41, #ff00ff, #00f5ff);
    background-size: 300% 100%;
  }

  /* Animation speeds */
  .gradient-text-container[data-speed="1"] .text-content,
  .gradient-text-container[data-speed="1"].has-border {
    animation-duration: 1s;
  }

  .gradient-text-container[data-speed="2"] .text-content,
  .gradient-text-container[data-speed="2"].has-border {
    animation-duration: 2s;
  }

  .gradient-text-container[data-speed="2.5"] .text-content,
  .gradient-text-container[data-speed="2.5"].has-border {
    animation-duration: 2.5s;
  }

  .gradient-text-container[data-speed="3"] .text-content,
  .gradient-text-container[data-speed="3"].has-border {
    animation-duration: 3s;
  }

  .gradient-text-container[data-speed="4"] .text-content,
  .gradient-text-container[data-speed="4"].has-border {
    animation-duration: 4s;
  }

  @keyframes gradient-text {
    0% {
      background-position: 0% 50%;
    }
    50% {
      background-position: 100% 50%;
    }
    100% {
      background-position: 0% 50%;
    }
  }

  @keyframes gradient-border {
    0% {
      background-position: 0% 50%;
    }
    50% {
      background-position: 100% 50%;
    }
    100% {
      background-position: 0% 50%;
    }
  }

  /* Hover effects */
  .gradient-text-container:hover {
    /* No hover effects */
  }

  /* Fallback for browsers that don't support background-clip: text */
  @supports not (-webkit-background-clip: text) {
    .text-content {
      background: none !important;
      -webkit-text-fill-color: initial !important;
      color: #40ffaa !important;
    }
  }

  /* Ensure text is visible for accessibility */
  .text-content::selection {
    background: rgba(64, 255, 170, 0.3);
    -webkit-text-fill-color: #40ffaa;
  }
</style>

/* Props Documentation */
interface GradientTextProps {
  /**
   * The content to be displayed inside the gradient text.
   */
  children: React.ReactNode;
  
  /**
   * Adds custom classes to the root element for additional styling.
   * @default ''
   */
  className?: string;
  
  /**
   * Defines the gradient colors for the text or border.
   * @default ['#40ffaa', '#4079ff', '#40ffaa', '#4079ff', '#40ffaa']
   */
  colors?: string[];
  
  /**
   * The duration of the gradient animation in seconds.
   * @default 8
   */
  animationSpeed?: number;
  
  /**
   * Determines whether a border with the gradient effect is displayed.
   * @default false
   */
  showBorder?: boolean;
}

/* Installation & Setup */
// 1. Copy the GradientText component
// 2. Add the required CSS styles
// 3. Import and use in your React application

// Example with Tailwind CSS classes
<GradientText 
  className="text-6xl font-extrabold"
  colors={['#667eea', '#764ba2', '#f093fb', '#f5576c']}
  animationSpeed={3}
  showBorder={true}
>
  Beautiful Gradient Text
</GradientText>

Configuration Options

  • data-colors: Comma-separated color values
  • data-speed: Animation speed in seconds
  • data-border: Enable gradient border (true/false)
  • text-content: Apply to any text element
  • background-clip: Creates gradient text effect

Best Practices

  • Use 3-5 colors for smooth gradients
  • Choose contrasting colors for visibility
  • Test animation speed for readability
  • Use borders for extra visual impact
  • Consider accessibility for motion sensitivity
Click Spark Component

Click Spark

Add delightful spark animations that trigger on user clicks. Customize colors, size, count, and animation duration for engaging interactive experiences.

Live Demo
Click anywhere on the demo areas to see spark effects
Default White Sparks
Click me!
Golden Sparks - Large Size
Golden Sparks ✨
Fast Cyan Sparks - Many Count
Fast Sparks ⚡
Large Radius - Slow Animation
Big Sparks 🎆
Interactive Button with Sparks
Click for Sparks!
Click Triggered

Activates on user clicks

Custom Colors

Any hex color support

Adjustable Size

Control spark dimensions

Animation Control

Duration & easing options

Implementation Code

/* React Component */
import { useRef, useEffect, useCallback } from "react";

const ClickSpark = ({
  sparkColor = "#fff",
  sparkSize = 10,
  sparkRadius = 15,
  sparkCount = 8,
  duration = 400,
  easing = "ease-out",
  extraScale = 1.0,
  children
}) => {
  const canvasRef = useRef(null);
  const sparksRef = useRef([]);
  const startTimeRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const parent = canvas.parentElement;
    if (!parent) return;

    let resizeTimeout;

    const resizeCanvas = () => {
      const { width, height } = parent.getBoundingClientRect();
      if (canvas.width !== width || canvas.height !== height) {
        canvas.width = width;
        canvas.height = height;
      }
    };

    const handleResize = () => {
      clearTimeout(resizeTimeout);
      resizeTimeout = setTimeout(resizeCanvas, 100);
    };

    const ro = new ResizeObserver(handleResize);
    ro.observe(parent);

    resizeCanvas();

    return () => {
      ro.disconnect();
      clearTimeout(resizeTimeout);
    };
  }, []);

  const easeFunc = useCallback(
    (t) => {
      switch (easing) {
        case "linear":
          return t;
        case "ease-in":
          return t * t;
        case "ease-in-out":
          return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
        default:
          return t * (2 - t);
      }
    },
    [easing]
  );

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");

    let animationId;

    const draw = (timestamp) => {
      if (!startTimeRef.current) {
        startTimeRef.current = timestamp;
      }
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      sparksRef.current = sparksRef.current.filter((spark) => {
        const elapsed = timestamp - spark.startTime;
        if (elapsed >= duration) {
          return false;
        }

        const progress = elapsed / duration;
        const eased = easeFunc(progress);

        const distance = eased * sparkRadius * extraScale;
        const lineLength = sparkSize * (1 - eased);

        const x1 = spark.x + distance * Math.cos(spark.angle);
        const y1 = spark.y + distance * Math.sin(spark.angle);
        const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle);
        const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle);

        ctx.strokeStyle = sparkColor;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();

        return true;
      });

      animationId = requestAnimationFrame(draw);
    };

    animationId = requestAnimationFrame(draw);

    return () => {
      cancelAnimationFrame(animationId);
    };
  }, [
    sparkColor,
    sparkSize,
    sparkRadius,
    sparkCount,
    duration,
    easeFunc,
    extraScale,
  ]);

  const handleClick = (e) => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const now = performance.now();
    const newSparks = Array.from({ length: sparkCount }, (_, i) => ({
      x,
      y,
      angle: (2 * Math.PI * i) / sparkCount,
      startTime: now,
    }));

    sparksRef.current.push(...newSparks);
  };

  return (
    <div 
      style={{
        position: 'relative',
        width: '100%',
        height: '100%'
      }}
      onClick={handleClick}
    >
      <canvas
        ref={canvasRef}
        style={{
          width: "100%",
          height: "100%",
          display: "block",
          userSelect: "none",
          position: "absolute",
          top: 0,
          left: 0,
          pointerEvents: "none"
        }}
      />
      {children}
    </div>
  );
};

export default ClickSpark;

/* Usage Examples */
// Basic usage
<ClickSpark>
  <button>Click me!</button>
</ClickSpark>

// Custom properties
<ClickSpark
  sparkColor="#ff6b6b"
  sparkSize={12}
  sparkRadius={20}
  sparkCount={10}
  duration={500}
  easing="ease-in-out"
>
  <div className="interactive-content">
    Custom spark effect!
  </div>
</ClickSpark>

// Fast, small sparks
<ClickSpark
  sparkColor="#00ff88"
  sparkSize={6}
  sparkRadius={10}
  sparkCount={16}
  duration={250}
>
  <span>Quick sparks!</span>
</ClickSpark>

// Large, slow sparks
<ClickSpark
  sparkColor="#fbbf24"
  sparkSize={15}
  sparkRadius={30}
  sparkCount={6}
  duration={800}
  easing="ease-out"
>
  <div>Big dramatic sparks!</div>
</ClickSpark>

Configuration Options

  • sparkColor: Hex color of sparks
  • sparkSize: Length of spark lines
  • sparkRadius: Maximum travel distance
  • sparkCount: Number of sparks per click
  • duration: Animation duration in ms
  • easing: Animation curve (linear, ease-in, ease-out, ease-in-out)

Best Practices

  • Use contrasting colors for visibility
  • Adjust spark count based on element size
  • Keep duration between 200-800ms for best UX
  • Test on mobile devices for performance
  • Consider accessibility for motion-sensitive users
Circular Gallery Component

Circular Gallery

Create stunning curved image galleries with WebGL rendering. Features smooth scrolling, interactive navigation, and customizable bend effects for immersive visual experiences.

OGL Library Required

This component requires the OGL WebGL library to be installed in your project. Install it with: npm install ogl

Live Demo
Drag or scroll to navigate through the gallery
Standard Curve (bend: 3)
Flat Layout (bend: 0)
Extreme Curve (bend: 8)
WebGL Powered

Hardware-accelerated 3D rendering

Interactive

Drag, scroll, and touch controls

Curved Layout

Customizable bend effects

Responsive

Works on all devices

Navigation Controls

Mouse: Click & drag to navigate
Wheel: Scroll to move gallery
Touch: Swipe on mobile devices

Implementation Code

/* Installation */
npm install ogl

/* React Component */
import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from "ogl";
import { useEffect, useRef } from "react";

// Complete implementation with all classes and functionality
// ... (Full code would be very long, showing condensed version)

const CircularGallery = ({
  items,
  bend = 3,
  textColor = "#ffffff",
  borderRadius = 0.05,
  font = "bold 30px Figtree",
  scrollSpeed = 2,
  scrollEase = 0.05,
}) => {
  const containerRef = useRef(null);
  
  useEffect(() => {
    const app = new App(containerRef.current, {
      items,
      bend,
      textColor,
      borderRadius,
      font,
      scrollSpeed,
      scrollEase
    });
    
    return () => {
      app.destroy();
    };
  }, [items, bend, textColor, borderRadius, font, scrollSpeed, scrollEase]);
  
  return (
    <div 
      className="w-full h-full overflow-hidden cursor-grab active:cursor-grabbing" 
      ref={containerRef} 
    />
  );
};

export default CircularGallery;

/* Usage Examples */
// Basic usage with default images
<div style={{ height: '600px', position: 'relative' }}>
  <CircularGallery />
</div>

// Custom gallery with your images
<div style={{ height: '600px', position: 'relative' }}>
  <CircularGallery
    bend={3}
    textColor="#ffffff"
    borderRadius={0.05}
    scrollEase={0.02}
    items={[
      { image: "/path/to/image1.jpg", text: "Image 1" },
      { image: "/path/to/image2.jpg", text: "Image 2" },
      { image: "/path/to/image3.jpg", text: "Image 3" }
    ]}
  />
</div>

// Flat layout (no curve)
<div style={{ height: '600px', position: 'relative' }}>
  <CircularGallery
    bend={0}
    textColor="#000000"
    borderRadius={0.1}
    scrollSpeed={1}
    items={customImages}
  />
</div>

// Extreme curve effect
<div style={{ height: '600px', position: 'relative' }}>
  <CircularGallery
    bend={8}
    textColor="#ffffff"
    borderRadius={0.02}
    scrollSpeed={3}
    scrollEase={0.1}
  />
</div>

/* Key Features */
// - WebGL-powered 3D rendering with OGL library
// - Interactive drag, scroll, and touch controls
// - Customizable curve effects (bend parameter)
// - Responsive design that adapts to container size
// - Smooth scrolling with configurable easing
// - Support for custom images and text labels
// - Automatic image aspect ratio handling
// - Rounded corners with border radius control
// - Performance optimized with proper cleanup

Configuration Options

  • items: Array of {image, text} objects
  • bend: Curve intensity (0 = flat, higher = more curved)
  • textColor: Color of image labels
  • borderRadius: Rounded corner amount (0-1)
  • scrollSpeed: Navigation speed
  • scrollEase: Smoothness of movement

Best Practices

  • Use consistent image aspect ratios for best results
  • Set explicit height on container element
  • Test bend values between 0-10 for optimal curves
  • Consider WebGL compatibility for target browsers
  • Optimize images for web to improve loading performance
Rotating Text Component

Rotating Text

Create dynamic rotating text animations with Framer Motion. Features customizable transitions, stagger effects, and multiple animation modes for engaging text displays and hero sections.

Library Requirements & Alternatives

For React Projects: This component requires Framer Motion to be installed: npm install framer-motion

For Regular Websites: You can't directly use React libraries on static HTML sites. However, I can create CSS-only versions of these animations that work without any external dependencies!

💡 Want this on your Landingsite.ai website? Ask me to create a pure CSS version that works without any libraries!

Live Demo
Watch the rotating text animations in action
Character Stagger Animation
React
Word-by-Word Rotation
Building
Styled Badge with Background
Modern
Large Display Text
Amazing
Smooth Animations

Framer Motion powered transitions

Stagger Effects

Character, word, and line staggering

Customizable

Timing, transitions, and styling

Accessible

Screen reader friendly

Animation Options

Split By: Characters, words, or lines
Timing: Customizable intervals and delays
Direction: First, last, center, or random

Implementation Code

/* Installation */
npm install framer-motion

/* React Component */
"use client";

import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useState,
} from "react";
import { motion, AnimatePresence } from "framer-motion";

function cn(...classes) {
  return classes.filter(Boolean).join(" ");
}

const RotatingText = forwardRef((props, ref) => {
  const {
    texts,
    transition = { type: "spring", damping: 25, stiffness: 300 },
    initial = { y: "100%", opacity: 0 },
    animate = { y: 0, opacity: 1 },
    exit = { y: "-120%", opacity: 0 },
    animatePresenceMode = "wait",
    animatePresenceInitial = false,
    rotationInterval = 2000,
    staggerDuration = 0,
    staggerFrom = "first",
    loop = true,
    auto = true,
    splitBy = "characters",
    onNext,
    mainClassName,
    splitLevelClassName,
    elementLevelClassName,
    ...rest
  } = props;

  const [currentTextIndex, setCurrentTextIndex] = useState(0);

  const splitIntoCharacters = (text) => {
    if (typeof Intl !== "undefined" && Intl.Segmenter) {
      const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
      return Array.from(segmenter.segment(text), (segment) => segment.segment);
    }
    return Array.from(text);
  };

  const elements = useMemo(() => {
    const currentText = texts[currentTextIndex];
    if (splitBy === "characters") {
      const words = currentText.split(" ");
      return words.map((word, i) => ({
        characters: splitIntoCharacters(word),
        needsSpace: i !== words.length - 1,
      }));
    }
    if (splitBy === "words") {
      return currentText.split(" ").map((word, i, arr) => ({
        characters: [word],
        needsSpace: i !== arr.length - 1,
      }));
    }
    if (splitBy === "lines") {
      return currentText.split("\n").map((line, i, arr) => ({
        characters: [line],
        needsSpace: i !== arr.length - 1,
      }));
    }

    return currentText.split(splitBy).map((part, i, arr) => ({
      characters: [part],
      needsSpace: i !== arr.length - 1,
    }));
  }, [texts, currentTextIndex, splitBy]);

  const getStaggerDelay = useCallback(
    (index, totalChars) => {
      const total = totalChars;
      if (staggerFrom === "first") return index * staggerDuration;
      if (staggerFrom === "last") return (total - 1 - index) * staggerDuration;
      if (staggerFrom === "center") {
        const center = Math.floor(total / 2);
        return Math.abs(center - index) * staggerDuration;
      }
      if (staggerFrom === "random") {
        const randomIndex = Math.floor(Math.random() * total);
        return Math.abs(randomIndex - index) * staggerDuration;
      }
      return Math.abs(staggerFrom - index) * staggerDuration;
    },
    [staggerFrom, staggerDuration]
  );

  const handleIndexChange = useCallback(
    (newIndex) => {
      setCurrentTextIndex(newIndex);
      if (onNext) onNext(newIndex);
    },
    [onNext]
  );

  const next = useCallback(() => {
    const nextIndex =
      currentTextIndex === texts.length - 1
        ? loop
          ? 0
          : currentTextIndex
        : currentTextIndex + 1;
    if (nextIndex !== currentTextIndex) {
      handleIndexChange(nextIndex);
    }
  }, [currentTextIndex, texts.length, loop, handleIndexChange]);

  const previous = useCallback(() => {
    const prevIndex =
      currentTextIndex === 0
        ? loop
          ? texts.length - 1
          : currentTextIndex
        : currentTextIndex - 1;
    if (prevIndex !== currentTextIndex) {
      handleIndexChange(prevIndex);
    }
  }, [currentTextIndex, texts.length, loop, handleIndexChange]);

  const jumpTo = useCallback(
    (index) => {
      const validIndex = Math.max(0, Math.min(index, texts.length - 1));
      if (validIndex !== currentTextIndex) {
        handleIndexChange(validIndex);
      }
    },
    [texts.length, currentTextIndex, handleIndexChange]
  );

  const reset = useCallback(() => {
    if (currentTextIndex !== 0) {
      handleIndexChange(0);
    }
  }, [currentTextIndex, handleIndexChange]);

  useImperativeHandle(
    ref,
    () => ({
      next,
      previous,
      jumpTo,
      reset,
    }),
    [next, previous, jumpTo, reset]
  );

  useEffect(() => {
    if (!auto) return;
    const intervalId = setInterval(next, rotationInterval);
    return () => clearInterval(intervalId);
  }, [next, rotationInterval, auto]);

  return (
    <motion.span
      className={cn(
        "flex flex-wrap whitespace-pre-wrap relative",
        mainClassName
      )}
      {...rest}
      layout
      transition={transition}
    >
      <span className="sr-only">{texts[currentTextIndex]}</span>
      <AnimatePresence mode={animatePresenceMode} initial={animatePresenceInitial}>
        <motion.span
          key={currentTextIndex}
          className={cn(
            splitBy === "lines"
              ? "flex flex-col w-full"
              : "flex flex-wrap whitespace-pre-wrap relative"
          )}
          layout
          aria-hidden="true"
        >
          {elements.map((wordObj, wordIndex, array) => {
            const previousCharsCount = array
              .slice(0, wordIndex)
              .reduce((sum, word) => sum + word.characters.length, 0);
            return (
              <span key={wordIndex} className={cn("inline-flex", splitLevelClassName)}>
                {wordObj.characters.map((char, charIndex) => (
                  <motion.span
                    key={charIndex}
                    initial={initial}
                    animate={animate}
                    exit={exit}
                    transition={{
                      ...transition,
                      delay: getStaggerDelay(
                        previousCharsCount + charIndex,
                        array.reduce((sum, word) => sum + word.characters.length, 0)
                      ),
                    }}
                    className={cn("inline-block", elementLevelClassName)}
                  >
                    {char}
                  </motion.span>
                ))}
                {wordObj.needsSpace && <span className="whitespace-pre"> </span>}
              </span>
            );
          })}
        </motion.span>
      </AnimatePresence>
    </motion.span>
  );
});

RotatingText.displayName = "RotatingText";
export default RotatingText;

/* Usage Examples */
// Basic character stagger
<RotatingText
  texts={['React', 'Bits', 'Is', 'Cool!']}
  mainClassName="text-4xl font-bold text-violet-600"
  staggerFrom="first"
  staggerDuration={0.025}
  rotationInterval={2000}
/>

// Word-by-word rotation
<RotatingText
  texts={['Building', 'Creating', 'Designing', 'Crafting']}
  splitBy="words"
  mainClassName="text-3xl font-semibold text-purple-700"
  initial={{ opacity: 0, scale: 0.8 }}
  animate={{ opacity: 1, scale: 1 }}
  exit={{ opacity: 0, scale: 0.8 }}
  transition={{ type: "spring", damping: 20, stiffness: 200 }}
/>

// Styled badge with background
<RotatingText
  texts={['Modern', 'Clean', 'Beautiful', 'Responsive']}
  mainClassName="px-4 py-2 bg-gradient-to-r from-violet-500 to-purple-500 text-white rounded-lg font-bold"
  staggerFrom="center"
  staggerDuration={0.03}
  splitLevelClassName="overflow-hidden"
  rotationInterval={1500}
/>

// Large display with gradient text
<RotatingText
  texts={['Amazing', 'Stunning', 'Incredible', 'Fantastic']}
  mainClassName="text-6xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-fuchsia-400"
  staggerFrom="last"
  staggerDuration={0.05}
  initial={{ y: "100%", rotateX: 90 }}
  animate={{ y: 0, rotateX: 0 }}
  exit={{ y: "-100%", rotateX: -90 }}
  transition={{ type: "spring", damping: 30, stiffness: 400 }}
/>

// Custom controlled rotation
const rotatingTextRef = useRef(null);

const handleNext = () => {
  rotatingTextRef.current?.next();
};

const handlePrevious = () => {
  rotatingTextRef.current?.previous();
};

<div className="flex items-center gap-4">
  <button onClick={handlePrevious}>Previous</button>
  <RotatingText
    ref={rotatingTextRef}
    texts={['React', 'Vue', 'Angular', 'Svelte']}
    auto={false}
    mainClassName="text-2xl font-bold text-gray-800"
  />
  <button onClick={handleNext}>Next</button>
</div>

// Line-by-line rotation
<RotatingText
  texts={[
    'First line\nSecond line\nThird line',
    'Line A\nLine B\nLine C',
    'Alpha\nBeta\nGamma'
  ]}
  splitBy="lines"
  mainClassName="text-xl font-medium text-center"
  staggerFrom="first"
  staggerDuration={0.1}
  rotationInterval={3000}
/>

Configuration Options

  • texts: Array of text strings to rotate
  • splitBy: "characters", "words", or "lines"
  • staggerFrom: "first", "last", "center", or "random"
  • rotationInterval: Time between rotations (ms)
  • staggerDuration: Delay between elements
  • transition: Framer Motion transition object

Best Practices

  • Use character splitting for smooth letter-by-letter effects
  • Keep rotation intervals above 1000ms for readability
  • Use overflow-hidden on parent for clean animations
  • Test with screen readers for accessibility
  • Consider reduced motion preferences