CSSでつくるタイプライター効果
タイプライター
切れます。この文は14けた要りますが、いまの書き方だと7けたぶんしか幅がありません(7けた足りない)。「けた数 × 1ch」を選ぶと直ります。
✓ 打ち終わるまで1.26秒です。5秒以内なので、止める手段を別に置かなくても決まりの中に収まります。
カーソルは1秒に1回、2回だけ点滅して、打ち終わると止まります(1秒あたり1回。決まりの上限は1秒に3回です)。箱の右の線なので、打っている場所を追いかけます。
✓ 入っています。書き出す CSS には、@media (prefers-reduced-motion: reduce) の段落が必ず付きます。つねに入ったままです。
この端末の設定は、画面がついた直後に読み取ります。
同じ書き方で、日本語だけ切れます
下の枠は、どれも定番のレシピどおりに「文字数 × 1ch」の幅を与えた箱です。できあがりの姿だけを出しています。 枠からはみ出したところは隠れます。
width: 14ch と書かれます本当の文はCSS typewriterです。14文字で14けた。定番のレシピが書く 14ch とぴったり合います。
計算した見積もりは 14けた(=14ch)。この画面で実際に測った値は、画面がついた直後に出します。
width: 7ch と書かれます本当の文はタイプライターです。7文字ですが、上の行と同じ14けた要ります。定番のレシピは 7ch しか書かないので、ちょうど半分で切れます。
計算した見積もりは 14けた(=14ch)。この画面で実際に測った値は、画面がついた直後に出します。
width: 6ch と書かれます本当の文はCSSで作るです。6文字で9けた。3けた足りません。しかも1段あたりの幅が一定なので、幅を直しても文字の途中で切れる段ができます。
計算した見積もりは 9けた(=9ch)。この画面で実際に測った値は、画面がついた直後に出します。
width: 12ch と書かれます本当の文はiiiiiiiiiiiiです。12文字なので定番のレシピは 12ch と書きますが、i は「0」より細いので右が余ります。
等幅とはちがうので、幅は実際に測った値だけを使います。この画面で実際に測った値は、画面がついた直後に出します。
width: 12ch と書かれます本当の文はWWWWWWWWWWWWです。同じ12文字でも、W は「0」より太いので 12ch では足りず、切れます。英語でも崩れるということです。
等幅とはちがうので、幅は実際に測った値だけを使います。この画面で実際に測った値は、画面がついた直後に出します。
貼れるコード
この CSS が、上のプレビューをそのまま動かしています。画像は0個、JavaScript は0行です(2369文字・4059バイト)。 図のぶんも入っているので、貼るときは上半分だけで動きます。
/* 幅を広げていく作り方。箱の幅を段ごとに広げて、隠れていた文字を出します。 */
.pt-type {
/* 幅を持たせるために、行の中の箱にします */
display: inline-block;
vertical-align: bottom;
/* 幅にカーソルの線を含めない指定です。貼り先が border-box にそろえていると、
線の2pxぶんだけ文字の場所がせまくなり、最後の文字が入りきりません */
box-sizing: content-box;
/* ⭐ はみ出したところを隠します。ここが「まだ打っていない」ところの正体です */
overflow: hidden;
/* 折り返すと高さが変わってしまうので、1行のままにします */
white-space: nowrap;
/* ⭐ 文字の幅をそろえます。そろっていないと、1段の幅と文字の幅が合いません */
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
/* ここが定番のレシピです。文字数をそのまま ch にしています。
ch は「0」の送り幅なので、全角の文字はこれより広く、足りなくなります。 */
--pt-w: 7ch;
width: var(--pt-w);
/* ⭐ カーソルは、この箱の右の線です。箱が広がると線もついてくるので、
打っている場所を追いかけます */
border-right: 2px solid currentColor;
/* 段の数は「文字の数」。上の幅とは別の数です。
2つ目は点滅で、打ち終わると止まります */
animation:
pt-typing 1.26s steps(7) forwards,
pt-blink 1s steps(2, jump-none) 2;
}
/* 0 から、できあがりの幅まで。あいだは steps が刻んでくれます */
@keyframes pt-typing {
from {
width: 0;
}
to {
width: var(--pt-w);
}
}
/* いまの幅では 7けた足りません。最後まで動いても、右が切れたままです。 */
/* ⭐ 終わったあとの指定(fill-mode)を書いていないので、
点滅が終わるとカーソルは元の色に戻り、消えずに残ります。 */
@keyframes pt-blink {
from {
border-right-color: currentColor;
}
to {
border-right-color: transparent;
}
}
/* この段落を消さないでください。
「動きを減らしたい」と機械に伝えている人がいます。
文字が次々に動くと、読むのがつらい人・気分が悪くなる人がいるからです。
その設定のときだけ、ここが効いて、はじめから全部見えます。 */
@media (prefers-reduced-motion: reduce) {
.pt-type {
/* 打つ動きも点滅も止めて、はじめから全部見せます */
animation: none;
/* ⭐ 幅の指定もやめます。こうしないと、切れたまま止まります
── つまりこの書き方は、動きを見る人にだけ文が欠けます */
width: auto;
}
}<!-- 文はそのまま1つ。CSS が箱の幅で見せ隠しします --> <p class="pt-type">タイプライター</p>
長い行は、この枠の中だけ横に動かせます(キーボードでも動きます)。 いまの文は7文字・14けたです。
steps の第2引数を書かないと、どうなるか
4段(steps(4))のときに、時間がどこまで進むと 幅がどこまで広がるかを並べます。入力は時間の進み具合、出力は幅の進み具合で、 どちらも0から1です。
| 入力時間の進み具合 | jump-end | jump-start | jump-none | jump-both |
|---|---|---|---|---|
| 0 | 0 | 0.25 | 0 | 0.2 |
| 0.25 | 0.25 | 0.5 | 0.33 | 0.4 |
| 0.5 | 0.5 | 0.75 | 0.67 | 0.6 |
| 0.75 | 0.75 | 1 | 1 | 0.8 |
| 1 | 1 | 1 | 1 | 1 |
書かないと jump-endです。表の1行目を見ると、 jump-end では時間が0のとき幅も0 ── つまりはじめの1段ぶん、何も出ません。 jump-start は逆で、始まった瞬間に1文字目が出るかわりに、最後の1段ぶん、何も起きない時間ができます。
よくある質問
タイプライター効果が、日本語だと途中で切れるのはなぜですか?
幅の単位に使う ch が「1文字の幅」ではないからです。CSSの仕様は ch を「そのフォントで数字の0(U+0030)を1つ並べたときの送り幅」と決めています。日本語の全角文字は0より広く、たいていちょうど2倍あります。ですから「文字数×1ch」と書くと、全角だけの文では必要な幅の半分しか用意できず、ちょうど半分のところで切れます。等幅フォントの英数字だけが、たまたま1文字=1chになります。
どう直せばいいですか?
3つあります。1つ目は幅を「けた数」で数えることです。全角を2けた、半角を1けたと数えて合計します。2つ目は ic という単位を使うことです。ic は漢字の「水」(U+6C34)の送り幅なので、全角1文字がちょうど1icになります。3つ目は幅を使わずに、文字を1つずつ span で包んで出る時刻をずらすことです。半角と全角がまざった文では、3つ目しか使えません。
半角と全角がまざると、幅を直しても崩れるのはなぜですか?
steps() が時間を等しく刻むからです。たとえば9けたを6段で割ると1段あたり1.5けたになり、段の切れ目が文字の切れ目と合いません。その結果、全角の文字が縦半分だけ出ている瞬間ができます。幅の合計が正しくても、途中の見え方が崩れるということです。1文字ずつ遅らせる作り方は、幅を1つも測らないのでこれが起きません。
steps(5) と steps(5, jump-start) は何が違いますか?
段の変わり目が来る場所が違います。第2引数を書かないと jump-end になり、最後の段が終わりぴったりに来ます。そのかわり、はじめの1段ぶんは何も出ません。jump-start は逆で、始まった瞬間に1文字目が出るかわりに、最後の1段ぶんは何も起きない時間になります。どちらが正しいということはなく、待たされる感じを先に置くか後ろに置くかの違いです。
絵文字を入れると、文字の数が合わなくなるのはなぜですか?
絵文字はいくつかの符号を組み合わせて1文字になっていることがあるからです。たとえば家族の絵文字は「人・つなぎ・人・つなぎ・人」の5つの符号でできていて、プログラムがふつうに数えると5と答えますが、画面に出るのは1文字ぶんです。人が1文字と感じるまとまりは書記素と呼ばれ、Intl.Segmenter で数えられます。この道具は書記素で数えています。
動く文字を出すとき、気をつけることはありますか?
2つあります。1つは、動きを減らしたいと機械に伝えている人がいることです。prefers-reduced-motion という設定で伝わるので、その人にははじめから全部見せます。もう1つは長さです。5秒より長く動き続けるものには、止めたり隠したりする手段が要る、という決まり(WCAG 2.2.2)があります。この道具は、打ち終わるまでの秒数が5秒を超えたら画面でそう伝えます。
文字が1つずつ打ち出される動きを、画像もJavaScriptも使わずに作ります。この効果は日本語で崩れます。どこがどう崩れるのかを画面で見せて、崩れない作り方まで書き出します。
chは「1文字の幅」ではありません
ここがこのページでいちばん大事なところです。タイプライター効果の定番の作り方は、箱の幅をchという単位で決めて、steps()で刻んで広げるというものです。ところがchは、多くの人が思っているものとは違います。
CSSの仕様書(CSS Values and Units Module Level 4(新しいタブで開きます))は、chをこう決めています。
そのフォントで「0」(ZERO、U+0030)の字を1つ置いたときの送り幅(the used advance measure of the "0" (ZERO, U+0030) glyph in the font used to render it)
つまり1chは数字のゼロ1個ぶんの幅であって、 「1文字ぶんの幅」ではありません。送り幅というのは、その字を書いたあと次の字を書き始める場所までの距離のことです。
日本語の全角の文字は、ゼロよりも広く場所を取ります。多くのフォントではちょうど2倍です。だから「文字数ぶんのch」と書くと、必要な幅の半分しか用意できません。 画面では、ちょうど半分のところで文が切れて止まります。
英語のお手本がうまくいくのは、たまたまです
等幅フォントは、どの字も同じ送り幅で並ぶように作られたフォントです。 そこではゼロも A も W も同じ幅なので、 「1文字=1ch」がぴったり成り立ちます。 よそで見かけるお手本が英語なのは、そのぶん崩れないからです。
上の図の1行目と2行目を見てください。「CSS typewriter」は14文字、「タイプライター」は7文字です。ところが並べたときの長さはどちらも14けた分で同じです。 文字数だけを見て幅を決めると、同じ長さの文に倍ちがう幅を与えてしまう、というわけです。
等幅でないフォントなら、英語でも崩れます。図の4行目と5行目は、どちらも12文字ですが、細いiは右が余り、 太いWは右が切れます。chはあくまでゼロの幅なので、ほかの字の幅とは関係がありません。
定番のレシピは、同じ「文字数」を2か所に使っています
よそで見かける書き方をよく読むと、文字数を2回使っています。 1回目はできあがりの幅(width)、 2回目は段の数(steps())です。
この2つは、別のものを数えています。幅が数えているのはけた数、段が数えているのは文字数です。 英数字を等幅で並べているあいだは、この2つがいつも同じ数になるので、別のものだと気づけません。日本語を入れたとたんに食い違います。
直し方は、幅はけた数で、段は文字数で数えることです。 「タイプライター」なら幅は14けた、段は7段。 上の道具で「けた数 × 1ch」を選ぶと、この形の CSS が出ます。
icは「水」の幅です
じつはCSSには、日本語や中国語のための長さの単位が用意されています。icです。同じ仕様書が、こう決めています。
そのフォントで「水」(CJK water ideograph、U+6C34)の字を置いたときの送り幅
なぜ「水」なのでしょう。全角の文字は、ほとんどが同じ幅の四角い枠に収まるように作られています。 その代表として、画数がほどよく、どのフォントにもきっと入っている字が選ばれた、というわけです。chのほうも同じ考え方で、どの欧文フォントにも必ずある「0」が代表になっています。
icを使うと、全角1文字がちょうど1icになるので、 数え方が素直になります。ただしこの単位を知らない古いブラウザもあるので、 この道具は@supportsで知らないときはchに戻す書き方まで一緒に出します。
まざった文は、幅を直しても崩れます
ここからが、幅の数え方だけでは届かないところです。半角と全角がまざった文を考えてみてください。 たとえば「CSSで作る」は6文字ですが、半角3けたと全角6けたで合わせて9けたあります。
幅を9けたに直しても、段は6段のままです。steps()は時間を等しく刻むので、 1段あたりに広がる幅は1.5けたになります。これは文字の切れ目と合いません。全角の文字が縦に半分だけ出ている瞬間ができます。
直しようがありません。幅を広げていく作り方は、 「どの字も同じ幅」という前提の上に乗っているからです。 前提のほうが崩れているので、数え方をどう変えても直りません。
幅を測らない作り方に変えます
そこで、まったく別の作り方に移ります。文字を1つずつspanで包んで、出てくる時刻をずらすのです。 1文字目は0秒後、2文字目は0.18秒後、3文字目は0.36秒後……というように、1文字ずつ順番に見えるようにします。
この作り方は幅を1つも測りません。だから半角と全角がまざっていても、絵文字が入っていても、等幅でないフォントでも崩れません。かわりに、隠すときはvisibilityを使うところが大事です。display: noneにするとその文字のぶん行が縮んでしまい、打つたびに行の長さが変わります。visibility: hiddenなら場所は取ったまま隠れるので、 行は最初から最後まで動きません。
そのかわり、カーソルは打っている場所を追いかけません。 場所を最初から取ってあるので、カーソルは行の終わりに置かれたままになります。どちらの作り方にも、できることとできないことがあります。
steps(5)と書いたときの「書かなかったほう」
steps()には2つ目の引数があります。 段の変わり目をどこに置くかを決めるもので、jump-start・jump-end・jump-none・jump-bothの4つから選べます。
書かなかったときは、どうなるのでしょう。CSS Easing Functions Level 1(新しいタブで開きます)は、こう決めています。
2つ目の引数が省かれたときは、
endが指定されたものとする(If the second parameter is omitted, the value end is assumed)
そしてendはjump-endと同じふるまいをする、とも書かれています。jump-endは最後の段が終わりぴったりに来る刻み方です。
これが見え方に出ます。jump-endでは、時間が0のとき幅も0のままです。つまりはじめの1段ぶんのあいだ、画面には何も出ません。 7文字を1.26秒で打つなら、最初の0.18秒はまっさらです。jump-startに変えると、始まった瞬間に1文字目が出るかわりに、最後の1段ぶんは何も起きない時間になります。 上の表で、同じsteps(4)の数字がどう入れ替わるか見比べてみてください。
絵文字が入ると、「何文字」かが変わります
段の数は文字の数で決めます。では「文字の数」とは何でしょう。
たとえば家族の絵文字は、「人・つなぎ・人・つなぎ・人」の5つの符号でできています。プログラムがふつうに数えると5と答えますが、 画面に出るのは1文字ぶんです。 濁点も同じで、「が」を「か」と濁点の2つに分けて書くことができます。
人が「1文字」と感じるまとまりを書記素といいます。 ブラウザにはIntl.Segmenterという道具があって、これで数えられます。この道具は書記素で数えています。符号の数で段を刻むと、打ち終わる前に文が全部出てしまうことになります。
動く文字には、決まりがあります
文字が動くのは楽しいのですが、読むのがつらくなる人や気分が悪くなる人がいます。その人たちは、 機械の設定で「動きを減らしてほしい」と伝えています。 CSSではprefers-reduced-motionで受け取れます。
この道具は、その段落を必ず書き出します。外すつまみは置いていません。設定している人には、打つ動きをやめてはじめから全部見せます。
ここで面白いことが起きます。幅が足りていない「定番のレシピ」のままでも、動きを止めた人には全部見えます。止めるときに幅の指定もやめるからです。つまりあの崩れ方は、動きを見る人にだけ起きます。
もう1つ、長さの決まりがあります。WCAG 2.2 の達成基準 2.2.2(新しいタブで開きます)は、自動で始まり、5秒より長く続く動きには止める・隠す手段が要ると定めています。文字数と速さをかけた秒数が そこを超えたら、この道具は画面でそう伝えます。 カーソルの点滅も、打ち終わったら止まるようにしてあります。
貼った文は、どこにも送りません
打ち込んだ文はこの画面の中だけで使われます。 どこかへ送ることも、保存することもありません。 できあがったCSSも、あなたのブラウザの中で組み立てています。
打った文がCSSに入ることもありません。CSSに入るのは数だけです ── けた数・秒数・段の数の3つで、 文そのものはHTMLの側にしか出てきません。この道具は、その決まりが守られているかを機械で見張っています。
文の長さは40文字までにしています。これより長い動きは、 上に書いた5秒の決まりに当たりやすくなるためです。 けた数の見分けはUnicode の東アジア文字幅(UAX #11)(新しいタブで開きます)の表から、日本語の文に出てくる範囲を写して使っています。全部の範囲を写したわけではないので、 珍しい文字は1けたとして数えます。