S-010 Loading medium

How to create a ai generating text loader AIテキストローダーの作り方

Each letter glows and fades in one by one as a chromatic scan sweeps behind — perfect for AI generation states. 文字が1つずつグロー発光しながらフェードインし、クロマティックな光のスキャンが背後を走るAI生成風ローダー。

ライブデモ Live Demo

G e n e r a t i n g · · ·
4s
4px
2.8em

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

何ができるかWhat it does

Each letter glows and fades in one by one as a chromatic scan sweeps behind — perfect for AI generation states.

文字が1つずつグロー発光しながらフェードインし、クロマティックな光のスキャンが背後を走るAI生成風ローダー。

どこで使うかWhere to use

loading screen, async UI state, page transition, data fetching indicator

AIチャットインターフェース、コンテンツ生成UI、ローディング画面、インタラクティブなデモ

特徴Key features

Letter-by-letter fade-in with a chromatic scan sweep behind the text, creating an AI-generation visual effect. CSS-only animation with staggered animation-delay per letter. Supports prefers-reduced-motion. Multiple color preset themes.

文字ごとのフェードインとテキスト背後のクロマティックスキャンスウィープでAI生成の視覚効果を演出。文字ごとにstaggered animation-delayを使用したCSSオンリーアニメーション。prefers-reduced-motionをサポート。複数カラープリセットテーマ。

調整可能パラメータ Adjustable Parameters

Parameter Default Description
--gen-durationfull cycle duration for letter fade and scan opacity (default: 4s)
--gen-glowblur radius for the letter glow (default: 4px); larger = stronger bloom
--scan-c1 〜 --scan-c5five radial gradient colors forming the scan blob. Presets: Chromatic / AI Blue / Amber
animation-delay (per letter)per-letter stagger (~0.105 s); add nth-child entries proportionally for different word lengths
.gen-dot delaystrailing dot delays (last letter + ~0.18 s steps); adjust dot count to change the ellipsis length
prefers-reduced-motiondisables all animations and shows letters and dots at full opacity

実装コード Implementation Code

// react/S-010.jsx
import "./S-010.css";

const STEP_MS = 105;
const DOTS = 3;
const DOT_STEP_MS = 180;

export default function GeneratingLoader({
  text = "Generating",
  duration = 4,
  glow = 4,
  fontSize = "2.8em",
  scanPreset = "chromatic", // "chromatic" | "ai-blue" | "amber"
}) {
  const letters = text.split("");
  const lastDelay = 0.1 + (letters.length - 1) * STEP_MS / 1000;

  return (
    <div
      className={`gen-loader-wrapper gen-loader-wrapper--${scanPreset}`}
      style={{
        "--gen-duration": `${duration}s`,
        "--gen-glow": `${glow}px`,
        fontSize,
      }}
    >
      {letters.map((char, i) => (
        <span
          key={i}
          className="gen-loader-letter"
          style={{ animationDelay: `${(0.1 + (i * STEP_MS) / 1000).toFixed(3)}s` }}
        >
          {char}
        </span>
      ))}
      {Array.from({ length: DOTS }, (_, i) => (
        <span
          key={`dot-${i}`}
          className="gen-loader-dot"
          style={{ animationDelay: `${(lastDelay + STEP_MS / 1000 + (i * DOT_STEP_MS) / 1000).toFixed(3)}s` }}
        >
          &middot;
        </span>
      ))}
      <div className="gen-loader-scan" />
    </div>
  );
}
:root {
  --gen-duration: 4s;
  --gen-glow: 4px;
  --scan-c1: #ff0;  --scan-c2: #f00;  --scan-c3: #0ff;
  --scan-c4: #0f0;  --scan-c5: #00f;
}

.gen-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 120px;
  padding: 0 16px;
  font-size: 2.8em;
  font-weight: 700;
  color: #fff;
  user-select: none;
}

/* scanline overlay */
.gen-scan {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-color: transparent;
  mask: repeating-linear-gradient(
    90deg,
    transparent 0, transparent 6px,
    black 7px, black 8px
  );
}

/* chromatic sweep */
.gen-scan::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle at 50% 50%, var(--scan-c1) 0%, transparent 50%),
    radial-gradient(circle at 45% 45%, var(--scan-c2) 0%, transparent 45%),
    radial-gradient(circle at 55% 55%, var(--scan-c3) 0%, transparent 45%),
    radial-gradient(circle at 45% 55%, var(--scan-c4) 0%, transparent 45%),
    radial-gradient(circle at 55% 45%, var(--scan-c5) 0%, transparent 45%);
  mask: radial-gradient(circle at 50% 50%,
    transparent 0%, transparent 10%, black 25%);
  /* 移動と不透明度を統合。opacity=0 の間に右端→左端へ瞬間ジャンプし
     常に左→右の一方向スイープに見せる。--gen-duration に連動する。 */
  animation: genScanCombined var(--gen-duration) linear infinite;
}

.gen-letter {
  display: inline-block;
  opacity: 0;
  animation: genLetterAnim var(--gen-duration) infinite linear;
  position: relative;
  z-index: 2;
}

/* trailing dots — original element */
.gen-dot {
  display: inline-block;
  opacity: 0;
  animation: genLetterAnim var(--gen-duration) infinite linear;
  position: relative;
  z-index: 2;
  color: rgba(255,255,255,0.55);
  font-size: 0.55em;
  align-self: flex-end;
  margin-bottom: 0.18em;
  letter-spacing: 0.15em;
}

/* letter delays */
.gen-letter:nth-child(1)  { animation-delay: 0.100s; }
.gen-letter:nth-child(2)  { animation-delay: 0.205s; }
.gen-letter:nth-child(3)  { animation-delay: 0.310s; }
.gen-letter:nth-child(4)  { animation-delay: 0.415s; }
.gen-letter:nth-child(5)  { animation-delay: 0.521s; }
.gen-letter:nth-child(6)  { animation-delay: 0.626s; }
.gen-letter:nth-child(7)  { animation-delay: 0.731s; }
.gen-letter:nth-child(8)  { animation-delay: 0.837s; }
.gen-letter:nth-child(9)  { animation-delay: 0.942s; }
.gen-letter:nth-child(10) { animation-delay: 1.047s; }
/* dot delays */
.gen-dot:nth-child(11)    { animation-delay: 1.200s; }
.gen-dot:nth-child(12)    { animation-delay: 1.380s; }
.gen-dot:nth-child(13)    { animation-delay: 1.560s; }

@keyframes genScanCombined {
  0%   { transform: translate(-70%); opacity: 0; }
  13%  { transform: translate(-70%); opacity: 0; }
  18%  { opacity: 1; }
  62%  { transform: translate(70%);  opacity: 0.8; }
  68%  { opacity: 0; }
  100% { transform: translate(70%);  opacity: 0; }
}
@keyframes genLetterAnim {
  0%   { opacity: 0; }
  5%   { opacity: 1;
         text-shadow: 0 0 var(--gen-glow, 4px) #fff;
         transform: scale(1.1) translateY(-2px); }
  20%  { opacity: 0.2; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .gen-scan::after      { animation: none; opacity: 0; }
  .gen-letter, .gen-dot { animation: none; opacity: 1; }
}
import "./S-010.css";

const STEP_MS = 105;
const DOTS = 3;
const DOT_STEP_MS = 180;

export default function GeneratingLoader({
  text = "Generating",
  duration = 4,
  glow = 4,
  fontSize = "2.8em",
  scanPreset = "chromatic", // "chromatic" | "ai-blue" | "amber"
}) {
  const letters = text.split("");
  const lastDelay = 0.1 + (letters.length - 1) * STEP_MS / 1000;

  return (
    <div
      className={`gen-loader-wrapper gen-loader-wrapper--${scanPreset}`}
      style={{
        "--gen-duration": `${duration}s`,
        "--gen-glow": `${glow}px`,
        fontSize,
      }}
    >
      {letters.map((char, i) => (
        <span
          key={i}
          className="gen-loader-letter"
          style={{ animationDelay: `${(0.1 + (i * STEP_MS) / 1000).toFixed(3)}s` }}
        >
          {char}
        </span>
      ))}
      {Array.from({ length: DOTS }, (_, i) => (
        <span
          key={`dot-${i}`}
          className="gen-loader-dot"
          style={{
            animationDelay: `${(lastDelay + STEP_MS / 1000 + (i * DOT_STEP_MS) / 1000).toFixed(3)}s`,
          }}
        >
          ·
        </span>
      ))}
      <div className="gen-loader-scan" />
    </div>
  );
}
/* scan color palettes via CSS variables */
.gen-loader-wrapper {
  --scan-c1: #ff0;
  --scan-c2: #f00;
  --scan-c3: #0ff;
  --scan-c4: #0f0;
  --scan-c5: #00f;
}

.gen-loader-wrapper--ai-blue {
  --scan-c1: #00e5ff;
  --scan-c2: #0066ff;
  --scan-c3: #00b4d8;
  --scan-c4: #0040ff;
  --scan-c5: #48cae4;
}

.gen-loader-wrapper--amber {
  --scan-c1: #ffd60a;
  --scan-c2: #ff8c00;
  --scan-c3: #ffc107;
  --scan-c4: #ff6d00;
  --scan-c5: #ffb300;
}

.gen-loader-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 120px;
  padding: 0 16px;
  font-family: "Inter", sans-serif;
  font-weight: 700;
  letter-spacing: 0.03em;
  user-select: none;
  color: #fff;
}

/* scanline mask overlay */
.gen-loader-scan {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-color: transparent;
  mask: repeating-linear-gradient(
    90deg,
    transparent 0,
    transparent 6px,
    black 7px,
    black 8px
  );
}

/* chromatic sweep blob */
.gen-loader-scan::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle at 50% 50%, var(--scan-c1) 0%, transparent 50%),
    radial-gradient(circle at 45% 45%, var(--scan-c2) 0%, transparent 45%),
    radial-gradient(circle at 55% 55%, var(--scan-c3) 0%, transparent 45%),
    radial-gradient(circle at 45% 55%, var(--scan-c4) 0%, transparent 45%),
    radial-gradient(circle at 55% 45%, var(--scan-c5) 0%, transparent 45%);
  mask: radial-gradient(
    circle at 50% 50%,
    transparent 0%,
    transparent 10%,
    black 25%
  );
  /* 移動と不透明度を統合し --gen-duration に連動。
     opacity=0 の間に右端→左端へ瞬間ジャンプするため常に左→右の一方向スイープ。 */
  animation: genLoaderScanCombined var(--gen-duration, 4s) linear infinite;
}

.gen-loader-letter {
  display: inline-block;
  opacity: 0;
  animation: genLoaderLetter var(--gen-duration, 4s) infinite linear;
  position: relative;
  z-index: 2;
}

/* trailing dots — original element */
.gen-loader-dot {
  display: inline-block;
  opacity: 0;
  animation: genLoaderLetter var(--gen-duration, 4s) infinite linear;
  position: relative;
  z-index: 2;
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.55em;
  align-self: flex-end;
  margin-bottom: 0.18em;
  letter-spacing: 0.15em;
}

@keyframes genLoaderScanCombined {
  0%   { transform: translate(-70%); opacity: 0; }
  13%  { transform: translate(-70%); opacity: 0; }
  18%  { opacity: 1; }
  62%  { transform: translate(70%);  opacity: 0.8; }
  68%  { opacity: 0; }
  100% { transform: translate(70%);  opacity: 0; }
}

@keyframes genLoaderLetter {
  0%   { opacity: 0; }
  5%   { opacity: 1;
         text-shadow: 0 0 var(--gen-glow, 4px) #fff;
         transform: scale(1.1) translateY(-2px); }
  20%  { opacity: 0.2; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .gen-loader-scan::after                    { animation: none; opacity: 0; }
  .gen-loader-letter, .gen-loader-dot        { animation: none; opacity: 1; }
}

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

仕組みHow it works

Each letter is wrapped in a <span> with an incrementally increasing animation-delay. A @keyframes animation transitions opacity from 0 to 1 and adds a text-shadow glow. A separate background element with a radial gradient and @keyframes animates its position across the text to simulate the "scan" sweep. All delays are coordinated for a synchronized reveal.

各文字は段階的に増加するanimation-delayを持つ<span>でラップされます。@keyframesアニメーションがopacityを0から1にトランジションしてtext-shadowグロウを追加。ラジアルグラデーションを持つ別の背景要素が@keyframesでテキスト全体にわたって位置をアニメーションして"スキャン"スウィープをシミュレート。全ての遅延が同期されたリビールのために調整されます。

カスタマイズ方法Customization

Switch between Chromatic, AI Blue, and Amber presets by updating --scan-c1 through --scan-c5. Change the stagger interval (default ~0.1s per letter) to control reading pace. Add trailing dots animation for a "typing…" effect at the end.

--scan-c1から--scan-c5を更新してChromatic・AI Blue・Amberプリセットを切り替え。スタッガー間隔(デフォルト〜0.1秒/文字)を変更して読み取りペースを制御。末尾に"typing..."エフェクトのためにトレイリングドットアニメーションを追加。

注意点Caveats

Character-by-character text updates should use aria-live="polite" only after the full text is revealed, not for each character, to avoid spamming screen reader announcements. Disable all animations under prefers-reduced-motion and show the complete text immediately.

スクリーンリーダーのアナウンスをスパムしないよう、文字ごとのテキスト更新には各文字ではなく完全なテキストが明らかになった後にのみaria-live="polite"を使用してください。prefers-reduced-motion下では全てのアニメーションを無効化して完全なテキストを即座に表示してください。

よくある質問 Frequently Asked Questions

How to customize the ai generating text loader? AI Generating Text Loaderをカスタマイズするには?

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 ai generating text loader in React? ReactでAI Generating Text Loaderを使うには?

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 ai generating text loader? AI Generating Text Loaderのパフォーマンスへの影響は?

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.