M-005 Text Animation medium

How to create a text reveal slide テキストスライド表示の作り方

Beautiful animation where text slides up word by word from below. Customize reveal speed and word delay. テキストが単語ごとに下からスライドして表示される美しいアニメーション。表示速度と間隔を調整できます。

ライブデモ Live Demo

Beautiful text animations made simple
0.8s
0.2s

概要・用途・特徴Overview, Usage & Features

何ができるかWhat it does

Beautiful animation where text slides up word by word from below. Customize reveal speed and word delay.

テキストが単語ごとに下からスライドして表示される美しいアニメーション。表示速度と間隔を調整できます。

どこで使うかWhere to use

web application, marketing page

ヒーロー見出し、セクションタイトル、ランディングページ、プレゼンテーション風UI

特徴Key features

Text enters from below a clipping mask, creating a clean reveal slide-in effect. CSS overflow: hidden on the wrapper acts as the curtain. Triggerable via IntersectionObserver or class toggle. No external libraries.

クリッピングマスク下からテキストが入る清潔感あるリビールスライドイン。ラッパーのoverflow:hiddenがカーテンの役割を果たす。IntersectionObserverまたはクラストグルで発火。外部ライブラリ不要。

調整可能パラメータ Adjustable Parameters

Parameter Default Description
--reveal-durationanimation duration for each word (seconds)
--reveal-delaydelay between each word reveal (seconds)
--reveal-distanceslide distance in pixels
backgroundbackground color or gradient customization
easinganimation easing function adjustment

実装コード Implementation Code

// react/M-005.jsx
import React, { useState, useRef } from 'react';
import './M-005.css';

const TextRevealSlide = ({ 
  text = "Beautiful text animations made simple",
  duration = 0.8,
  delay = 0.2,
  distance = 60,
  className = "",
  onAnimationComplete 
}) => {
  const [isAnimating, setIsAnimating] = useState(false);
  const containerRef = useRef(null);

  const words = text.split(' ');

  const replay = () => {
    if (isAnimating) return;
    
    setIsAnimating(true);
    const wordElements = containerRef.current?.querySelectorAll('.reveal-word span');
    
    wordElements?.forEach((word) => {
      word.style.animation = 'none';
      word.offsetHeight; // trigger reflow
      word.style.animation = '';
    });

    // Reset animation state after animation completes
    const totalDuration = (words.length * delay + duration) * 1000;
    setTimeout(() => {
      setIsAnimating(false);
      onAnimationComplete?.();
    }, totalDuration);
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      replay();
    }
  };

  return (
    <div className={`text-reveal-container ${className}`}>
      <div 
        ref={containerRef}
        className="text-reveal" 
        style={{
          '--reveal-duration': `${duration}s`,
          '--reveal-delay': `${delay}s`,
          '--reveal-distance': `${distance}px`
        }}
      >
        {words.map((word, index) => (
          <span key={`${word}-${index}`} className="reveal-word">
            <span style={{ animationDelay: `${index * delay}s` }}>
              {word}
            </span>
          </span>
        ))}
      </div>
      <button 
        className="replay-button"
        onClick={replay}
        onKeyDown={handleKeyDown}
        disabled={isAnimating}
        aria-label="Replay animation"
      >
        {isAnimating ? 'Playing...' : 'Replay'}
      </button>
    </div>
  );
};

// Usage example component
const TextRevealSlideDemo = () => {
  const [settings, setSettings] = useState({
    duration: 0.8,
    delay: 0.2,
    distance: 60,
    text: "Beautiful text animations made simple"
  });

  const handleSettingChange = (key, value) => {
    setSettings(prev => ({
      ...prev,
      [key]: value
    }));
  };

  return (
    <div className="text-reveal-demo">
      <div className="demo-stage">
        <TextRevealSlide 
          text={settings.text}
          duration={settings.duration}
          delay={settings.delay}
          distance={settings.distance}
          onAnimationComplete={() => console.log('Animation completed!')}
        />
      </div>
      
      <div className="controls">
        <div className="control-group">
          <label htmlFor="duration-slider">
            Animation Duration: {settings.duration.toFixed(1)}s
          </label>
          <input
            id="duration-slider"
            type="range"
            min="0.3"
            max="1.5"
            step="0.1"
            value={settings.duration}
            onChange={(e) => handleSettingChange('duration', parseFloat(e.target.value))}
          />
        </div>
        
        <div className="control-group">
          <label htmlFor="delay-slider">
            Word Delay: {settings.delay.toFixed(2)}s
          </label>
          <input
            id="delay-slider"
            type="range"
            min="0.1"
            max="0.5"
            step="0.05"
            value={settings.delay}
            onChange={(e) => handleSettingChange('delay', parseFloat(e.target.value))}
          />
        </div>
        
        <div className="control-group">
          <label htmlFor="distance-slider">
            Slide Distance: {settings.distance}px
          </label>
          <input
            id="distance-slider"
            type="range"
            min="20"
            max="100"
            step="10"
            value={settings.distance}
            onChange={(e) => handleSettingChange('distance', parseInt(e.target.value))}
          />
        </div>
        
        <div className="control-group">
          <label htmlFor="text-input">
            Text Content:
          </label>
          <input
            id="text-input"
            type="text"
            value={settings.text}
            onChange={(e) => handleSettingChange('text', e.target.value)}
            placeholder="Enter your text here"
          />
        </div>
      </div>
    </div>
  );
};

export default TextRevealSlide;
export { TextRevealSlideDemo };
/* react/M-005.css */
/* M-005 Text Reveal Slide Styles */

.text-reveal-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
}

.text-reveal {
  font-size: clamp(28px, 5vw, 48px);
  font-weight: 700;
  color: #ffffff;
  line-height: 1.3;
  text-align: center;
  max-width: 600px;
  
  /* CSS Custom Properties for customization */
  --reveal-duration: 0.8s;
  --reveal-delay: 0.2s;
  --reveal-distance: 60px;
}

.reveal-word {
  display: inline-block;
  overflow: hidden;
  margin: 0 0.1em;
  vertical-align: bottom;
}

.reveal-word span {
  display: inline-block;
  transform: translateY(var(--reveal-distance));
  opacity: 0;
  animation: slideReveal var(--reveal-duration) cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
  text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}

@keyframes slideReveal {
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* Demo Stage Styles */
.text-reveal-demo {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  padding: 2rem;
}

.demo-stage {
  border-radius: 18px;
  padding: 48px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border: 1px solid rgba(255, 255, 255, 0.1);
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  margin-bottom: 2rem;
}

.replay-button {
  padding: 0.75rem 1.5rem;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 1rem;
}

.replay-button:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.replay-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.replay-button:focus {
  outline: 2px solid #667eea;
  outline-offset: 2px;
}

/* Controls Styles */
.controls {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1.5rem;
  padding: 1.5rem;
  background: #f8fafc;
  border-radius: 12px;
  border: 1px solid #e2e8f0;
}

.control-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.control-group label {
  font-weight: 600;
  color: #374151;
  font-size: 0.875rem;
}

.control-group input[type="range"] {
  width: 100%;
  height: 6px;
  background: #e2e8f0;
  border-radius: 3px;
  outline: none;
  cursor: pointer;
}

.control-group input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 18px;
  height: 18px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.control-group input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.control-group input[type="text"] {
  padding: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  font-size: 1rem;
  transition: border-color 0.2s ease;
}

.control-group input[type="text"]:focus {
  outline: none;
  border-color: #667eea;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

/* Dark theme support */
@media (prefers-color-scheme: dark) {
  .controls {
    background: #1f2937;
    border-color: #374151;
  }
  
  .control-group label {
    color: #f3f4f6;
  }
  
  .control-group input[type="text"] {
    background: #374151;
    border-color: #4b5563;
    color: #f3f4f6;
  }
  
  .control-group input[type="text"]:focus {
    border-color: #667eea;
  }
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .text-reveal-demo {
    padding: 1rem;
  }
  
  .demo-stage {
    padding: 24px;
    min-height: 150px;
  }
  
  .controls {
    grid-template-columns: 1fr;
    gap: 1rem;
  }
}

/* High contrast mode support */
@media (prefers-contrast: high) {
  .demo-stage {
    border: 2px solid #000;
  }
  
  .replay-button {
    border: 2px solid #000;
  }
  
  .control-group input[type="range"]::-webkit-slider-thumb {
    border: 3px solid #000;
  }
}
import React, { useState, useRef } from 'react';
import './M-005.css';

const TextRevealSlide = ({ 
  text = "Beautiful text animations made simple",
  duration = 0.8,
  delay = 0.2,
  distance = 60,
  className = "",
  onAnimationComplete 
}) => {
  const [isAnimating, setIsAnimating] = useState(false);
  const containerRef = useRef(null);

  const words = text.split(' ');

  const replay = () => {
    if (isAnimating) return;
    
    setIsAnimating(true);
    const wordElements = containerRef.current?.querySelectorAll('.reveal-word span');
    
    wordElements?.forEach((word) => {
      word.style.animation = 'none';
      word.offsetHeight; // trigger reflow
      word.style.animation = '';
    });

    // Reset animation state after animation completes
    const totalDuration = (words.length * delay + duration) * 1000;
    setTimeout(() => {
      setIsAnimating(false);
      onAnimationComplete?.();
    }, totalDuration);
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      replay();
    }
  };

  return (
    <div className={`text-reveal-container ${className}`}>
      <div 
        ref={containerRef}
        className="text-reveal" 
        style={{
          '--reveal-duration': `${duration}s`,
          '--reveal-delay': `${delay}s`,
          '--reveal-distance': `${distance}px`
        }}
      >
        {words.map((word, index) => (
          <span key={`${word}-${index}`} className="reveal-word">
            <span style={{ animationDelay: `${index * delay}s` }}>
              {word}
            </span>
          </span>
        ))}
      </div>
      <button 
        className="replay-button"
        onClick={replay}
        onKeyDown={handleKeyDown}
        disabled={isAnimating}
        aria-label="Replay animation"
      >
        {isAnimating ? 'Playing...' : 'Replay'}
      </button>
    </div>
  );
};

// Usage example component
const TextRevealSlideDemo = () => {
  const [settings, setSettings] = useState({
    duration: 0.8,
    delay: 0.2,
    distance: 60,
    text: "Beautiful text animations made simple"
  });

  const handleSettingChange = (key, value) => {
    setSettings(prev => ({
      ...prev,
      [key]: value
    }));
  };

  return (
    <div className="text-reveal-demo">
      <div className="demo-stage">
        <TextRevealSlide 
          text={settings.text}
          duration={settings.duration}
          delay={settings.delay}
          distance={settings.distance}
          onAnimationComplete={() => console.log('Animation completed!')}
        />
      </div>
      
      <div className="controls">
        <div className="control-group">
          <label htmlFor="duration-slider">
            Animation Duration: {settings.duration.toFixed(1)}s
          </label>
          <input
            id="duration-slider"
            type="range"
            min="0.3"
            max="1.5"
            step="0.1"
            value={settings.duration}
            onChange={(e) => handleSettingChange('duration', parseFloat(e.target.value))}
          />
        </div>
        
        <div className="control-group">
          <label htmlFor="delay-slider">
            Word Delay: {settings.delay.toFixed(2)}s
          </label>
          <input
            id="delay-slider"
            type="range"
            min="0.1"
            max="0.5"
            step="0.05"
            value={settings.delay}
            onChange={(e) => handleSettingChange('delay', parseFloat(e.target.value))}
          />
        </div>
        
        <div className="control-group">
          <label htmlFor="distance-slider">
            Slide Distance: {settings.distance}px
          </label>
          <input
            id="distance-slider"
            type="range"
            min="20"
            max="100"
            step="10"
            value={settings.distance}
            onChange={(e) => handleSettingChange('distance', parseInt(e.target.value))}
          />
        </div>
        
        <div className="control-group">
          <label htmlFor="text-input">
            Text Content:
          </label>
          <input
            id="text-input"
            type="text"
            value={settings.text}
            onChange={(e) => handleSettingChange('text', e.target.value)}
            placeholder="Enter your text here"
          />
        </div>
      </div>
    </div>
  );
};

export default TextRevealSlide;
export { TextRevealSlideDemo };
/* M-005 Text Reveal Slide Styles */

.text-reveal-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
}

.text-reveal {
  font-size: clamp(28px, 5vw, 48px);
  font-weight: 700;
  color: #ffffff;
  line-height: 1.3;
  text-align: center;
  max-width: 600px;
  
  /* CSS Custom Properties for customization */
  --reveal-duration: 0.8s;
  --reveal-delay: 0.2s;
  --reveal-distance: 60px;
}

.reveal-word {
  display: inline-block;
  overflow: hidden;
  margin: 0 0.1em;
  vertical-align: bottom;
}

.reveal-word span {
  display: inline-block;
  transform: translateY(var(--reveal-distance));
  opacity: 0;
  animation: slideReveal var(--reveal-duration) cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
  text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}

@keyframes slideReveal {
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* Demo Stage Styles */
.text-reveal-demo {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  padding: 2rem;
}

.demo-stage {
  border-radius: 18px;
  padding: 48px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border: 1px solid rgba(255, 255, 255, 0.1);
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  margin-bottom: 2rem;
}

.replay-button {
  padding: 0.75rem 1.5rem;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 1rem;
}

.replay-button:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.replay-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.replay-button:focus {
  outline: 2px solid #667eea;
  outline-offset: 2px;
}

/* Controls Styles */
.controls {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1.5rem;
  padding: 1.5rem;
  background: #f8fafc;
  border-radius: 12px;
  border: 1px solid #e2e8f0;
}

.control-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.control-group label {
  font-weight: 600;
  color: #374151;
  font-size: 0.875rem;
}

.control-group input[type="range"] {
  width: 100%;
  height: 6px;
  background: #e2e8f0;
  border-radius: 3px;
  outline: none;
  cursor: pointer;
}

.control-group input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 18px;
  height: 18px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.control-group input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.control-group input[type="text"] {
  padding: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  font-size: 1rem;
  transition: border-color 0.2s ease;
}

.control-group input[type="text"]:focus {
  outline: none;
  border-color: #667eea;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

/* Dark theme support */
@media (prefers-color-scheme: dark) {
  .controls {
    background: #1f2937;
    border-color: #374151;
  }
  
  .control-group label {
    color: #f3f4f6;
  }
  
  .control-group input[type="text"] {
    background: #374151;
    border-color: #4b5563;
    color: #f3f4f6;
  }
  
  .control-group input[type="text"]:focus {
    border-color: #667eea;
  }
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .text-reveal-demo {
    padding: 1rem;
  }
  
  .demo-stage {
    padding: 24px;
    min-height: 150px;
  }
  
  .controls {
    grid-template-columns: 1fr;
    gap: 1rem;
  }
}

/* High contrast mode support */
@media (prefers-contrast: high) {
  .demo-stage {
    border: 2px solid #000;
  }
  
  .replay-button {
    border: 2px solid #000;
  }
  
  .control-group input[type="range"]::-webkit-slider-thumb {
    border: 3px solid #000;
  }
}

仕組みとカスタマイズHow It Works & Customization

仕組みHow it works

Each text element sits inside an overflow: hidden container. The text starts translated below (translateY: 100%) and transitions to translateY: 0 when triggered. Staggered animation-delay values create a cascading line-by-line reveal effect for multi-line text.

各テキスト要素はoverflow:hiddenコンテナ内に配置。テキストは下方向(translateY:100%)から開始し、トリガー時にtranslateY:0へトランジション。複数行では段階的なanimation-delayで行ごとのカスケードリビールを実現。

カスタマイズ方法Customization

Vary the animation-delay per line to control the cascade speed. Try translateX for a horizontal slide instead. Combine with opacity for a fade+slide hybrid. Use cubic-bezier for a bouncy or editorial-style curve.

行ごとのanimation-delayでカスケード速度を調整。translateXで水平スライドに変更も可能。opacityと組み合わせてフェード+スライドのハイブリッドに。cubic-bezierでバウンシーまたはエディトリアルスタイルのカーブを使用。

注意点Caveats

Screen readers read the text regardless of its visual position, so hidden state is purely visual. Apply prefers-reduced-motion to skip the animation and show text immediately at its final position.

スクリーンリーダーは視覚的な位置に関係なくテキストを読み上げるため、非表示状態は純粋に視覚的なものです。prefers-reduced-motionでアニメーションをスキップし、テキストを最終位置で即座に表示してください。

よくある質問 Frequently Asked Questions

How to customize the text reveal slide? Text Reveal Slideをカスタマイズするには?

Modify the CSS custom properties and class styles defined in the code section. Key adjustable values include colors, sizes, durations, and spacing. See the Adjustable Parameters section for specific variables.

コードセクションで定義されているCSSカスタムプロパティとクラススタイルを変更してください。色、サイズ、時間、間隔が主な調整可能値です。具体的な変数は調整可能パラメータセクションを参照してください。

How to use the text reveal slide in React? ReactでText Reveal Slideを使うには?

Import the provided React component and its CSS file. The component accepts props for customization. Check the React code section for the full implementation and available props.

提供されているReactコンポーネントとCSSファイルをインポートしてください。コンポーネントのpropsでカスタマイズできます。完全な実装と利用可能なpropsはReactコードセクションを参照してください。

What are the performance implications of text reveal slide? Text Reveal Slideのパフォーマンスへの影響は?

This implementation uses CSS transforms and opacity for animations, which are GPU-accelerated. It's lightweight and doesn't cause layout thrashing. Consider using prefers-reduced-motion for accessibility.

この実装はCSSのtransformとopacityを使用しており、GPUアクセラレーションされます。軽量でレイアウトスラッシングを引き起こしません。アクセシビリティのためにprefers-reduced-motionの使用を検討してください。

AIへの指示テンプレート AI Prompt Template

以下をAIに貼り付けると実装を依頼できます。 Paste the following into your AI assistant to request implementation.