GODRICH

CSS タイピング アニメーション 9選 — 一行でコピペOK

css タイピング アニメーションは、文字が一文字ずつ入力されていくように見せる、CSSだけで動く小さな部品です。この記事では9種類を選び、steps()とwidth、clip-pathという仕組みだけで実装しました。グリッドの9つはiframeの中で最初から動いていて、クリックでは止められません。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並び順は効果の派手さではなく、文字を区切って見せる仕組みの難易度です。まずopacityだけでカーソルを点滅させる最も単純な形から始め(01)、widthとsteps()で文字を1文字ずつ伸ばす核となる書き方に進み(02)、その仕組みが等幅フォント前提で壊れる場面をJSのピクセル計測で補い(03)、複数行への拡張(04)と、1つのkeyframesで入力と削除をまとめる書き方(05)を重ねます。06と07はJSでリズムや段階を足す発展形、08はwidthではなくSVGのstroke-dashoffsetという別の仕組み、09は実際の場面への転用として、文の一部だけを入れ替える書き方で締めくくります。

01カーソル点滅

入力が終わった位置に文字幅のカーソルバーを置き、opacityをsteps(2, jump-none)で切り替えて本物のカーソルのように点滅させます。フォームのプレースホルダー強調や、チャット入力欄の待機状態に向いています。

step-endopacity 깜빡임::after 커서
.line__caret {
  display: inline-block;
  width: clamp(8px, 2.6vw, 14px);
  height: 1.3em;
  margin-left: clamp(2px, 1vw, 6px);
  background: $color;
  animation: caret-blink .3s steps(2, jump-none) infinite;
}

@keyframes caret-blink {
  from { opacity: 1; }
  to   { opacity: 0; }
}

02steps()でタイプ

widthを0から文字数(ch)まで、steps()を使って1文字ずつ区切って伸ばし、タイプライターが打つように見せます。ヒーローの見出しやLPのキャッチコピーに向いています。

steps(n)ch 단위white-space: nowrap
.type__text {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .12em solid $color;
  animation: type-loop 2s linear infinite;
}

@keyframes type-loop {
  0%, 6%  { width: 0; animation-timing-function: steps(10, end); }
  58%     { width: var(--full, 10ch); }
  88%     { width: var(--full, 10ch); }
  94%, 100% { width: 0; }
}

03多言語グリフ合成タイピング

steps()+chは等幅フォント前提のため、ハングルの合成字や絵文字が混じると文字が途中で切れます。JSで各文字の実際のピクセル幅を測り、その境界までclip-pathで区切ることで、日英絵文字が混在するサービス名でも崩れずに表示します。MDNのElement.animate()対応表によると、この仕組みが使うWeb Animations APIはChrome・Firefox・Safari・Edgeの最新版で動きます。

clip-path 스텝getBoundingClientRect가변폭 대응
var el = document.getElementById('gtext'), s = el.textContent;
var cv = document.createElement('canvas').getContext('2d');
cv.font = getComputedStyle(el).font;
var w = 0, cum = [0];
for (var i = 0; i < s.length; i++) {
  w += cv.measureText(s[i]).width;
  cum.push(w);
}
var kf = cum.map(function (px) {
  return { clipPath: 'inset(0 ' + (100 - px / w * 70) + '% 0 0)', offset: px / w * .7 };
});
el.animate(kf, { duration: 2000, iterations: Infinity });

04複数行タイピング

各行に独立したsteps()アニメーションを持たせ、それまでの合計時間だけanimation-delayを与えることで、上の行が打ち終わってから次の行が続けて打たれます。コードエディターのイントロや、複数行の自己紹介カードに向いています。

줄마다 steps()퍼센트 구간 분리overflow: hidden
.lines__text {
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .1em solid $color;
  animation-duration: 2s;
  animation-iteration-count: infinite;
}

@keyframes l2-type {
  0%, 10% { width: 0; animation-timing-function: steps(16, end); }
  34%     { width: var(--full, 16ch); }
  88%     { width: var(--full, 16ch); }
  96%, 100% { width: 0; }
}

05消してから再入力

widthが0→100%→0を行き来する1つのkeyframesに入力と削除をまとめ、一文を書いて少し止まり、また消す動作をJSなしで繰り返します。空のチャットの案内文や、検索窓のプレースホルダーアニメーションに向いています。

단일 keyframes0→100%→0hold 구간
.search__text {
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .1em solid $color;
  animation: search-loop 2s linear infinite;
}

@keyframes search-loop {
  0%, 6%   { width: 0; animation-timing-function: steps(11, end); }
  46%      { width: var(--full, 11ch); }
  62%      { width: var(--full, 11ch); }
  96%, 100% { width: 0; }
}

06AI回答のストリーミング風

一定間隔のsteps()の代わりに30〜90msのランダムな間隔で文字を追加するJSループを使い、AIの回答がトークン単位で不規則に流れる感じを再現します。AIチャットボットの回答欄や、プロンプト結果のローディング演出に向いています。

랜덤 간격 JSsetTimeout 루프오브 커서 pulse
var el = document.getElementById('atext'), s = el.textContent;
var n = s.length, d = [], sum = 0;
for (var i = 0; i < n; i++) {
  var r = 30 + Math.random() * 60;
  d.push(r);
  sum += r;
}
var acc = 0, kf = [{ clipPath: 'inset(0 100% 0 0)', offset: 0 }];
for (var i = 0; i < n; i++) {
  acc += d[i];
  kf.push({ clipPath: 'inset(0 ' + (100 - acc / sum * 78) + '% 0 0)', offset: acc / sum * .78 });
}
el.animate(kf, { duration: 2000, iterations: Infinity });

07ターミナルプロンプト風

$プロンプトの後ろでコマンドがsteps()で打たれ、そのアニメーションの時間だけ遅れてレスポンス行がopacityでフェードインする、2段階のシーケンスで疑似ターミナルを演出します。開発ツールの紹介セクションや、CLI製品のヒーローデモに向いています。

명령어→응답 순서monospace 터미널 폰트트래픽라이트 장식
.term__cmd {
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .1em solid $color;
  animation: cmd-type 2s linear infinite;
}
.term__resp {
  color: $color;
  animation: resp-fade 2s ease-out infinite;
}

@keyframes resp-fade {
  0%, 44% { opacity: 0; }
  50%     { opacity: 1; }
  82%     { opacity: 1; }
  90%, 100% { opacity: 0; }
}

08SVG手書き効果

SVG pathのstroke-dasharrayを線の全長にし、stroke-dashoffsetをその長さから0へトランジションすることで、手書き文字が線をなぞるように描かれて見えます。サイン風演出や、手書き風のイベント招待状に向いています。

stroke-dasharraystroke-dashoffsetpath 길이 측정
.sig__svg path {
  stroke: $color;
  stroke-width: 5;
  stroke-linecap: round;
  stroke-dasharray: 520;
  stroke-dashoffset: 520;
  animation: draw 2s ease-out infinite;
}

@keyframes draw {
  0%, 6%    { stroke-dashoffset: 520; }
  55%       { stroke-dashoffset: 0; }
  80%       { stroke-dashoffset: 0; }
  96%, 100% { stroke-dashoffset: 520; }
}

09単語入れ替えタイピング

同じ位置に重ねた3つの単語がwidth 0で待機し、自分の番の区間だけsteps()でタイピング・削除されることで、文の残りはそのままに1単語だけが変わって見えます。役職や名詞が入れ替わるヒーローコピーや、複数機能の紹介スローガンに向いています。

구간별 steps()단어 3개 겹쳐두기고정 문장 + 가변 단어
.swap__word--1 { animation: w1 2s linear infinite; }
.swap__word--2 {
  position: absolute;
  left: 0;
  top: 0;
  animation: w2 2s linear infinite;
}

@keyframes w2 {
  0%, 40%   { width: 0; animation-timing-function: steps(3, end); }
  46%       { width: var(--full, 4ch); }
  68%       { width: var(--full, 4ch); }
  74%, 100% { width: 0; }
}

どこで崩れるか — 落とし穴

9つともwidthやopacity、stroke-dashoffsetといった数値をkeyframesやJSのアニメーションで少しずつ動かしていて、文字そのものを直接書き換えるコードは使っていません。1つ目の落とし穴は02を組み立てているときに踏んだもので、widthを0からmax-contentへトランジションすると、steps()を付けても文字が一段でジャンプしてしまい、1文字ずつ伸びているようには見えませんでした。JSのcanvasで実際の描画幅をピクセル単位で測り、--fullというカスタムプロパティにpx値で入れてからkeyframesのwidthに使ったことで、steps()どおりに1文字ずつ止まりながら伸びる動きになりました。

2つ目は04の複数行を組み立てたときのことです。3行それぞれのkeyframeを0%から並べただけで試すと、上から順ではなく3行が同時に打たれて見えました。1行目が終わる10%を2行目のsteps()の開始点に、2行目が終わる34%を3行目の開始点にそろえたことで直りました。この修正込みの9ファイルを開くにはerv8c6ebという文字列が必要です。下のカードからzipを取ってください。

3つ目は09の単語入れ替えを作ったときのことです。3つの単語を普通にposition: staticのまま並べて試すと、単語が入れ替わるたびに幅が変わって後ろの文の位置が横にがたつきました。2番目と3番目の単語だけposition: absoluteで同じ場所に重ね、1番目の単語だけが実際の文の幅を持つようにしたことで、入れ替わっても文全体の位置は動かなくなりました。

アクセシビリティ

9つとも prefers-reduced-motion: reduce で入力や削除の動きが止まりますが、要素そのものは消えません。カーソル点滅(01)はopacity 1のまま静止し、steps()タイプ(02)と消してから再入力(05)、複数行(04)、ターミナルのコマンド行(07)はカーソルが消えて文字列全体がそのまま表示されます。多言語合成(03)とAIストリーミング(06)はJS側でmatchMediaを先に見てアニメーション自体を起動せず、文字は最初から全部表示されます。SVG手書き(08)は線が最初から描き終わった状態で止まり、単語入れ替え(09)は1番目の単語だけが残り、2番目と3番目は幅0のまま隠れます。

@media (prefers-reduced-motion: reduce) {
  .line__caret, .type__text, .lines__text,
  .search__text, .term__cmd, .term__resp,
  .sig__svg path, .swap__word { animation: none !important; }
  .type__text, .search__text,
  .term__cmd { border-right-color: transparent; }
}

タイピング以外のCSSアニメーションはこちらのカテゴリー一覧に、このサイトの成り立ちは紹介ページにまとめています。

FAQ

9つを同じページに複数置いても動きは重なりませんか?

重なりません。それぞれのアニメーションは自分自身のクラス名やidにしか結び付いていないため、01のカーソルと09の単語入れ替えを隣に並べても、片方のanimationがもう片方の要素に影響することはありません。04の複数行のようにsteps()を使う項目でも、animation-timing-functionと--fullは要素ごとに独立しているので、同時設置に問題はありません。

Reactでは何を使ってタイピングを再現しますか?

zipのreact/フォルダの各コンポーネントは、これまでと同じクラス名をspan要素に付けているだけで、タイプする速さと区切りはCSSのanimation@keyframesが担います。03と06だけはuseEffectでcanvasやWeb Animations APIを呼び、文字の実際の幅やランダムな間隔を計算してからelement.animate()に渡します。

日本語やハングルが混じっても文字は崩れませんか?

02や04、05のようにsteps()とch単位だけで組む場合は、等幅フォントを前提にしているため合成字や絵文字が混じると途中で切れることがあります。03のようにJSで実際のピクセル幅を測ってclip-pathで区切る書き方に変えると、日本語・ハングル・絵文字が混ざった文章でも文字の途中で切れずに表示できます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。