タイプライター効果

ターミナル風UIで複数のフレーズを1文字ずつ打って消すループ。点滅カーソル付きで、ヒーローの動的キャッチコピーに使えます。

#js#css#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<div class="page">
  <header class="nav">
    <div class="brand"><span class="logo"></span>FlowDesk</div>
    <span class="badge">v2.4 リリース</span>
  </header>

  <section class="hero">
    <p class="eyebrow">DEVELOPER FIRST</p>
    <h1 class="head">FlowDeskで、<br><span class="type" aria-live="polite"></span><span class="caret"></span></h1>
    <p class="lead">コマンド一発で連携。あなたのワークフローに溶け込むSaaS。</p>

    <div class="terminal">
      <div class="bar"><i></i><i></i><i></i><span>flowdesk — bash</span></div>
      <pre class="body"><span class="prompt">$</span> flowdesk init my-team
<span class="ok">✓</span> ワークスペースを作成しました
<span class="prompt">$</span> flowdesk deploy
<span class="ok">✓</span> 公開URLを発行: https://my-team.flowdesk.app</pre>
    </div>
  </section>
</div>
CSS
/* FlowDesk:タイプライターのキャッチコピーが主役 */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(700px 360px at 18% -10%, #18294f 0%, transparent 60%),
    linear-gradient(160deg, #0f1b34 0%, #0b1326 100%);
  color: #e7ecf7;
  min-height: 400px;
  overflow: hidden;
}
.page { padding: 16px 26px; }

.nav { display: flex; align-items: center; justify-content: space-between; }
.brand { display: flex; align-items: center; gap: 9px; font-weight: 700; font-size: 16px; }
.logo {
  width: 17px; height: 17px; border-radius: 6px;
  background: linear-gradient(135deg, #4f7cff, #8ab4ff);
  box-shadow: 0 0 10px rgba(79,124,255,0.6);
}
.badge {
  font-size: 11px; color: #8ab4ff;
  border: 1px solid #2c3c63; padding: 4px 10px; border-radius: 20px;
}

.hero { padding: 24px 4px 0; }
.eyebrow { font-size: 11px; letter-spacing: 0.3em; color: #6f86c2; font-weight: 700; }
.head {
  margin-top: 12px;
  font-size: clamp(26px, 5.5vw, 40px);
  font-weight: 800; line-height: 1.25;
}
/* タイプされる本文(ブランド青で強調) */
.type { color: #6a90ff; }
.caret {
  display: inline-block; width: 3px; height: 1em;
  background: #6a90ff; margin-left: 2px;
  vertical-align: -0.12em;
  animation: blink 1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }

.lead { margin-top: 14px; font-size: 13.5px; color: #aeb9d4; }

/* ターミナル風カード */
.terminal {
  margin-top: 20px;
  background: #0a1124;
  border: 1px solid #20305a;
  border-radius: 11px;
  overflow: hidden;
  box-shadow: 0 14px 36px rgba(0,0,0,0.45);
}
.bar {
  display: flex; align-items: center; gap: 7px;
  padding: 9px 13px; background: #111c38;
  font-size: 11px; color: #7e8fb8;
}
.bar i { width: 10px; height: 10px; border-radius: 50%; background: #2b3c64; }
.bar i:nth-child(1) { background: #ff5f57; }
.bar i:nth-child(2) { background: #febc2e; }
.bar i:nth-child(3) { background: #28c840; }
.bar span { margin-left: 6px; }
.body {
  padding: 13px 16px;
  font-family: "Consolas", "SFMono-Regular", monospace;
  font-size: 12.5px; line-height: 1.8; color: #c3cee6;
  white-space: pre-wrap;
}
.prompt { color: #6a90ff; }
.ok { color: #28c840; }

@media (prefers-reduced-motion: reduce) {
  .caret { animation: none; }
}
JavaScript
// 複数フレーズを1文字ずつ打って消すループ
(function () {
  const el = document.querySelector('.type');
  if (!el) return; // null安全

  const phrases = [
    'チームはひとつに。',
    'デプロイは一瞬。',
    '面倒は自動化。',
    '今日から始まる。'
  ];
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 控えめ設定では先頭フレーズを静的表示
  if (reduce) { el.textContent = phrases[0]; return; }

  let p = 0, i = 0, deleting = false;

  function tick() {
    const word = phrases[p];
    // 打つ/消すで文字数を増減
    i += deleting ? -1 : 1;
    el.textContent = word.slice(0, i);

    let wait = deleting ? 55 : 95;
    if (!deleting && i === word.length) {
      wait = 1300;            // 打ち終えたら少し待つ
      deleting = true;
    } else if (deleting && i === 0) {
      deleting = false;
      p = (p + 1) % phrases.length; // 次のフレーズへ
      wait = 320;
    }
    setTimeout(tick, wait);
  }
  tick();
})();

コード

HTML
<main class="stage">
  <div class="terminal">
    <div class="bar">
      <span class="dot red"></span>
      <span class="dot yellow"></span>
      <span class="dot green"></span>
      <span class="tname">~/portfolio</span>
    </div>
    <p class="line">
      <span class="prompt">$</span>
      <!-- ここにJSが1文字ずつ打ち込む -->
      <span class="typed" id="typed"></span><span class="caret" aria-hidden="true"></span>
    </p>
  </div>
</main>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(800px 500px at 50% 120%, #15324a 0%, transparent 60%),
    #0b0f17;
  padding: 24px;
  overflow: hidden;
}

/* ターミナル風ウィンドウ */
.terminal {
  width: min(560px, 92vw);
  background: #11161f;
  border: 1px solid #232b3a;
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.55);
  overflow: hidden;
}

.bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  background: #1a2130;
  border-bottom: 1px solid #232b3a;
}
.dot { width: 12px; height: 12px; border-radius: 50%; }
.red { background: #ff5f56; }
.yellow { background: #ffbd2e; }
.green { background: #27c93f; }
.tname {
  margin-left: 8px;
  font-size: 12px;
  color: #6b768c;
  font-family: "Consolas", monospace;
}

.line {
  padding: 26px 20px 30px;
  font-family: "Consolas", "Courier New", monospace;
  font-size: clamp(16px, 4.5vw, 24px);
  color: #e6edf3;
  display: flex;
  align-items: baseline;
  white-space: pre-wrap;
  word-break: break-word;
}
.prompt { color: #27c93f; margin-right: 10px; }
.typed { color: #7ee787; }

/* 点滅カーソル */
.caret {
  display: inline-block;
  width: 10px;
  height: 1.1em;
  margin-left: 2px;
  background: #7ee787;
  transform: translateY(0.12em);
  animation: blink 1s step-end infinite;
}
@keyframes blink {
  50% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .caret { animation: none; }
}
JavaScript
// タイプライター効果: 複数フレーズを打って消す
(function () {
  const out = document.getElementById('typed');
  if (!out) return; // null安全

  const phrases = [
    'npm run build',
    'deploy --prod',
    'git push origin main',
    'echo "Hello, Web"'
  ];

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let p = 0;       // 現在のフレーズindex
  let i = 0;       // 現在の文字位置
  let deleting = false;

  // setTimeoutを使い、速度を可変にして自然なタイピングに
  function tick() {
    const word = phrases[p];

    if (!deleting) {
      i++;
      out.textContent = word.slice(0, i);
      if (i === word.length) {
        deleting = true;
        return schedule(1100); // 打ち終えたら少し待つ
      }
      return schedule(70 + Math.random() * 60);
    } else {
      i--;
      out.textContent = word.slice(0, i);
      if (i === 0) {
        deleting = false;
        p = (p + 1) % phrases.length; // 次のフレーズへ
        return schedule(280);
      }
      return schedule(38);
    }
  }

  function schedule(ms) { window.setTimeout(tick, ms); }

  if (reduce) {
    // モーション控えめ: 1フレーズだけ静的表示
    out.textContent = phrases[0];
  } else {
    schedule(500);
  }
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「タイプライター効果」の効果を追加してください。

# 追加してほしい効果
タイプライター効果(タイポグラフィ)
ターミナル風UIで複数のフレーズを1文字ずつ打って消すループ。点滅カーソル付きで、ヒーローの動的キャッチコピーに使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<main class="stage">
  <div class="terminal">
    <div class="bar">
      <span class="dot red"></span>
      <span class="dot yellow"></span>
      <span class="dot green"></span>
      <span class="tname">~/portfolio</span>
    </div>
    <p class="line">
      <span class="prompt">$</span>
      <!-- ここにJSが1文字ずつ打ち込む -->
      <span class="typed" id="typed"></span><span class="caret" aria-hidden="true"></span>
    </p>
  </div>
</main>

【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(800px 500px at 50% 120%, #15324a 0%, transparent 60%),
    #0b0f17;
  padding: 24px;
  overflow: hidden;
}

/* ターミナル風ウィンドウ */
.terminal {
  width: min(560px, 92vw);
  background: #11161f;
  border: 1px solid #232b3a;
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.55);
  overflow: hidden;
}

.bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  background: #1a2130;
  border-bottom: 1px solid #232b3a;
}
.dot { width: 12px; height: 12px; border-radius: 50%; }
.red { background: #ff5f56; }
.yellow { background: #ffbd2e; }
.green { background: #27c93f; }
.tname {
  margin-left: 8px;
  font-size: 12px;
  color: #6b768c;
  font-family: "Consolas", monospace;
}

.line {
  padding: 26px 20px 30px;
  font-family: "Consolas", "Courier New", monospace;
  font-size: clamp(16px, 4.5vw, 24px);
  color: #e6edf3;
  display: flex;
  align-items: baseline;
  white-space: pre-wrap;
  word-break: break-word;
}
.prompt { color: #27c93f; margin-right: 10px; }
.typed { color: #7ee787; }

/* 点滅カーソル */
.caret {
  display: inline-block;
  width: 10px;
  height: 1.1em;
  margin-left: 2px;
  background: #7ee787;
  transform: translateY(0.12em);
  animation: blink 1s step-end infinite;
}
@keyframes blink {
  50% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .caret { animation: none; }
}

【JavaScript】
// タイプライター効果: 複数フレーズを打って消す
(function () {
  const out = document.getElementById('typed');
  if (!out) return; // null安全

  const phrases = [
    'npm run build',
    'deploy --prod',
    'git push origin main',
    'echo "Hello, Web"'
  ];

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let p = 0;       // 現在のフレーズindex
  let i = 0;       // 現在の文字位置
  let deleting = false;

  // setTimeoutを使い、速度を可変にして自然なタイピングに
  function tick() {
    const word = phrases[p];

    if (!deleting) {
      i++;
      out.textContent = word.slice(0, i);
      if (i === word.length) {
        deleting = true;
        return schedule(1100); // 打ち終えたら少し待つ
      }
      return schedule(70 + Math.random() * 60);
    } else {
      i--;
      out.textContent = word.slice(0, i);
      if (i === 0) {
        deleting = false;
        p = (p + 1) % phrases.length; // 次のフレーズへ
        return schedule(280);
      }
      return schedule(38);
    }
  }

  function schedule(ms) { window.setTimeout(tick, ms); }

  if (reduce) {
    // モーション控えめ: 1フレーズだけ静的表示
    out.textContent = phrases[0];
  } else {
    schedule(500);
  }
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。