font-familyに指定するフォントスタックの一般的なパターン

font-familyのベストな書き方20XX年みたいな記事が毎年出るけど、実際のところケースバイケースで、唯一のベストな解があるというわけでない。なので個人的な理解に基づいたパターンの分類をメモとして書いておく。特に目新しいことはない。

日本語環境で、ウェブフォントは考慮しないという前提。

和文フォントのみ

html {
  font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

たぶん最も無難。

游ゴシックはいろいろあって不安なので、特に寿命が長いウェブサイトでは採用したくない。次のバージョンのOSでは状況が変わったりしそうだから。

メイリオWindows環境で利用するため。MS Pゴシックは基本的には絶対悪で、アンチエイリアスがかからず可読性が悪いので避けたい。すると和文フォントはメイリオ択一になる。Windows環境のブラウザでは、sans-serifのデフォルトがメイリオになりつつあるんだけど、まだまちまちなので明示的に指定してあげないといけない。

ヒラギノMaciOSのデフォルトだけど、メイリオより高い優先順位にするために指定する。

メイリオWindowsのデフォルトになってくれれば、これはsans-serifと指定するだけで事足りるようになる。

和欧混植

html {
  font-family: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}

上記のスタックに欧文フォントを加えただけ。

従属欧文はあまりよく思われないことが多いので、欧文フォントを和文フォントより優先して指定する。選択肢はいろいろあるけど、この辺がよく使われる。

システムフォント

html {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

デバイスのシステムフォントとして採用されているフォントをウェブサイトにも利用する例。

近年のCSSフレームワークなどではよく採用されていて、上記の記事にあるMediumの他に、GitHubとかQiitaとかでもこれ。

結果的にこれも和欧混植になることが多いけど、分類のために便宜上区別してる。


フォントスタックを選択するときの考え方は、たぶんこういう分類にするのが適切だと思う。

わざと引っかかりのあるアニメーションを実装する

一般的にはアニメーションは高パフォーマンスであることが良しとされる。が、あえてFPSを下げたアニメーションにしたいことがあった。レトロゲームのような世界観を表現したいというやつだった。

tween.jsは、ユーザーがアニメーションの更新タイミングを制御できるAPIになっている。こういう感じ。

var coords = { x: 0, y: 0 };
var tween = new TWEEN.Tween(coords)
    .to({ x: 100, y: 100 }, 1000)
    .onUpdate(function() {
        console.log(this.x, this.y);
    })
    .start();

requestAnimationFrame(animate);

function animate(time) {
    requestAnimationFrame(animate);
    TWEEN.update(time);
}

つまりTWEEN.updateを呼ぶ頻度を制御できればいい。それを踏まえてこういう風になる。

const createFpsControlledTicker = (handleTick, fps = 60) => {
  let startTime = null
  let lastFrame = -1
  const secondsPerFrame = 1000 / fps
  let requestId = null
  let isRunning = false

  const tick = () => {
    requestId = requestAnimationFrame(tick)

    if (startTime == null) startTime = performance.now()

    const currentTime = performance.now()
    const elapsedTime = currentTime - startTime
    const currentFrame = Math.floor(elapsedTime / secondsPerFrame)

    if (lastFrame !== currentFrame) {
      handleTick(currentFrame)
      lastFrame = currentFrame
    }
  }

  const start = () => {
    if (isRunning) return
    requestId = requestAnimationFrame(tick)
    isRunning = true
  }

  const stop = () => {
    if (!isRunning) return
    cancelAnimationFrame(requestId)
    isRunning = false
  }

  return {
    start,
    stop,
  }
}

createFpsControlledTicker(() => {
  TWEEN.update()
}, 15).start()

以上のようなコードで15fpsのアニメーションを実装できた。

ブレイクポイントは端末のサイズに依存させない

レスポンシブデザインの最適なブレイクポイントは何pxなのかという話題をよく見る。既存のデバイスのサイズを比較することで答えにたどり着こうとするアプローチがほとんどだが、それらは間違っている。

ブレイクポイントは、コンテンツの見た目が切り替わるべきタイミングを基準に設定されなければならない。特定のデバイスのサイズを基準にすべきではない。

主流のデバイスのサイズは時間が経てば変わるし、あるデバイスに合わせた設計にしていたら別のデバイスには嬉しくない結果になってるかもしれない。レスポンシブデザインはどんな環境においてもある程度の最適化をさせるために妥協可能な落としどころを目指す思想なので、それと対立してしまう。

改善策は、既存のデバイスのサイズを気にし過ぎずに、コンテンツ自身のためのブレイクポイントを考えること。コンテンツ自身に合わせたブレイクポイントの設計にすることで、結果的にどのデバイスでも最適な見た目になるという形が望ましい。

とはいえ現実的には、ブレイクポイントは決定事項であった方が都合がいい。デザインごとに最適なブレイクポイントを探るのは手間がかかるし、デザイナーとコーダーが分業する際のやりとりは少ない方がいいからだ。そのため、あらかじめ定数として定義しておいた方がやりやすいというのは理解できる。個人的には以下のように決めている。Material designを参考にした。

$breakpoint-xs: 480px;
$breakpoint-sm: 600px;
$breakpoint-md: 960px;
$breakpoint-lg: 1280px;

バイスを意識させない命名にした上で、適切な位置や間隔でブレイクポイントを配置することを意識している。これらは、コンテンツの見た目が切り替わるブレイクポイントとして汎用性の高い値だと思っている。

最終的にやってることは既存の記事と同じようになったけど、そもそもなぜこうするのかを理解した前提でするべき話だと思ったのでこれを書いた。

もっと細かい値を基準にしたVertical Rhythm

本文のline-heightを基数としたVertical Rhythmは、値が大きくなりすぎるのでどうしても細かい調整がしたくなってうまくいかない。

それに悩んでいて@terkelに聞いてみたら、「本文のline-heightより細かい数字を基準にしてる、4pxとか。Material Designも多分そういうルールに則ってる」という話を聞いた。なるほど良さそうと思ったものの、細かい数字になってくると計算がめんどくさそう。

Rhythmic Sizingを利用できれば、それはかなり簡単に実現できる。要素のサイズやline-heightの値を、必ず指定した数値の倍数になるようにできるというやつだ。以下はline-height4pxの倍数にする例。

html {
  line-height-step: 4px;
}

.text {
  font-size: 1rem; // 16px
  line-height: 1.7; // 27.2px => 28px
}

.heading {
  font-size: 3rem; // 48px
  line-height: 1.3; // 62.4px => 64px
}

これが動くようになれば嬉しいんだけど、まだ実装されるかもわからないのでSassでline-height-stepもどきを作ってみる。

$line-height: 1.7;
$line-height-small: 1.3;

$font-size: 1rem;
$font-size-large: 3rem;

$rhythm-text-line: ($font-size * $line-height);
$rhythm-unit: ($rhythm-text-line / 4);

// step unit like
// Reference: https://www.w3.org/TR/css-rhythm-1/#step-unit
@function rhythm-step($length) {
  @if unit($length) != unit($rhythm-unit) {
    @error "The unit of $length should be #{unit($rhythm-unit)}";
  }
  $steps: ceil($length / $rhythm-unit);
  @return ($rhythm-unit * $steps);
}

// unit less `line-height`
// Reference: https://twitter.com/terkel/status/826600067930873857
@function rhythm-line-height($font-size, $line-height) {
  @if unit($font-size) != unit($rhythm-unit) {
    @error "The unit of $font-size should be #{unit($rhythm-unit)}";
  }
  @if not unitless($line-height) {
    @error "$line-height should not have a unit";
  }
  $rhythmical-length: rhythm-step($font-size * $line-height);
  @return ($rhythmical-length / $font-size);
}

.text {
  margin: 0 0 $rhythm-text-line;
  font-size: $font-size;
  line-height: rhythm-line-height($font-size, $line-height);
}

.heading {
  margin: 0 0 $rhythm-text-line;
  font-size: $font-size-large;
  line-height: rhythm-line-height($font-size-large, $line-height-small);
}

rhythm-stepに与えた値は$rhythm-unitの倍数になる。$rhythm-unitの値は、pxにしたときに整数になるようにした方が安心できるんじゃないかとか、本文のfont-sizeline-heightの最大公約数になってた方が都合がいいんじゃないかとか考えると4pxにするのがちょうどいい気もする。けど、小さすぎる値になるとパターンとして認識されない気がしたので、少し大きめの本文のline-heightを4で割った値にしてる。

rhythm-line-heightは、line-heightから単位を取り除いた値に変換する。というのも、line-heightに絶対的な値を指定していると、フォントサイズが想定より大きい値になったときに行間が詰まりすぎてしまうからだ。具体的には、Chromeの最小フォントサイズ設定はデフォルトでは10pxになっているが、ユーザーがそれを16pxに設定しているとする。その場合、次のようになっていると問題が起こる。

.text {
  font-size: 10px; // => 16px
  line-height: 15px;
}

単位無しの値を設定しておくと、リズムは崩れるものの、最低限の可読性を確保することはできる。JavaScriptで無理やり解決することはできそうだけど、そこまでのリスクを負ってやるべきことではなさそう。

ユーザーは常にスクロールしながらコンテンツを閲覧するので、視点を一定のリズムで制御できる利点は失われるという意見もあるので、どの程度のコストをかけて実装していくべきなのかは考えたい。

エモさ on 地味な土台 in ウェブサイト

地味な土台の部分をきちんと作った上に、いわゆるエモさみたいなものが積み上げられた形のウェブサイトを作りたいなというのをずっと思ってる。

地味な土台というのは、アクセシビリティやパフォーマンス、サイト全体を通してのスタイルの一貫性など、ウェブサイトを作るために普通に意識すべきことのことを指している。

というのも、最近のウェブサイトを作るときの意識って、気持ちいいアニメーションがあって、インタラクティブに動いて、見た人が魅了されるみたいなエモさを追求することに向かい過ぎてる。その結果、当たり前にやるべきはずのことが蔑ろにされている。しかし、エモさはあくまで付加価値であって、それ自体を最優先で考えるのはウェブサイトの目的を見失っていることに他ならないと感じる。

なんでその目に見えない地味な部分を見た目にわかりやすいエモい部分より優先しないといけないのかというと、それが他のメディアにはないウェブの強みを活かすために必要な要素だからだ。ウェブは普遍的であることで発展してきた。誰もがどこからもアクセスできて、いつでもちゃんと使える。これはごく当たり前のことに聞こえるけど、多くはそれを意識できてないか無視してる。

反して、ウェブの利点を犠牲にしてまでウェブサイトのエモさを追求したところで他のメディアには敵わない。縦横比が固定された動画や、加えてプラットフォームが限定されたゲームなどと違って、多様なユーザーエージェントに対応するためには点で見たときの見た目や体験だけをあてにできない。だから、発想の柔軟性は制限されているし、コストも掛かり過ぎる。

ユーザーの多様性を認めることがウェブの良さを最も素直に発揮できる方法であって、全てのウェブサイトはその前提の下で設計されるべきだ。

とは言いつつも、僕はウェブサイトにおけるエモさを否定はしてないし、それを取り入れること自体は良いことだと思ってる。けど、そのために別の環境やユーザーからはアクセスできない、使えないようになってしまうなら採用すべきでない。

ウェブサイトを制作するために必要なことをピラミッドで表すと、地味な土台の上にエモさは位置している。下の層から順に達成できている状態で、その先でエモさを作り出すことに取り組めれば最も理想的。そんな優先順位でウェブ制作に取り組むことが当たり前になれば、みんなにとって魅力的なウェブにしていけると僕は信じてます。

CSSにおける和欧混植のベストプラクティス

欧文はウェブフォント、和文はユーザーエージェントのデフォルトにするというのがベスト。

html {
  font-family: Lato, sans-serif;
}

というのも、ローカルの欧文フォントを指定するためにはプラットフォームごとにインストールされているフォントを把握しておく必要があるからだ。現状のシェアを占めているものだけに絞ればまだしも、クライアント環境が多様化し続けている昨今ではそのアプローチは不十分に思える。

ちなみに、主要なプラットフォームにインストールされているフォントの一覧は以下。

ウェブフォントを利用した指定にしておけば、プラットフォームに関わらず和欧混植にできる。読み込みに失敗するというケースも考えられるが、その場合の見栄えは妥協できるだろう。

和文の指定に関しては、まずウェブフォントはコストが高いので避けたい。加えて、前述したような理由でフォントファミリー名を指定するのも微妙。そこでsans-serifなどのジェネリックな指定にしておけば、ユーザーエージェントが最適なフォントを選択してくれる可能性が高い。

とはいえ、タイポグラフィとしては和文フォントと欧文フォントの組み合わせがまずいものにならないようにはしたい。そういう場合は、それぞれのフォントが利用できる場合のみ特定の処理を行うというアプローチも取れる。

参考:

Sassのmixinでユニークなキーフレーム名を宣言する

スプライトアニメーションをコンテンツの背景にフィットさせる - ライデンの新人ブログ

こういうやつを複数回やりたかったのでmixinにした。普通にやるとキーフレーム名がかぶるので、mixinを呼び出すたびにIDをインクリメントさせるようにしたらいけた。

$_sprite-id: -1;

@mixin sprite($frame-count, $seconds-per-frame) {
  $_sprite-id: $_sprite-id + 1 !global;

  background-size: 100% ($frame-count * 100%);
  animation: sprite-#{$_sprite-id} #{$frame-count * $seconds-per-frame}s steps($frame-count) infinite;

  @keyframes sprite-#{$_sprite-id} {
    from {
      background-position: left top;
    }
    to {
      background-position: left (100% * $frame-count / ($frame-count - 1));
    }
  }
}

.sprite1 {
  @include sprite(3, .1);

  background-image: url("sprite1.png");
}

.sprite2 {
  @include sprite(12, .2);

  background-image: url("sprite2.png");
}

keyframesの内容が同じでも複数回宣言するので雑だと思うけど、ちゃんとやるのめんどくさそうなのでやめた。Mapで$frame-countをキーにしてIDを保存しとくとかすればいけそう。