日本語向けフォントスタックの現状
日本語のウェブサイト向けのフォントスタックの現状と無難な設定についてまとめた。sans-serif
、serif
、system-ui
のそれぞれの総称フォントファミリーに基づいて、主要な端末(Windows、Mac、iOS、Android)のフォントの搭載状況を整理する。
sans-serif
まず、Windowsはメイリオ一択だと考えたほうが良い。游ゴシックはWindows 8.1ではかなり細く、Windows 10でも一般的なフォントと比べると少し細いのが問題だ。ハック的に回避する方法はあるものの、積極的に採用したくはない。メイリオはWindowsユーザーにとって馴染みがあり、最も問題になりにくいフォントだと考えられるため、あえて別の選択をする必要性は低いと思う。
Yu Gothic UIという選択肢もあるが、本文向きでは無さそうだ。
Macでは問題なく游ゴシックが利用できるので、ヒラギノ角ゴシックかいずれかを選択できる。
ヒラギノ角ゴシックを選択する場合、ファミリーの名称はこれまでのようにHiragino Kaku Gothic ProN
とするか、新しいHiragino Sans
とするかが悩みどころになる。前者はウェイトのバリエーションがW3とW6の2段階しかなく、後者はW1からW9までの9段階あるのが違いだ。ウェイトの多い別のフォントとの併用を考えると、Hiragino Sans
を指定しておく方が見た目の印象を一貫させやすいだろう。
注意点は、Hiragino Kaku Gothic ProN
をHiragino Sans
にそのまま置き換えると違うウェイトになることがあるということ。CSSでfont-weight: normal
が指定されていると、前者ではW3が適応され、後者ではW4になる。bold
の場合、前者ではW6、後者ではW7だ。ウェイトのバリエーションが増えたことにより、CSSの指定に対応するフォントが変わるということだ。あえてW3を利用したい場合、font-weight: 300
と指定する必要がある。どのウェイトが最適かは場合によりけりだが、これまで慣れた見た目と違うものになり得ることは頭に入れておいたほうが良い。
ただし、ウェイトのバリエーションが増えるのはMacのみで、iOSはこれまでと同じくW3とW6だ。
これらを踏まえると次のような指定になる。
$font-stack-sans-serif: "Hiragino Sans", "Meiryo", sans-serif;
Windowsの主要なブラウザでは、sans-serif
を指定するとメイリオが選択される。だが、Windows 7のIE11に限ってはMS Pゴシックで表示される。それを上書きするために指定する必要がある。
また、古いバージョンのMS Office(Office for Mac 2011?)をインストールすると、Macでもメイリオが有効になってしまう。Macではメイリオが選択されてしまわないようにするため、ヒラギノ角ゴシックを先に指定する。
Macで游ゴシックを利用したい場合は次のようになる。
$font-stack-sans-serif: "YuGothic", "Hiragino Sans", "Meiryo", sans-serif;
iOSには游ゴシックがインストールされていないため、ヒラギノ角ゴシックで表示されるように指定する。
Hiragino Sans
はOS X El Capitan及びiOS 9以降から搭載されたフォントなので、それ以前のバージョンもサポートするには、Hiragino Sans
に続いてHiragino Kaku Gothic ProN
も指定する必要がある。
serif
Windowsには游明朝を利用させる。游ゴシックと同様に細く見える問題はあるが、MS P明朝と比べるとかなりいい状態で読める。現在のChromeではserif
の既定のフォントは游明朝になっており、Firefox 57以降でも同じように変更されることになっている。
Macではヒラギノ明朝か游明朝を選択できる。ヒラギノ明朝を利用する場合は次のようになる。
$font-stack-serif: "Yu Mincho", serif;
游明朝を利用する場合は次のようになる。
$font-stack-serif: "YuMincho", "Yu Mincho", serif;
遊書体はWindows 8.1以降から搭載されたフォントであるため、Windows 7ではMS P明朝になる。
また、Android端末の場合、serif
に対応するフォントは搭載されていない。ウェブフォントを利用するか、諦めてsans-serif
等で代用するしかない。
system-ui
system-ui
はsans-serif
などと同じ総称フォントファミリーだ。CSS Fonts Module Level 4で追加された。プラットフォームのUIと同じフォントを利用できる。現状ではChromeのみで実装されており、その他のブラウザのためにはフォールバックを書く必要がある。
フォールバックを含めた指定方法はいろいろ紹介されているが、英語圏のブログに書かれているような指定は日本語向きではなさそうだ。
Chromeでsystem-ui
を指定した場合、MacとiOSではSan Franciscoになる(ヒラギノ角ゴシックは含まれていない)。Windows 10ではYu Gothic UI
で、Windows 8.1以前ではSegoe UI
とMeiryo
の混植。AndroidではRoboto
とNoto
の混植。
ブラウザをまたいでもこれと同じ結果にするためには、次のように指定する。
$font-stack-system-ui: system-ui, -apple-system, "Hiragino Sans", "Yu Gothic UI", "Segoe UI", "Meiryo", sans-serif;
それぞれの値の意味は次のようになる。
$font-stack-system-ui: // 1. OS X Chrome(欧文)、Windows Chrome(和文・欧文)、Android Chrome(和文・欧文) system-ui, // 2. OS X Safari(欧文)、iOS Safari(欧文)、OS X Firefox(欧文) -apple-system, // 3. OS XとiOS全て(和文) "Hiragino Sans", // 4. Windows 10 Chrome以外(和文・欧文) "Yu Gothic UI", // 5. Windows 8.1以前 Chrome以外(欧文) "Segoe UI", // 6. Windows 8.1以前 Chrome以外(和文)(Windows 7のIE11用の指定) "Meiryo", // 7. その他 sans-serif;
Mac及びiOSの場合、1か2の指定でSan Franciscoを利用できる。それだけだと和文の指定が無いので3を指定する。
Windows 10の場合、Chromeは1の指定、その他のブラウザは4の指定でYu Gothic UI
になる。Yu Gothic UI
自体にSegoe UI
が含まれているため、あえてSegoe UI
を指定する必要はない。
Windows 8.1以前の場合、Chromeは1の指定、その他のブラウザは5と6の指定でSegoe UI
とMeiryo
の混植になる。
Android Chromeは1の指定でRoboto
とNoto
の混植になる。
San FranciscoはOS X El Capitan及びiOS 9以降から搭載されたフォントなので、それ以前のOSもサポートすると、Hiragino Sans
の前にHelvetica Neue
の指定が必要。加えて、Hiragino Sans
も同じタイミングで搭載されたフォントなので、その後ろにHiragino Kaku Gothic ProN
の指定が必要になる。
参考
よくある「font-family
の最適な指定」系の記事を見ると、違う文脈のファミリーをひとつのフォントスタックに押し込んで、自分の好きな順番に並べただけのようなまとめ方が多いと思っていた。なのでこの記事では、総称フォントファミリーを基準にした一般的(汎用的)な分類でまとめた。自分の主観に依らない視点になっていると嬉しい。
:focus-ringの代用としてwhat-inputを試す
前回の記事で紹介した:focus-ring
のポリフィルはイマイチだった。element.focus()
で制御したときにいい感じにならないというつぶやきを見て知った。
具体的な例として、モーダルを閉じた後の挙動について考えたい。
モーダルを閉じた後は、フォーカスはモーダルを開いたボタンに戻るように実装する。そのために、button.focus()
のようにしてフォーカスをスクリプトで制御する。この際、:focus-ring
のポリフィルだとfocus-ringが有効になる処理が実行されず、キーボード操作をしていても適切なスタイルが表示されないことになる。
調べてみるとWhat Input?という似たライブラリがあった。ユーザーの入力端末を検出する機能があり、同じくfocus-ringの制御をすることが目的らしい。:focus-ring
のポリフィルとは違い、入力端末の検出結果をhtml
要素の属性などを通して公開している。フォーカスのスタイルはそれに基づいて設定すればいい。これを利用すれば、意図した通りにフォーカスのスタイルを機能させることができた。
提供される状態は、initial
、mouse
、keyboard
、touch
のいずれかだ。initial
はまだ検出できていないことを示す。マウスとタッチデバイスではoutline
を非表示にしたいので次のようにする。
[data-whatinput="mouse"] :focus, [data-whatinput="touch"] :focus { outline: none; }
また、E:focus-ring
と同じ意図を示すセレクタは次のようになる。
html:not([data-whatinput="mouse"]):not([data-whatinput="touch"]) .awesome-button:focus { // focus-ring style }
ただこれでは冗長だ。E:hover
と併記したいことも考えると、Sassで次のように抽象化できる。
@mixin focus-ring() { html:not([data-whatinput="mouse"]):not([data-whatinput="touch"]) &:focus { @content; } } .awesome-button:hover { background-color: red; } .awesome-button { @include focus-ring() { @extend .awesome-button:hover; } }
多分これで問題なく:focus-ring
風の実装ができるはずだ。将来的に未知の入力端末が登場しても、マウスとタッチデバイス以外ではフォーカスのスタイルが表示されるため、ウェブサイトが操作不能になる可能性は低いと思う。
CSSのセレクタが複雑になってしまう問題はあるが、フォーカスのスタイルを非表示にしたいという要望とのトレードオフだろう。
本当は:focus-ring
がネイティブで実装される日を待ちたいが、これが今のところの現実解なんだと思う。
outline: noneをやめよう、focus-ringを使おう
次のようなスタイルが指定されたサイトを見かけることがある。
* { outline: none; }
ボタンなどの要素をクリックしたときに、格好の悪いアウトラインが表示されてしまうのを打ち消したい、という意図だと思われる。
だが、上記のような指定をしてはいけない。サイトをキーボード操作することができなくなってしまうからだ。
クリックされたボタンはフォーカスされる。フォーカスされているということを視覚的にユーザーに伝えるためにアウトラインが表示される。キーボード操作するためには、現在のフォーカス位置が明示されている必要がある。上記のような方法によってフォーカス位置の手がかりを奪ってしまうと、キーボードユーザーにはそのサイトが利用できなくなってしまう。
とはいえ、マウスのみでサイトを利用するユーザーにとっては過剰な装飾に見えるかもしれない。マウスユーザーにとってはフォーカス位置が明示されている必要性は低いからだ。この考え方に基づいたアイデアとして、キーボード操作時のみフォーカスを明示するというものがある。
現在、:focus
の代わりに:focus-ring
という擬似クラスを使ってその機能が利用できる、という仕様を標準にするための作業が進められている(Editor’s Draft)。
それに近い機能を実装してくれるポリフィルもあり、それを利用することで今からこれがイマイチだったので代替手段を紹介しました。:focus-ring
もどきなことができる。
キーボードでの操作性を確保しつつ、マウスユーザーには不要なアウトラインを表示しない。見た目の良さかアクセシビリティか、という二者択一にならない現実的な妥協点だと思う。積極的に利用していきたい。
参考
ページ内リンクへの移動時にスクロール位置が固定ヘッダーと被らないようにする方法
追記:
IE以外の主要ブラウザでは scroll-margin-top
が利用できる。
固定ヘッダーがあるサイトだと、ページ内リンクをクリックしたときに対象の要素が固定ヘッダーと被ってしまうことがある。
固定ヘッダーがあるサイトの例としてVue.jsとかBootstrapのドキュメントを見ると、この問題は起こらないようになっていた。調べてみるとそれぞれ同じようなやり方で問題を回避している。
メジャーな手法なのかなと思ってググったらHash Tag Links That Don’t Headbutt The Browser Windowというドンピシャな記事があった。すでに不要になった古いハックとかにも言及していて冗長だったので、メモ代わりにこの記事に書き直しておく。
ページ内リンクへの移動時のスクロール位置は、対象の要素の座標の上端になる。なので単にpadding-top
で余白を作って調整することもできる。でもそれだと、要素間のmargin
の設定に影響したり、ヘッダーの高さが要素間の余白より大きい場合に対応できなかったりする。
次のような方法を用いれば問題なく解決できる。
.heading[id]::before { content: ""; display: block; height: 8rem; margin-top: -8rem; visibility: hidden; }
要素の上部にヘッダーの高さ分の余白を作るために高さを指定して、見た目上はその空間を作らないようにネガティブマージンで打ち消す。こういう感じに動く。
スタイルによっては余計な<span>
要素を作らないといけないとかはありそうだけど、これでだいたい問題なくなる。
静的サイト開発のための最強のボイラープレートを作った
プロジェクトの雛形を雑に作ると開発でかなりストレスを抱えることになる。仮に小規模な静的サイトであっても。
とはいえ開発環境を作ることをがんばりすぎてもコストに見合わないこともある。コストを軽減するために各々ボイラープレートを作ってたりもするけど、その作りもバラバラでだいたい不満が出る。
この問題を解決するために、自分が本当に正しいと思える構成でボイラープレートを作った。作る過程で考えたことについて書く。
受託で静的ウェブサイト作ってるみたいな人向け。
最初に、ほとんどのプロジェクトはビルド前提だが、何をどのようにビルドするかはかなり慎重に考えるべきだ。どんなディレクトリ構成にして、どのファイルをどこにコピーするのか。HTMLテンプレートはどうやって設計するのか。複雑にするのか単純にするのか。
全体の方針
できるだけ標準的な構成に寄せて、初見でも全体を予測させやすくすることを目指した。ビルドプロセスは可能な限りシンプルにした。
想定外の仕様を求められること(文字コードをShift_JISにする、改行コードをCRLFにする、ディレクトリ構成を指定されている)はあるあるなので、ユーザーが当たり前に拡張しやすくするためだ。
Create React Appみたいに外部パッケージ化すると柔軟性が失われるので、あえて全部露出させている。
CSS
まず、CSSはビルドツールに頼り切るしかない。Sassで書いてPostCSSで最適化するというフローからはしばらく抜け出せそうにない。論理的な単位でのソースファイルの分割、共通化すべき値の変数化、ベンダープリフィックスの自動付与などは必須だろう。
ファイルの構成は、個別ファイルを読み込むだけのmain.scss
から、Normalize.cssとベースCSSを読み込んだ上で、その他のコンポーネントをGlobで読み込んでいる。
ちなみに、SassでやっていたことをPostCSSに置き換えることに関してはあまり積極的ではない。
cssnextのアプローチは微妙。機能の雰囲気だけ真似て仕様とぜんぜん違う形で使わされたり、何年経っても実装されなさそうな糖衣構文を使えるようになってもあまり嬉しくない。
Sassは機能が多すぎるので、最低限の機能だけ利用するためにPostCSSを使うというのもある。ただそれだと、使う機能の基準を定める必要があるし、プロジェクトごとにそのバラつきが出て面倒になる。
そうすると、意識的にSassの機能を制限しながら使うというところに落ち着く。たぶん近い未来にCSSをビルドレスで使うということもなくて、10年後も普通にSassを書いてる気がする。
JavaScript
最近のJavaScriptはやたら複雑そうだが、普通のウェブサイトのためのスクリプトとして見ると要点は2つだ。モジュールシステムと、JavaScriptの(モジュールシステム以外の)最新機能だ。複雑なJavaScriptでの実装が求められるとこれらは欠かせない。
webpackとBabelを使って、できるだけ単純な設定でビルドできるようにしている。
これらは、数年後には一通りのモダンブラウザに実装されて、ビルドレスで利用できる見込みがある。ただ、IEをサポートするためにはこれらの複雑なビルドツールに依存し続けるしかない。
HTMLテンプレート
HTMLをテンプレートから生成するのはそこそこのコストになることがある。記述量やページの性質によってはそのままHTMLを書いた方が良いことも少なくない。コードの短縮や修正の効率化のため、今回は仕組みに含めた。
テンプレートエンジンはPugを選んだ。インデントでタグの入れ子を表現するという構文のため、修正の手間をかなり削減できる。個人的にタグというもの自体を書きたくないというのもある。
テンプレートの継承ができたり、テンプレート内にそのままJavaScriptを書けたりという便利な機能もあるが、気をつけないと全く読めない最悪なファイルにしてしまうことがある。これは不安要素としてある。
HTMLの構文とかけ離れているため、学習コストがかかるというデメリットもあるが、それ以上に効率化はできる。
テンプレート内で利用する変数はJSONで管理できるようにした。管理方法は2通り用意した。
まず、単一のテンプレートファイルのみだけで有効なデータ。テンプレートファイルと同名のファイル(company.pug
だとcompany.json
が対応する)を作成すると利用できる。テンプレート内のpage
変数から参照できる。
次に、全てのテンプレートで有効なデータだ。_data/products.json
というファイルを作成すると、テンプレート内のfile.products
変数から参照できる。
└── src └── html ├── _data │ └── products.json ├── company.json └── company.pug
ページのパスも変数として提供する。これがあると、メタ情報の設定やナビゲーションのリンクがアクティブかなどを判定することができる。
それぞれのページからpage.path
変数を参照すると、プロジェクトルートから見たファイルのパスが取得できる。product/drink.pug
だと/product/drink.html
になり、product/index.pug
は/product/
になる。
HTMLテンプレートと開発サーバー
ひとつのテンプレートを変更するたびに全てのテンプレートをビルドしているとかなりの秒数がかかってしまう。ファイル数が少ないと問題にならないが、増えてくると深刻な問題になってくる。これを解決するために、開発サーバーにリクエストしたタイミングでURLに対応するファイルをビルドする仕組みにした。開発時にはライブリロードが有効なまま、ファイル数が増えても高速にビルドできるようになっている。
サブディレクトリの解決
サブディレクトリにウェブサイトを納品する機会は多い。サブディレクトリ直下のページしかない場合は問題になりにくいが、そこからさらにディレクトリが深化する場合はパスの解決が複雑になる。
これは設定ファイルに記述するだけで解決できるようにした。開発サーバーや、ビルド時に生成されるディレクトリ構成もそれに沿ったものになる。
HTMLのテンプレートにも、サブディレクトリのパスを解決できる便利関数を提供している。
ディレクトリ構成
├── dist/ │ └── subdir/ │ ├── assets/ │ │ ├── css/ │ │ │ └── main.css │ │ ├── img/ │ │ │ └── logo.png │ │ └── js/ │ │ └── main.js │ └── index.html ├── public/ │ └── assets/ │ └── img/ │ └── logo.png ├── src/ │ ├── css/ │ │ └── main.scss │ ├── html/ │ │ ├── _data/ │ │ │ └── products.json │ │ ├── _includes/ │ │ │ ├── global-header.pug │ │ │ ├── head.pug │ │ │ └── scripts.pug │ │ ├── index.json │ │ └── index.pug │ └── js/ │ └── main.js └── vendor-public/
src/
ディレクトリにビルドの対象になる全てのファイルが格納される。ディレクトリの見通しのよさのために種類別に分離している。src/html/
ディレクトリの中身はその階層を保ったままプロジェクトルートに出力される。
public/
ディレクトリには単にコピーされるだけのファイルが格納される。画像ファイルやファビコン、あるいはテンプレートから生成しないHTMLファイルなど。
vendor-public/
ディレクトリには開発時には必要だが納品しないファイルを格納する。共通CSSや共通ヘッダーなど。開発サーバーではルートディレクトリから参照できる。
dist/
ディレクトリには本番向けビルドで生成されたファイルが格納される。vendor-public/
ディレクトリの中身は含まれない。
この構成であれば、ディレクトリごとの役割がはっきりしてかなり見通しが良くなる。
他の機能
差分納品やSSIなど、よくある要件の解決策もガイドとして含めた。プロジェクトコードを管理する際の面倒くささはかなり解決できたと思う。
仕事をはじめてからずっとめんどいって思ってた問題は結構解決できた。これからは開発環境じゃなくて、ウェブサイトの作り方自体を改善していきたい。
Normalize.cssの意図(想像)
最近、Normalize.cssがいろいろ揉めてた。
ある日、Normalize.cssには純粋な正規化以外のコードは含むべきでないとして、破壊的な変更を含むリリースがあった。html { font-family: sans-serif; }
やbody { margin: 0; }
などのいわゆるopinionatedなスタイルが削除された。
しかし、それはオーナーの意志ではなく、すぐにその変更を打ち消したバージョンがリリースされた。
その際の主張としては、Normalize.cssは純粋な正規化だけのためのものではなく、有用なデフォルトを提供するベースになるCSSであるというものだった。
ただそう考えると、何を基準にopinionatedなルールが存在しているのかという疑問が生まれる。実際はこれは、作者の独自の考えから来るものということではなく、どの環境でも同じスタイルを適応させるための指定だ。ユーザーエージェントのスタイルシートには指定が無くても、別々のスタイルが適応されることはある。(この辺自分でも根本的に理解できてないけど)その差を是正するためにhtml { font-family: sans-serif; }
とか書かれてる。
normalize.css、こうじゃなくてこうだろって思うんだけどopinionatedの解釈は人それぞれという気がしてきて何も考えられない pic.twitter.com/mJuo9Yd6u7
— キャッシュクリアお願いします (@_yuheiy) 2017年6月6日
この辺のスタイルも気になってたんだけど、ユーザーエージェントのスタイルシートはこの辺に対して個別のスタイルを書いているので、明示的にスタイルを指定しなければばらつきが出るからこうなっている。継承したほうが便利だとは思うんだけど、ユーザーエージェントのスタイルシートは基本的には継承するように書かれていない。継承しないのがユーザーエージェントのデフォルトだ。だからNormalize.cssとしてはこの形が正しいのだと納得できた。いや想像なんだけど。
そう考えると、例外はbody { margin: 0; }
のみだと言える。その他は全てユーザーエージェントをまたいでもスタイルを統一させるための宣言だ。Normalizeとして正しい。
自明なのかもしれないし、どこかに言及が有るのかもしれないけど、忘れそうなので書いといた。
そもそも、ベースになるCSSをなぜここまで深く考える必要があるのかというと、CSSにおいて最初に採用したベースは二度と変更できないケースがほとんどだからだ。なぜならスコープが大き過ぎて影響範囲が予想できないから。この問題はおそらく、Shadow DOMを採用できる時代が来て、真にScopedなスタイルを実現できるようになれば解決の糸口は見える。最長でも10年くらい辛抱すればどうにかなるはずだと思ってる。
Re: なぜピクセルパーフェクトは筋が悪いのか
昨日書いた記事はあまりに雑だった。それに対するつぶやきを見て、思考が整理されたので改めて書く。
ピクセルパーフェクトが正しくないのは、カンプの時点ではまだデザインは未完成であるからだ。つまりは、未完成なデザインをそのまま形にすることを強制していることになる。
GUIのデザインツールとCSSは、それぞれ別のアプローチでデザインを表現できる手段だと言える。
GUIは自由に発想を展開して試行錯誤するためのプレイグラウンドだ。あえて制約を緩めることによって、さまざまな形を実験する効率を高めている。反して、実際の媒体の制約と照らし合わせると現実性の無いものもできる。制約は意識しにくいし、ルールで縛りたい場合には向いてない。そのための仕組みが弱いからだ。
CSSはルールを適応するためのツールだ。指定した要素に対して一律同じスタイルを当てることができる。デザインを構成するルールは論理的な単位で分解して再利用できる。反面、デザインが理路整然としていないとCSSは難しくなる。痛みに最も敏感になれる場所だとも言える。例外に弱い。
良いデザインを作るためには、たくさんの試行錯誤と一貫したルールが必要だ。それを実現するためには、互いを組み合わせてデザインするのが最も強い。
GUIツールを使って作られたカンプを基にCSSを書くことも立派なデザインの過程である。カンプを作ることとはまた別の視点での。それら一通りを同じ意志を持った人が行うことで、初めてひとつのデザインが完成する。
分業は大きな壁だ。いわばデザインの途中段階で、その完成を他人に委ねてしまうことになる。うまく進めるには可能な限り思考を同期させるしかない。
けど、正しい答えにたどり着くための、文脈を理解する手がかりや、そこで見つけた間違いをフィードバックするための仕組みは全く未熟なままだ。ウェブにおいて差し迫った問題として活発に議論されているが、それでもまだまだ重要性が認識されていないことがほとんどだ。
問題を解決するための答えはそれぞれの場所に必ずある。互いのメンタルモデルを深く理解することが、壁を取り払って本質的な問題に目を向けるための第一歩になる。