縦書きでレスポンシブなブログを作った
趣味で会社の人のサイトを作った。縦書きでレスポンシブなブログ。prismic.ioとNext.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の人たちがみんなそこに向かってるのはどうなんだろう……。
ということで結構楽しかったです。ソースとかここです。
ウェブサイトにおけるタイポグラフィのパターンの設計
ウェブサイトのタイポグラフィのルールは一貫性を欠いてしまいがちだ。
単純にページやコンポーネント単位でデザインしていると、その周辺の要素だけで判断することになるので、全体として見ると一貫性を失ってしまう。
僕が考える解決策は、コンポーネントからは独立したタイポグラフィだけを定義するルールセットを作ること。そして全てのコンポーネントは必ずいずれかのルールセットに従うようにする。
@mixin typography-heading { font-size: 2rem; line-height: 1.3; } .article-heading { @include typography-heading; }
ルールセットは必ずセマンティックな(意味のある)命名をする。見た目に依存しない命名だと、デザイン変更のたびに大幅にコードを変更しなくて済むことが多い。そして、情報として同じ意味を持つ要素は、タイポグラフィも一貫させるという意図を強調させるためだ。
これに加えて、反復の原則をより意識すると、ルールセット内で利用する値はデザイン全体で共通のものにしたい。これをコードで表現すると、あらかじめ利用する可能性のある値は全て定義しておいた上で、ルールセットはその値を参照するだけになる。
$breakpoint-small: 480px; $font-size-root: 16px; // browser default $font-size: 1rem; $font-size-x-small: (12px / $font-size-root * 1rem); $font-size-small: (14px / $font-size-root * 1rem); $font-size-large: (24px / $font-size-root * 1rem); $font-size-x-large: (32px / $font-size-root * 1rem); $font-size-xx-large: (48px / $font-size-root * 1rem); $font-size-xxx-large: (64px / $font-size-root * 1rem); $font-size-xxxx-large: (96px / $font-size-root * 1rem); $font-size-xxxxx-large: (128px / $font-size-root * 1rem); $line-height: 1.7; $line-height-small: 1.3;
この段階で定義する値は、意味を持たない単なるサイズでしかないため、基準になる値からの相対的な命名にする。これらの値は自身がどこから参照されるかには興味がない。
line-height
のバリエーションはこれくらいシンプルでも問題ないと思っている。Vertical Rhythmを意識するといろいろあるけど、設計の単純化のためには考えない方がいい。
@mixin typography-heading { font-size: $font-size-x-large; line-height: $line-height-small; } @mixin typography-sub-heading { font-size: $font-size-large; line-height: $line-height-small; } @mixin typography-lead { @media (min-width: $breakpoint-small) { font-size: $font-size-large; } } @mixin typography-caption { font-size: $font-size-small; } @mixin typography-button { font-size: $font-size-small; line-height: $line-height-small; } @mixin typography-display { font-size: $font-size-x-large; line-height: $line-height-small; @media (min-width: $breakpoint-small) { font-size: $font-size-xx-large; } }
先述したように、これらはセマンティックな命名をした上で、単に先ほど定義した値を参照するだけ。メディアクエリによるスタイルの変更は、できればやらない方が設計としては単純になる。が、これはさすがに避けられないのでこの層に書く。
html { line-height: $line-height; } .article-heading { @include typography-heading; } .article-body { // inherit from root } .article-footer { @include typography-caption; } .article-more-link { @include typography-button; } .page-heading { @include typography-display; } .page-summary { @include typography-lead; }
実際のコンポーネントからは、先ほど定義したmixin
をinclude
するだけになる。ルートからfont-size
とline-height
をそのまま継承して使えるものには何もしない。
当然これで答えだと思ってないので、みなさんのいろいろな意見を聞かせてください。
font-familyに指定するフォントスタックの一般的なパターン
font-family
のベストな書き方20XX年みたいな記事が毎年出るけど、実際のところケースバイケースで、唯一のベストな解があるというわけでない。なので個人的な理解に基づいたパターンの分類をメモとして書いておく。特に目新しいことはない。
日本語環境で、ウェブフォントは考慮しないという前提。
和文フォントのみ
html { font-family: "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; }
たぶん最も無難。
游ゴシックはいろいろあって不安なので、特に寿命が長いウェブサイトでは採用したくない。次のバージョンのOSでは状況が変わったりしそうだから。
メイリオはWindows環境で利用するため。MS Pゴシックは基本的には絶対悪で、アンチエイリアスがかからず可読性が悪いので避けたい。すると和文フォントはメイリオ択一になる。Windows環境のブラウザでは、sans-serif
のデフォルトがメイリオになりつつあるんだけど、まだまちまちなので明示的に指定してあげないといけない。
ヒラギノはMacとiOSのデフォルトだけど、メイリオより高い優先順位にするために指定する。
メイリオが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-height
を4px
の倍数にする例。
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-size
とline-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 ウェブサイト
地味な土台の部分をきちんと作った上に、いわゆるエモさみたいなものが積み上げられた形のウェブサイトを作りたいなというのをずっと思ってる。
地味な土台というのは、アクセシビリティやパフォーマンス、サイト全体を通してのスタイルの一貫性など、ウェブサイトを作るために普通に意識すべきことのことを指している。
というのも、最近のウェブサイトを作るときの意識って、気持ちいいアニメーションがあって、インタラクティブに動いて、見た人が魅了されるみたいなエモさを追求することに向かい過ぎてる。その結果、当たり前にやるべきはずのことが蔑ろにされている。しかし、エモさはあくまで付加価値であって、それ自体を最優先で考えるのはウェブサイトの目的を見失っていることに他ならないと感じる。
なんでその目に見えない地味な部分を見た目にわかりやすいエモい部分より優先しないといけないのかというと、それが他のメディアにはないウェブの強みを活かすために必要な要素だからだ。ウェブは普遍的であることで発展してきた。誰もがどこからもアクセスできて、いつでもちゃんと使える。これはごく当たり前のことに聞こえるけど、多くはそれを意識できてないか無視してる。
反して、ウェブの利点を犠牲にしてまでウェブサイトのエモさを追求したところで他のメディアには敵わない。縦横比が固定された動画や、加えてプラットフォームが限定されたゲームなどと違って、多様なユーザーエージェントに対応するためには点で見たときの見た目や体験だけをあてにできない。だから、発想の柔軟性は制限されているし、コストも掛かり過ぎる。
ユーザーの多様性を認めることがウェブの良さを最も素直に発揮できる方法であって、全てのウェブサイトはその前提の下で設計されるべきだ。
とは言いつつも、僕はウェブサイトにおけるエモさを否定はしてないし、それを取り入れること自体は良いことだと思ってる。けど、そのために別の環境やユーザーからはアクセスできない、使えないようになってしまうなら採用すべきでない。
ウェブサイトを制作するために必要なことをピラミッドで表すと、地味な土台の上にエモさは位置している。下の層から順に達成できている状態で、その先でエモさを作り出すことに取り組めれば最も理想的。そんな優先順位でウェブ制作に取り組むことが当たり前になれば、みんなにとって魅力的なウェブにしていけると僕は信じてます。