シンプルなレスポンシブデザインを実現するためのメディアクエリの考え方
スタイルはシンプルなものから宣言していく方が無駄がない。
レスポンシブデザインにおいては、小さい画面のほうがより簡素な画面構成であることが多い。そのため、小さい画面から順に大きい画面まで設計したほうが理に適っている。例えば以下のようなコードになる。
$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)
など)よりも、適用されるスタイルが把握しやすくて簡単になる。
この場合、小さい画面のほうがスタイルが複雑な要素(ハンバーガーメニューなど)があっても、メディアクエリの書き方は一貫させたほうが良い。むしろあまりにそういったケースが多いのであれば、デザインが間違っていることを疑うべきだ。
カンプを作るときや、コーダーとのやりとりをするときにも、この考え方を知っている前提だとうまくいく。幅480px
未満ではこれ、幅480px
以上で幅960px
未満のときはこうなる、という風にできると円滑になる。
縦書きでレスポンシブなブログを作った
趣味で会社の人のサイトを作った。縦書きでレスポンシブなブログ。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;
デバイスを意識させない命名にした上で、適切な位置や間隔でブレイクポイントを配置することを意識している。これらは、コンテンツの見た目が切り替わるブレイクポイントとして汎用性の高い値だと思っている。
最終的にやってることは既存の記事と同じようになったけど、そもそもなぜこうするのかを理解した前提でするべき話だと思ったのでこれを書いた。