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

追記
IE以外の主要ブラウザでは scroll-margin-top が利用できる


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

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未満のときはこうなる、という風にできると円滑になる。

縦書きでレスポンシブなブログを作った

趣味で会社の人のサイトを作った。縦書きでレスポンシブなブログ。prismic.ioNext.jsで作った。

ウェブデザインに縦書きを活かすことは難しい。部分的に取り入れることはできても、縦書きの文章を主要な要素として扱うのはかなり難がある。というのも、ウェブサイトは縦にスクロールするのが当たり前だけど、普通に縦書きで実装すると横スクロールになるからだ。

横書きでは文章は上から下に流れ、ページは縦スクロールになる。対して縦書きの場合、文章は右から左に流れるため、横スクロールになる。スクロール操作が不自然だと目に見えてユーザービリティが低下するので、どうしても当たり前のスクロールができるようにしたい。

幸い、縦書きにしながら縦スクロールにする方法はひとつある。新聞や雑誌のように段組にすることだ。

それはcolumnsを利用すれば、一見簡単にできそうな感じはする。けど、僕が望む仕様を実現しようとすると案外難航した。僕の今回の実装は次のような仕様になっている。

  • 縦書き
  • 段が最初の右端から左端、あふれたら次の段からという風に順番に埋められる
  • 文章の量には依存しない
  • レスポンシブ

columnsは、全ての段の高さを揃えようとする。そのため、最初の段から順番にブロックの中を埋めていくことはできない。ハック的に解決するしかなかった。

.ArticleBody {
  width: 100%;
  max-width: 100%;
  height: 14rem;
  writing-mode: vertical-rl;
  columns: 14rem;
}

高さを固定することで、最初の段からはみ出そうになるまで別の段にテキストが流れることはなくなる。はみ出した分は次の段へ移り、おおよそ望む形になる。

ただそうすると、要素自体の高さは一段分にしかならない。これでは次に続く要素がこの要素と重なってしまう。JavaScriptで要素の高さ分のpaddingを設定することで解決した。

const adjustSize = el => {
  const fullHeight = el.scrollHeight
  const defaultHeight = Number(getComputedStyle(el).height.replace('px', ''))
  el.style.paddingBottom = `${fullHeight - defaultHeight}px`
}

scrollHeightは要素のスクロール分も含めた高さを返す。正直これまでscrollHeightの存在を知らなかったんだけど、コンソールに勘で$0.scro...とか打ってたら候補で出てきたのでびっくりした。

この段組の仕組み自体はIE11を含むクロスブラウザで動作する(このサイトでは対応してない)。ただ、Firefoxの挙動が壊れていて、デベロッパーツールでスタイルを付け外しすると直るって感じだった。今回はsetTimeoutで擬似的に再現した。

ついでに言うと、Firefoxは縦書き周りのバグがかなり厳しくて、今回作ったサイトではFirefoxだけ別のデザインにした部分がある。あと、CSS VariablesとかLogical Propertiesを使いたかったので、IEとEdgeは無視した。

この段組の実装は結構珍しいアイデアな気がしてて、さっき縦書きアワードのやつ一通りみたけどやってる人がいなかったので世界初の可能性がある。なんか金とか欲しい。

Firefoxの実装が直ったら仕事でも使えなくは無いかな、という感じだと思う。ちょっと怖いけど。

ちなみにcolumn-fill: autoではやりたいようにならなかった。


裏側の仕組みもちょっと遊んでみてる。

このブログの記事は僕が書いていくという訳ではない。なのでGitにマークダウンのファイル作るとかじゃなくて、CMSで管理したいということだった。

まともにWordpressとかでCMS環境を作るのはつらいしやりたくない。データベースとか勝手に管理してくれて、REST APIだけ提供してくれるようなのないかなーと思ってたら、案外いろいろあるらしい。なんとなくprismic.ioというやつを選んだ。

使い方は、カスタムフィールドみたいなのをJSONで定義して(GUIもある)、後はそれに合わせたフォームを提供してくれるのでデータを追加していくだけ。基本的にはいい感じなんだけど、フォームにIME周りのバグが結構あって、使えても社内ツールくらいかなと思った。

他にもTumblrとかGoogle Spread Sheetで管理するのも試したけど、これの優位点は自由にフォーマットを決められて画像の管理もしてくれるところだった。

CMSの更新時にはWebhookを飛ばしてくれるので、そのときにCIでビルドしてデプロイしたら静的サイトとして作れる。けど、ビルド環境作るのがだるくてさぼった。代わりにNext.jsを使った。

Next.jsはReactでIsomorphicなウェブアプリを作るためのフレームワーク。ビルド環境とかルーティングの仕組みとかが組み込まれてて、驚異的に楽。ただ、CSS in JSが前提なので普通のCSSを書く環境作るのがめんどくさかったのは良くなかった。CSS in JSの類のものがCSSの根本的な難しさを解決するとは思わない。SPAの人たちがみんなそこに向かってるのはどうなんだろう……。


ということで結構楽しかったです。ソースとかここです。