cssnextを使うべきか

cssnextは、未来のCSS構文を今のブラウザでも解釈できるようにトランスパイルするPostCSSプラグインだ。そう聞くとさも、将来のCSSの書き方をそのまま先取りできる素晴らしいツールであるような印象を抱く。だが実際は、cssnextで表現できる形と標準の仕様は大きく異なっていることがある。cssnextを前提にして書いたコードは、未来のブラウザで違う挙動をする可能性があるということだ。

cssnextは、単一の機能を持ついくつかのPostCSSプラグインをまとめたプラグインセットだ。それぞれのプラグインは単に、ある構文を現在のブラウザでそれっぽく動くコードに変換することしかできない。対象とされている構文の多くは、現状の実装でフォールバックすることが不可能であるため、プラグイン作者の主観に基づいたなんちゃって実装に置き換えるしかないのだ。
そのため、cssnextを通して利用できる構文のいくつかは、実際の仕様と異なった形で利用する必要があり、仕様と異なった挙動をするコードを生成する。

標準の仕様と異なった利用方法をするプラグインとして、postcss-custom-propertiesは顕著な例である。カスタムプロパティは、動的でDOMスコープのプロパティであるという仕様だ。対してこのプラグインでは、プリプロセッサ上でのみ有効な変数として解釈されて、Sassのように静的な値に置き換えられる。これではカスタムプロパティを利用する意味が変わってしまうので、本来の仕様の意図から外れたコードを書かざるを得ない。

ほとんどのプラグインは、仕様通りの挙動を実現できないにも関わらず、無理やり標準の構文を利用することを目的にしてしまっている。それによってもたらされる弊害は、標準の構文を利用できるという利点よりも大きいだろう。
ユーザーが標準の仕様を誤解してしまうことも考えられるし、コードが仕様通りに動かないことを理解するためにプラグインの知識が必要になることもある。

また、標準と言えども破棄されてしまう仕様もある。仕様を追い続けた上で、利用していた構文が使えなくなったと決まれば、既存のコードを書き直す必要もあるだろう。

だったら無理に未来の構文に憧れを抱くのではなく、プリプロセッサの独自構文に身を委ねてしまったほうが安心できるはずだ。幸いSassは市民権を得た当たり前の言語になった。極端なコードを書かない限り、Sassに依存していることは大きな問題にならない。プリプロセッサ無しでみんなが書きたいCSSを書けるようになるのは、どうせかなり先の未来だろうし。


蛇足ながら、妥当に利用できそうなプラグインについても考えた。基準は次のふたつ。

  • 未来の仕様と全く同じ挙動を実現できる(単に糖衣構文である)
  • 仕様自体が破棄される可能性が高くない(Working Draft以降)

これに基づいて次のプラグインを選択できた。

列挙した全てのプラグインがベースにしている仕様は、現在Working Draftだ。慎重に考えるとどのプラグインも利用すべきではないのかもしれない。とは言え、僕にも未来の構文を使いたいという願望はある。ちょっとした個人プロジェクトでは利用してみたい。

日本語向けフォントスタックの現状

日本語のウェブサイト向けのフォントスタックの現状と無難な設定についてまとめた。sans-serifserifsystem-uiのそれぞれの総称フォントファミリーに基づいて、主要な端末(WindowsMaciOSAndroid)のフォントの搭載状況を整理する。

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 ProNHiragino Sansにそのまま置き換えると違うウェイトになることがあるということ。CSSfont-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 SansOS 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-uisans-serifなどと同じ総称フォントファミリーだ。CSS Fonts Module Level 4で追加された。プラットフォームのUIと同じフォントを利用できる。現状ではChromeのみで実装されており、その他のブラウザのためにはフォールバックを書く必要がある。
フォールバックを含めた指定方法はいろいろ紹介されているが、英語圏のブログに書かれているような指定は日本語向きではなさそうだ。

Chromesystem-uiを指定した場合、MaciOSではSan Franciscoになる(ヒラギノ角ゴシックは含まれていない)。Windows 10ではYu Gothic UIで、Windows 8.1以前ではSegoe UIMeiryoの混植。AndroidではRobotoNotoの混植。
ブラウザをまたいでもこれと同じ結果にするためには、次のように指定する。

$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 UIMeiryoの混植になる。
Android Chromeは1の指定でRobotoNotoの混植になる。

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要素の属性などを通して公開している。フォーカスのスタイルはそれに基づいて設定すればいい。これを利用すれば、意図した通りにフォーカスのスタイルを機能させることができた。

提供される状態は、initialmousekeyboardtouchのいずれかだ。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もどきなことができる。これがイマイチだったので代替手段を紹介しました。

キーボードでの操作性を確保しつつ、マウスユーザーには不要なアウトラインを表示しない。見た目の良さかアクセシビリティか、という二者択一にならない現実的な妥協点だと思う。積極的に利用していきたい。

参考

見やすいスタイル  |  Web  |  Google Developers

ページ内リンクへの移動時にスクロール位置が固定ヘッダーと被らないようにする方法

固定ヘッダーがあるサイトだと、ページ内リンクをクリックしたときに対象の要素が固定ヘッダーと被ってしまうことがある。

f:id:yuheiy:20170816184501g:plain

固定ヘッダーがあるサイトの例として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/になる。

この辺は、HugoJekyllあたりを参考にした。

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など、よくある要件の解決策もガイドとして含めた。プロジェクトコードを管理する際の面倒くささはかなり解決できたと思う。


仕事をはじめてからずっとめんどいって思ってた問題は結構解決できた。これからは開発環境じゃなくて、ウェブサイトの作り方自体を改善していきたい。

yuheiy/real-world-website-boilerplate

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としてはこの形が正しいのだと納得できた。いや想像なんだけど。

そう考えると、例外はbody { margin: 0; }のみだと言える。その他は全てユーザーエージェントをまたいでもスタイルを統一させるための宣言だ。Normalizeとして正しい。

自明なのかもしれないし、どこかに言及が有るのかもしれないけど、忘れそうなので書いといた。

そもそも、ベースになるCSSをなぜここまで深く考える必要があるのかというと、CSSにおいて最初に採用したベースは二度と変更できないケースがほとんどだからだ。なぜならスコープが大き過ぎて影響範囲が予想できないから。この問題はおそらく、Shadow DOMを採用できる時代が来て、真にScopedなスタイルを実現できるようになれば解決の糸口は見える。最長でも10年くらい辛抱すればどうにかなるはずだと思ってる。