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年くらい辛抱すればどうにかなるはずだと思ってる。

Re: なぜピクセルパーフェクトは筋が悪いのか

昨日書いた記事はあまりに雑だった。それに対するつぶやきを見て、思考が整理されたので改めて書く。

ピクセルパーフェクトが正しくないのは、カンプの時点ではまだデザインは未完成であるからだ。つまりは、未完成なデザインをそのまま形にすることを強制していることになる。

GUIのデザインツールとCSSは、それぞれ別のアプローチでデザインを表現できる手段だと言える。

GUIは自由に発想を展開して試行錯誤するためのプレイグラウンドだ。あえて制約を緩めることによって、さまざまな形を実験する効率を高めている。反して、実際の媒体の制約と照らし合わせると現実性の無いものもできる。制約は意識しにくいし、ルールで縛りたい場合には向いてない。そのための仕組みが弱いからだ。

CSSはルールを適応するためのツールだ。指定した要素に対して一律同じスタイルを当てることができる。デザインを構成するルールは論理的な単位で分解して再利用できる。反面、デザインが理路整然としていないとCSSは難しくなる。痛みに最も敏感になれる場所だとも言える。例外に弱い。

良いデザインを作るためには、たくさんの試行錯誤と一貫したルールが必要だ。それを実現するためには、互いを組み合わせてデザインするのが最も強い。

GUIツールを使って作られたカンプを基にCSSを書くことも立派なデザインの過程である。カンプを作ることとはまた別の視点での。それら一通りを同じ意志を持った人が行うことで、初めてひとつのデザインが完成する。

分業は大きな壁だ。いわばデザインの途中段階で、その完成を他人に委ねてしまうことになる。うまく進めるには可能な限り思考を同期させるしかない。

けど、正しい答えにたどり着くための、文脈を理解する手がかりや、そこで見つけた間違いをフィードバックするための仕組みは全く未熟なままだ。ウェブにおいて差し迫った問題として活発に議論されているが、それでもまだまだ重要性が認識されていないことがほとんどだ。

問題を解決するための答えはそれぞれの場所に必ずある。互いのメンタルモデルを深く理解することが、壁を取り払って本質的な問題に目を向けるための第一歩になる。

なぜピクセルパーフェクトは筋が悪いのか

  • カンプは実装するために作る中間成果物
  • 実装時にはスタイルにブレがあると都合が悪い。デザインとしても破綻している
  • 余白やフォントサイズのルールなど、デザインツールでは一律再現するのが難しい
  • スタイルシートという概念を持ってすることで、デザインツールでは不可能な一貫性を表現できる
  • あくまでカンプはデザインの印象を伝えるだけのもの。細かいルールのブレはCSSを書くときに正規化すべき

後日続きを書いた

シンプルなレスポンシブデザインを実現するためのメディアクエリの考え方

スタイルはシンプルなものから宣言していく方が無駄がない。

レスポンシブデザインにおいては、小さい画面のほうがより簡素な画面構成であることが多い。そのため、小さい画面から順に大きい画面まで設計したほうが理に適っている。例えば以下のようなコードになる。

$breakpoint-small: 480px;
$breakpoint-medium: 960px;

.component {
  margin: 1rem;
  padding: 1rem;
  border: 1px solid lightgray;

  @media (min-width: $breakpoint-small) {
    margin-bottom: 3rem;
  }

  @media (min-width: $breakpoint-medium) {
    font-size: larger;
  }
}

このような実装にすることで、小さい画面のスタイルは引き継ぎつつ、上書きする必要があるプロパティだけ宣言すればよくなる。複数のクエリを組み合わせる((min-width: 576px) and (max-width: 767px)など)よりも、適用されるスタイルが把握しやすくて簡単になる。

f:id:yuheiy:20170605185018p:plain

この場合、小さい画面のほうがスタイルが複雑な要素(ハンバーガーメニューなど)があっても、メディアクエリの書き方は一貫させたほうが良い。むしろあまりにそういったケースが多いのであれば、デザインが間違っていることを疑うべきだ。

カンプを作るときや、コーダーとのやりとりをするときにも、この考え方を知っている前提だとうまくいく。幅480px未満ではこれ、幅480px以上で幅960px未満のときはこうなる、という風にできると円滑になる。