ウェブアプリケーション開発における、現代的なCSSの基礎技術についてまとめました。
ちまたには「CSSとは何か」を学ぶ教材はたくさんあっても、「CSSをどうやってうまく使うか」についてはあまり詳しく触れられません。
仕様をたくさん記憶したところで、いつになっても開発力はあがらないのです。
本記事は「CSSをうまく使う技術」に焦点をあてて、実際に現代的なウェブアプリケーションに求められるレベルのCSSを書くための知識を紹介します。
特に
- プログラミング経験はあるもののウェブフロントエンドの経験が浅い方
- 初級レベルのCSSはある程度理解したものの、次にどうしたらいいかわからない方
にお勧めです。
プロローグ
CSSの書き方は一通りではありません。
好きな書き方を自由に選ぶことができます。
これは一見すると良いことですが、裏を返すと最適ではない書き方がたくさんあるということです。
この場において最適な書き方がどれか、CSSを書く側に判断を委ねているのです。
つまり、深い洞察力と思考力を持った者が、さらに重厚な知識を備えなければ、ひどいものを大量生産できてしまいます。
ひどいものかどうかは、それを見ただけでは当人に判断できません。
どうしたらより良くなるのかも、もちろん簡単にはわかりません。
世界は自分の洞察力と思考力の範囲でしか知覚できないからです。
世界を広げるには、視野を広げることです。
新しい考え方や知識が、ものを見る目を養うのです。
しかし、CSSの仕様を記憶したところで世界は広がりません。
「CSSが何であるか」という知識は「CSSをどう使うか」には直結しないからです。
私自身も、私の能力の範囲内でしか世の中を見ることができないため、ひどいものを生産している可能性はあります。
しかし、自分が生産したものがひどいものであったことに気づくという経験を何十回も繰り返してきました。
まだまだ最高ではありませんが、その過程で得られたものにもそれなりに価値はあるはずです。
ここに書くことは必ずしも最高の方法ではないですし、半年後に私自身が否定するかもしれません。
「そういう考え方もあるかぁ でも実際どうなんだろう」と、思索の起点に使っていただけると幸いです。
優秀な誰かしらがそうやってご自身で進化するためのきっかけくらいにはなるのではと思い、ここにまとめます。
ここまで読んで「概念的な話ばっかりだな」と思った方も多いと思います。
でも、具体的なテクニックはすぐに陳腐化するし、状況によって最高にも最低にもなります。
そして具体的なテクニックばかり追い求めても、自ら世の中に新しい価値をつくりだすことはできません。
一方でひとたび概念的な本質が分かれば、具体的なテクニックなんていくらでもそこから導出・発明できるものです。
対象とする領域
さて、一口にCSSと言っても、実はその対象領域は大きく2つに分けることができます。
- ウェブページ: 主に情報を伝えるための1枚のページで、いわゆるLP(ランディングページ)などが含まれます
- ウェブアプリケーション: ウェブ技術を利用したアプリケーションで、管理画面やSNSなど、ユーザーの操作に応じて複雑な状態をあつかうものです
ウェブページの場合は、毎回つくり捨てることも可能です。保守や拡張の必要性がそこまで高くありません。
一方でウェブアプリケーションは、将来的な拡張まで視野に入れて作成する必要があり、高い頻度で改修できることが求められます。
このように、ウェブページとウェブアプリケーションでは、求められる要件が大きく異なります。
ゆえにどちらを対象にするのかによって、CSSの使いかたも大きく異なってくるのです。
どちらを対象にするのかを明言しないような記事や本は「CSSの使いかた」という点ではあまり参考にならないと考えて良いでしょう。
本記事ではウェブアプリケーションを主な対象とします。
対象とするブラウザー
世の中にはサポートが終わったIE10などを「のっぴきならない事情」で使い続ける企業がいます。
それが理由で古いブラウザーをサポートするウェブアプリケーションも存在するようです。
しかし、そういった企業向けのウェブアプリケーションは、先に述べた「ウェブアプリケーション」の定義とは性質が異なります。
文字を左寄せから中央寄せにするために改修費として数百万円もらえる世界であり、保守性があまり重要にならないからです。
もしも、お金も払わないうえに古いブラウザーのサポートを求めてこられることがあったら、全力で逃げましょう。
どうせそいつらは何かあったときに全責任をあなたに押し付けてきます。
ということで、この記事では現在サポートされている主要なブラウザーを対象とします。
なお、IE11についてはすでに切り捨てているアプリケーションもありますが、ここで紹介する内容はIE11も対象としています。
現代的な使いかたの基礎
まずはCSSの使いかたに関する基礎の部分を述べます。
これらはウェブページにおいても共通する部分がほとんどです。
あくまでも基本なので、本記事を読み進めるとこの基本に従わないところが次々にでてきます。
「こんな当然のこと知っとるわい😠」という方はさくぅっと流し読みしてレスポンシブな表示を支える技術に進んでください。
さて、実はこの記事を書いているのはバーチャルヤギチューバーのさくらちゃんだよぉぉおおおお!
ぶめぇええええええ🐐
さくらちゃんはかしこいので、いきなり正体を明かすと「なんやこれ意味わからん」と離脱しちゃう人もいると思って、ホモ・サピエンスの知的レベルに合わせた書き出しにしたんだよぉ🐐
でもここからはところどころ本来のさくらちゃんが顔を出しちゃうよぉ🐐
文章構造と表示方法の分離
「CSSの使いかた」と言いながら、いきなりCSSと関係ない話をします。
Googleドキュメントで次のような文章を作っていると仮定してください。
Microsoft Wordの方が想像しやすければそちらでも構いません。
タイトルぶめぇ🐐
見出しぶめぇ🐐
小見出しぶめぇ🐐
本文ぶめぇ🐐本文ぶめぇ🐐
小見出しぶめぇ🐐
本文ぶめぇ🐐本文ぶめぇ🐐
見出しぶめぇ🐐
小見出しぶめぇ🐐
本文ぶめぇ🐐本文ぶめぇ🐐
小見出しぶめぇ🐐
本文ぶめぇ🐐本文ぶめぇ🐐
見出しぶめぇ🐐
小見出しぶめぇ🐐
本文ぶめぇ🐐本文ぶめぇ🐐
小見出しぶめぇ🐐
本文ぶめぇ🐐本文ぶめぇ🐐
とてつもなく読みにくいです。
ぐっちゃぐちゃです。
ちゃんとメリハリつけないと上司に怒られてしまいます!
「怒られちゃうから」っていう小学生みたいな理由で仕事をする人は、あまり仕事ができません。
1行1行、丁寧な手作業でフォントサイズや文字の太さを指定していくものです。
一方、小学生みたいな社員の上司というものは、自分の狭い見識が世界の常識だと思いこんでいるものです。
「見出しの背景色は黄色にするのが常識だろうが! お前はそんな常識も知らんのかぁ!」
...
...
また1行1行、丁寧な手作業で背景色を付けていくことになりました。
見出しが100個あったら100個分作業しなくてはなりません。
このような面倒が生じるのは、文章構造と表示方法の2つの概念を分けられていないからです。
これをきっちり分離できている人は、例えばGoogleドキュメントであれば「段落スタイル」の機能を使います。
「段落スタイル」機能を使って、各段落が「標準テキスト」なのか「タイトル」なのか「見出し1」なのか「見出し2」なのか指定しておくのです。
そのうえで、「標準テキスト」の表示方法、「タイトル」の表示方法……と設定していきます。
スタイルを指定するHTMLタグを使わない
HTMLでも同じことが言えます。
いにしえの技術を知っている方は、<font>
タグを使っているかもしれません。
<font color="red">ここ大事</font>
こんなものはすでに非推奨になっています。
理由は、文章構造をあらわすためのHTMLの中に、表示方法に関する情報が入り込んでいるからです。
インラインにスタイルを書かない
さらに、インラインスタイルと呼ばれるものも同様に使わないのが基本です。
<h1 style="font-size:26px; color:red;">大事な見出し</h1>
文章構造と表示方法を分けて記述するために、独立したCSSファイルを用意し、<link>
タグで読み込むのが現代的なCSSの書き方の基本です。
クラスを通してスタイルをあてる
CSSの例としてよく挙げられるのが以下のようなコードです。
h1 {
font-size: 24px;
font-weight: bold;
background-color: yellow;
border-bottom: dashed 2px black
}
h1
タグのスタイルを定義しています。
実は、これは基本的に使わない書き方です。
がんばってなんかいろんな「セレクター」を覚えた方もいるかもしれません。
それもあまり使いません。
「せっかく覚えたから」と、無理にいろいろなセレクターを使うとCSSの詳細度が複雑になります。
その結果できあがるのが、!important
だらけのCSSファイルです。
このように、1つのことにたくさんの選択肢があると、あまり良い結果にはなりません。
ではどうしたらいいのか。自らに制約をかけて最小限の機能しか使わないようにするのです。
全ての仕様を理解する必要もなければ、使い切る必要もないのです。
CSSのセレクターについて言えば、クラスのセレクターだけを使います。
クラスを通してスタイルをあてる例
クラスのセレクターを使う例として、MDNのCSS入門から題材をお借りします。
まずは、以下のようにHTML側でスタイルをあてたい要素にクラス名をつけます。
<ul>
<li>Item one</li>
<li class="special">二つ目の項目</li>
<li><em>三つ目</em>の項目</li>
</ul>
そのうえでCSSファイル側に、そのクラス名をセレクターで指定してスタイルを記述するだけです。
.special {
color: orange;
font-weight: bold;
}
さて、MDNのCSS入門のページは「CSSとは何か」の入門なので、以下のような例も紹介しています。
li.special {
color: orange;
font-weight: bold;
}
タグ名がli
でspecial
というクラスを持った要素に限定して表示方法を指定しています。
しかし、タグ名は表示方法ではなく、文章構造を管轄するものです。
タグ名を使って表示方法の対象を絞り込むこのようなやり方はオススメしません。
表示方法を意味するクラス名を使わない
きっとこの記事の読者のみなさまの多くは賢明な方だと思います。
しかし、世の中にはメモをとるのが下手な人や、話の聞き方が下手くそな人がいます。
そういう人は、分かりやすい具体的な結果のみを知りたがるものです。
「話がなげぇなぁ。クラスにスタイルを当てろってことだろ??」
そんな困ったちゃんは以下のようなコードを書いてしまいます。
<h1 class="font-size-big color-red">大事な見出し</h1>
……はぁあ〜! 人間は愚かだね。さくらちゃんはため息がでちゃうよ🐐
この際だから言っておくけどね、ヤギさんもウシさんも、子どもを生まないとおっぱいはでないんだよ!
「文章構造と表示方法を分離させる」って何度も言ったじゃん。
具体的なテクニックばっかり学んで、本質を身に着けないと「入門レベルから次にいく方法が分からない ぴえん🥺」ってなっちゃうでしょ🐐
閑話休題。
「文章構造と表示方法を分離させる」の原則にしたがって、要素がページ上でどんな役割をしているのかをクラス名にしましょう。
<h1 class="page-title">大事な見出し</h1>
.page-title {
font-size: 26px;
color: red;
}
CSSフレームワークと呼ばれるものの中には、Bootstrapのようにこの原則に背いたものが多くあります。
賛否両論ありますので、それが許容できることなのかどうかは、この記事を最後まで読んだ上でご自身で判断してください。
名前の衝突を避ける
サイドメニューの要素にitem
というクラス名をつけるのはどうでしょうか?
これは、「どう表示するか」ではなく「どのような役割か」を示す名前です。
その意味では特に問題がないのですが、名前があまりにも一般的です。
例えば、本文中の箇条書き要素にも同じくitem
というクラス名をつけてしまう可能性があります。
箇条書き要素のために追加したスタイル定義が、サイドメニューにも適用されてしまいます。
これを避けるために、BEM記法などの面白みのない原始的な方法がよく使われています。
原始的ですが、余計なテクニックをこねくり回してかえって面倒な目にあうよりは、よっぽど素晴らしい手法です。
実際のところ、これを解決する手法がいくつか提案されているものの、まだまだ決定打に欠けています。
正しい使いかたを確認する
基礎知識の結びとして、信頼できる最新の情報を入手する方法について述べます。
CSSやHTMLは歴史が長く、古い情報や誤った情報がインターネット上にあふれています。
たとえば、読者の中にはもしかしたらいまだに <br />
とか <input type="text"></input>
のような古いHTMLを書いている人もいるかもしれません。
信頼できる最新の情報については、できるだけMDNの情報を参照するようにしましょう。
ググる時に「input text mdn」みたいにすればいいだけです。
レスポンシブな表示を支える技術
現代においては、スマホにタブレット、ノートPCから様々なディスプレイなど、様々なサイズの画面が存在します。
また、ただ文字を表示するだけの簡素なウェブページだった時代から、ウェブアプリケーションとして複雑なレイアウトが必要になりました。
このような背景から、現代的なウェブアプリケーション開発においては、表示崩れが大きな問題になります。
それを解決するのが、画面幅に応じてレイアウトを柔軟に変更するレスポンシブな画面設計です。1
ところで、さくらちゃんは数年前に「CSS一筋10年のベテランCSSコーダー」と名乗るオジサンと出会ったことがあります。
🤪「さくらチャン、こんにちは❗オジサン、ちゃんと、レスポンシブ対応も、できるよ😀😀😀」
🐐「ここの部分、環境によっては文字がはみ出てるぶめぇ...🐐」
🤪「そうか❗これはね、枠の幅を、6pxくらい、余裕を持たせればいいね😀😀😀オジサンの、お腹は、60pxくらい、減らせないかな❓ナンチャッテ😀」
🐐「さくらちゃんが知ってるレスポンシブとなんか違うやぎぃ...🐐」
こんな恥ずかしいオジサンにならないように、現代的な技術を学びましょう!
px単位に頼らない
サイズの指定といえば、px単位をまっさきに思い浮かべる方が多いと思います。
しかし、px単位を使うのは表示崩れの原因になるのでできるだけ控えるのが望ましいです。
代わりに使える単位が、em
rem
vw
vh
などの相対長と呼ばれる単位です。
実際に、昔ながらの人がpx
単位で指定するところを、さくらちゃんはほとんどem
で指定してしまいます。
唯一px
を使うところがあるとしたら、border-width
くらいです。
em
単位は親要素のフォントサイズを基準にしています。
そのため、例えばユーザーが「ちょっと目がつかれたなぁ」とブラウザーのフォントサイズを大きくしても崩れにくいレイアウトが実現できるのです。
flexbox
今どき、要素の横並べのために float
とか clearfix
のような、いにしえの技術を使っている人はもう少ないです。
あるいはposition: absolute
を使って「右に50pxずらす」のようなレスポンシブではない無理矢理な方法も、さすがに使われていないと思います。
代わりに、flexboxを使えば、柔軟に画面幅にあわせたレイアウトを実現できます。
少し習得するのに時間はかかりますが、その価値は十分にある技術です。
また、CSSグリッドレイアウトも強力なレイアウトツールです。
説明が容易ではない理由によって、さくらちゃん自身はウェブアプリケーションではほとんど使いませんが、十分に学ぶ価値のある技術だと思います。
状態に応じたスタイルを実現する技術
ウェブアプリケーションである以上、ページには状態が存在します。
例えば以下のような状態がありえます。
- どの項目が選択されているか
- エラーはないか
- データを送信中かどうか
ここでは、状態に応じて要素の表示方法を変える便利な方法を紹介します。
疑似クラス
擬似クラスを使うことで、JSに頼らなくてもCSSだけである程度完結することができます。
例えば、要素にフォーカスがあたっているかどうかを判定する:focus
擬似クラスを使えば、
mdnの例のように入力欄にフォーカスがあたっているときの見た目を変更することができます。
WAI-ARIA
擬似クラスでは対応しきれない場合にぜひ使っていただきたいのが、WAI-ARIA(ウェイ・アリア)です。
うぇいうぇい!🐐
WAI-ARIAはアクセシビリティ、つまり障害を持つ方に配慮した技術です。
たとえば目が見えない方はスクリーンリーダーなどを使ってウェブサイトを閲覧します。
スクリーンリーダーはHTMLの構造をもとに文章を読み上げてくれますが、CSSでdisplay: none
をあてた要素があったらどうなるでしょうか?
目が見える方にはその要素が隠されていることは明らかです。
しかし、スクリーンリーダーを通して「見て」いる方にはその要素はそこに確かに存在しているのです。
このような情報を伝えるために、WAI-ARIAという仕様に定義されたaria-hidden="true"
のような属性をHTMLにつけます。
そうは言っても、多くの方は「アクセシビリティに配慮するような暇はないからWAI-ARIAなんて後回しだ」と考えるでしょう。
マーケティング戦略としてはインパクトが小さい対応を後回しにするのも仕方ないことです。
世の中のキーボードが五本指の動物に向けたものばかりで、ヤギさんに優しくないのも、さくらちゃんは文句言いません!
しかたないもん...🐐
しかし、WAI-ARIAはもっと「不純」な目的で使っても良いのです。
アクセシビリティうんぬんは置いておいて、要素の状態をあらわす標準化された技術だと考えるとどうでしょうか?
そう、CSSと組み合わせて要素の状態に応じた表示方法を制御するのにぴったりなのです!
たとえば、「その他」を含む選択肢をもつフォームを考えてみましょう。
このフォームでは、「その他」を選んだときだけ、詳細を記入する入力欄が表示されます。
この入力欄が隠れていることを示すのが、その名の通りaria-hidden
属性です。
具体的には次のように実装します。
<label class="form_question" for="favoriteAnimal">好きな動物は?</label>
<div id="favoriteAnimal">
<select id="favoriteAnimal-main" class="form_question_controll form_question_controll-select">
<option value="goat" selected>ヤギが好き</option>
<option value="goat2">やっぱりヤギが好き</option>
<option value="goat3">とってもヤギが好き</option>
<option value="other">その他(正気か??)</option>
</select>
<input type="text" id="favoriteAnimal-detail" aria-hidden="true" class="form_question_controll form_question_controll-textInput">
</div>
<script src="./index.js"></script>
// `favoriteAnimal-main`というIDを持つ要素の値が変更されたとき...
document.getElementById("favoriteAnimal-main").addEventListener("change", function (eve) {
// その変更後の値が`"other"`なら...
if (eve.target.value === "other") {
// `favoriteAnimal-detail`というIDを持つ要素の`aria-hidden`属性を`"false"`に変更する
document.getElementById("favoriteAnimal-detail").setAttribute("aria-hidden", "false");
// その変更後の値が`"other"`ではないなら...
} else {
// `favoriteAnimal-detail`というIDを持つ要素の`aria-hidden`属性を`"true"`に変更する
document.getElementById("favoriteAnimal-detail").setAttribute("aria-hidden", "true");
}
}, false);
/* `form_question_controll`クラスを持つ属性が
* `aria-hidden`属性の値が`"true"`のときのみ...
*/
.form_question_controll[aria-hidden="true"] {
/* 表示しない */
display: none;
}
属性値を指定するCSSセレクターを使用して、aria-hidden
属性が"true"
のときにのみ表示を隠すようにしています。
なお、本記事の主題ではないので詳細は省きますが、aria-hidden
の値はJavaScriptで切り替えています。
このような簡単な処理はjQueryやVueなどのJavaScriptフレームワークを使わなくても簡単に実現できます。
カスタムデータ属性
さて、WAI-ARIAは標準化された仕様でした。
標準化されているということは、「ありがちな用途」にしか対応できないということです。
では、アプリケーション固有の状態をあらわしたいときにはどうしたらいいのでしょうか?
たとえば、デジタルカメラを模したアプリケーションを作っているとします。
多くのデジタルカメラには、シャッターの半押し機能が付いていて、半押しすると自動フォーカス機能が動作します。
その後、最後までシャッターボタンを押し切ることで写真を実際に撮影できるようになっています。
これを模して、
- 要素内でマウスのボタンを押し下げた状態がシャッターの半押し
- その後もとの状態に戻したらシャッターの全押し
に相当するようなウェブアプリケーションを作ってみましょう。
このサンプルアプリでは、フォーカス中(マウスのボタンを押し下げた状態)では枠線を緑色にします。
その後もとの状態に戻ったら、一瞬枠線を赤色にして、黒色に戻します。
これはどのように実現できるのでしょうか?
「カメラが自動フォーカス中です」なんていう具体的すぎるWAI-ARIAの仕様が存在するはずありません。
そこで使えるのがカスタムデータ属性です。
簡単に言えば、自分だけの独自の属性をつくる機能です。
MDNのページにあるとおり、属性名には大文字のアルファベットを使うことができません。
小文字のアルファベットをハイフンでつなぐのが無難な命名方法です。
カスタムデータ属性には、以下のように任意の値を渡すことができます。
<h1>Secret agents</h1>
<ul>
<li data-id="10784">Jason Walters, 003: Found dead in "A View to a Kill".</li>
<li data-id="97865">Alex Trevelyan, 006: Agent turned terrorist leader; James' nemesis in "Goldeneye".</li>
<li data-id="45732">James Bond, 007: The main man; shaken but not stirred.</li>
</ul>
これを使ってデジタルカメラアプリを作ってみましょう。
<div class="camera_view" data-shutter-status="init"></div>
<script src="./index.js"></script>
// `data-shutter-status`属性をもつ全ての要素について...
Array.prototype.slice.call(document.querySelectorAll("[data-shutter-status]") || []).forEach(function (target) {
// その要素でマウスのボタンが押し下げられた瞬間に...
target.addEventListener("mousedown", function () {
// `data-shutter-status`属性の値を`"focus"`(自動フォーカスモード)に変更する
target.setAttribute("data-shutter-status", "focus");
// (`target.dataset`で書き換えることもできますが、いろいろ特殊なのでここでは`setAttribute`を使っています。)
}, false);
// その要素で押し下げられたマウスのボタンが戻った瞬間に...
target.addEventListener("mouseup", function () {
// `data-shutter-status`属性の値を`"shooting"`(撮影中)に変更する
target.setAttribute("data-shutter-status", "shooting");
// その後 200ms 経ったところで...
window.setTimeout(function () {
// `data-shutter-status`属性の値を`"init"`(初期状態)に変更する
target.setAttribute("data-shutter-status", "init");
}, 200);
}, false);
});
/* 全状態で共通のスタイル */
.camera_view {
border: solid 10px;
width: 30em;
height: 20em;
}
/* 初期状態 */
.camera_view[data-shutter-status="init"] {
/* 枠線の色を黒色にする */
border-color: black;
}
/* 自動フォーカス状態 */
.camera_view[data-shutter-status="focus"] {
/* 枠線の色を緑色にする */
border-color: green;
}
/* 撮影中 */
.camera_view[data-shutter-status="shooting"] {
/* 枠線の色を赤色にする */
border-color: red;
}
WAI-ARIAの場合と同じように、CSSの属性セレクターでdata-shutter-status
の状態に応じたスタイルをあてています。
マルチブラウザー対応のための技術
ウェブアプリケーションを開発する上で避けて通れないのがマルチブラウザー対応です。
ざっと挙げても、以下の主要なブラウザーの、現在サポートされているバージョンに対応する必要があります。
- Chrome
- Edge
- Firefox
- Internet Explorer
- Opera
- Safari
- Android Webview
- Android版 Chrome
- Android版 Firefox
- Android版 Opera
- iOSの Safari
- Samsung Internet
ここでは、どのブラウザーでもできるだけ同じ見た目、同じ挙動を実現するテクニックをまとめます。
リセットCSSを使う
CSSによってスタイルを指定していないまっさらな状態の見た目は、ブラウザーごとに異なります。
この初期状態を定義しているのは UA stylesheet と呼ばれるCSSファイルです。
- Chromium UA stylesheet (Chrome, Opera)
- Mozilla UA stylesheet (Firefox)
- WebKit UA stylesheet (Safari)
これらの初期設定を上書きして、ブラウザーの初期状態をあわせるのが「リセットCSS」と呼ばれるものです。
いろんな種類のリセットCSSが存在しますが、特にこだわりがなければ以下の2つを参考にしてみてください。
- CSSリセットの先駆け Eric MeyerのCSS Reset
- 昨今のブラウザ事情を反映したModern CSS reset
box-sizingについて
リセットCSSで一番大切なことは、すべての要素に対して、box-sizing
プロパティの値をborder-box
に変更することです。
詳しい話はMDNの説明にゆだねますが、これを設定しないとCSSで設定した幅よりも実際に表示される幅が大きくなってしまいます。
特別なのっぴきならない事情がなければ、素直にbox-sizing: border-box;
と設定しましょう。
ノーマライズCSS・サニタイズCSSについて
リセットCSSは完全にタグの意味に関わらずあらゆるタグの見た目を同じように初期化します。
たとえばh1
タグとh2
タグの見た目を同じ大きさ・同じ太さで表示するのが一般的です。
それに対して、ノーマライズCSSやサニタイズCSSと呼ばれるものは、特に開発者が追加でスタイルをあてなくてもそれっぽく見えるように初期化してくれます。
たとえば、h1
タグはh2
タグよりも大きなフォントでより太く表示するように初期化します。
ノーマライズCSSやサニタイズCSSは簡素な文書を作成する際には便利です。
しかし、「HTMLタグと表示方法は別の概念」という原則にも違反しますし、特にウェブアプリケーションであればこのような配慮は逆にお節介になりがちです。
これは個人的な意見ではありますが、ウェブアプリケーション開発においてはリセットCSSの方が向いていると思います。
ベンダープレフィックス
一部のCSSプロパティは、ベンダープレフィックスをつける必要があります。
例えばtransition
プロパティを使う場合、以下のように書くことで、複数のブラウザーをサポートすることができます。
.target {
-webkit-transition: all 4s ease;
-moz-transition: all 4s ease;
-ms-transition: all 4s ease;
-o-transition: all 4s ease;
transition: all 4s ease;
}
新しめのCSSプロパティを使いたい場合は、MDNのページでブラウザーごとの対応状況をチェックし、適切なベンダープレフィックスを付与するようにしましょう。
……なんて言われてもこんなアホみたいな作業やりたくないですよね?
後ほど取り上げるコードの変換処理の項目で便利な方法をお伝えします。
flexbug-fixes
レスポンシブ対応でflexboxをお勧めしました。
実は、flexboxは比較的あたらしい技術なうえに大きな機能群であるため、一部のブラウザー実装にバグが存在します。
これらのバグへの対処方法がFlexbugsのページにまとまっているので、これらに目を通して注意深く実装しましょう。
……なんて言われたらflexboxなんて使いたくなくなりますよね?
後ほど取り上げるコードの変換処理の項目で便利な方法をお伝えします。
アニメーションを実現する技術
アニメーションを実装したいとき、ホモ・サピエンスは「アニメーションフレームワーク」をまず探しがちです。
でも待ってください。
下手にJavaScriptでアニメーションを制御するより、CSSを使ったほうが滑らかなアニメーションを簡単に実現できます。
transition
transition
プロパティは、要素の状態が変わったときのアニメーションを担当します。
詳細はMDNのページを見たほうが良いので、ここではカスタムデータ属性と組み合わせた例を紹介します。
先ほど取り上げたデジタルカメラアプリの例で、自動フォーカス中に枠線の色が徐々に変化するようにしてみます。
追加箇所は1行だけです。
/* 自動フォーカス状態 */
.camera_view[data-shutter-status="focus"] {
/* 枠線の色を緑色にする */
border-color: green;
/* 黒色から滑らかに変化させる */
transition: border-color 800ms ease-out;
}
これで、data-shutter-status
が"focus"
に変化した瞬間から徐々に枠色を変化させていくアニメーションが実現できます。
ただし、実際にはtransition
プロパティにベンダープレフィックスを付与する必要があることに注意してください。
animation
このtransition
を使ったアニメーションによって、あたかも自動フォーカスが焦点を探しているような雰囲気が醸し出されました。
さらに、フォーカスが完了したことを示すために800ms
後に枠線を2回光らせてみましょう。
具体的には以下のような挙動です。
-
800ms
待つ - 枠線の色を白色に変更する
-
100ms
待つ - 枠線の色を緑色に戻す
-
100ms
待つ - 枠線の色を白色に変更する
-
100ms
待つ - 枠線の色を緑色に戻す
これはtransition
プロパティでは実現が困難です。
transition
プロパティは「一方向」「一段階」のアニメーションにしか対応できないからです。
ここで使えるのがanimation
プロパティです。
これも詳細はMDNの説明にゆだねますが、今回の仕様を実現するには以下のようにtransition
をanimation
に置き換えます。
/* 自動フォーカス状態 */
.camera_view[data-shutter-status="focus"] {
/* 枠線の色を緑色にする */
border-color: green;
/* 黒色から滑らかに変化させる */
/* フォーカスが完了したら2回光らせる */
animation: 800ms ease-out 1 focus, 100ms ease-out 800ms 1 flush, 100ms ease-out 1000ms 1 flush;
}
...
...
@keyframes focus {
from { border-color: black; }
to { border-color: green; }
}
@keyframes flush {
from { border-color: white; }
to { border-color: green; }
}
ここでも、実際にはanimation
プロパティや@keyframes
にベンダープレフィックスを付与する必要があることに注意してください。
開発者ツール
主要なウェブブラウザーには「開発者ツール」「開発ツール」「検証ツール」などと呼ばれる機能が用意されています。
これを使うと、思ったとおりの表示にならない時に問題を解決したり、開発の効率を飛躍的に高めることができます。
細かい使いかたはブラウザーごとに異なりますが、大まかな使い勝手はだいたいどれも同じように設計されています。
一度MDNのドキュメントに目を通して全体像を把握しておきましょう。
CSSを書く際には、特にインスペクターをよく使います。
コードの変換処理
さて、ページの読み込み速度を高速にするためには、改行やスペースをできる限り取り除いてCSSファイルの容量を減らす工夫が必要です。
このような処理をminifyを呼び、JavaScriptのファイルなどにも使われるテクニックです。
一方で、そのようなCSSファイルは読み解くのも編集するのも難しくなってしまいます。
「ソースコード」として管理しやすいCSSファイルと、実際にブラウザーに読み込ませるのに使いたいCSSファイルとの間には、このような開きがあるのです。
そこで、現代的なウェブアプリケーションでは、CSSファイルに対してある種の変換処理をほどこすのが一般的です。
ここでは、ファイルのminifyを含め、便利な変換処理を取り上げます。
npm
コードの変換ツールなど、フロントエンドで使うツール類はnpm
コマンドでインストールするのが一般的です。
すでにJavaScriptフレームワークなどを使っている方には馴染み深いものだと思います。
さて、npmへの不満を解決するために作られたyarnというツールもあります。
ただ、のっぴきならない事情がない限りはnpmの使用をおすすめします。
理由は、さくらちゃんが一度yarnに乗り換えた上で、再度npmに戻ってきた過去があるからです。
これ以上にわかりやすくて信頼できる理由なんてないでしょ?🐐
どうしても詳しい理由を知りたい人は、補足のツイートをご参照ください。
もう1点、注意すべきポイントとして
「フロントエンド開発においては、できる限りapt
やyum
のようなOSが提供するパッケージマネージャーを使わない」
というのを覚えておいてください。
これはただのさくらちゃんの体験談であり、個人的な感想ですが、フロントエンドの環境構築で困ったときのほとんどはこれが原因でした。
npm
コマンド自体も、Node.jsの公式ページでNode.jsと一緒にバイナリをダウンロードしてパスを通しておくのがおすすめです。
npm
コマンドが使えるようになったら、ひとつプロジェクトのディレクトリを作成し、そのディレクトリ内で以下のコマンドを実行してください。
# 以下の`$`はターミナルに最初から表示されているものです。
# コピペするときは最初の`$`を除いてターミナルに貼り付けてください。
$ npm init
いろいろ質問されますが、全部y
(はい)と答えておいてください。
これで、package.json
というファイルが作成され、このディレクトリ内でnpm
を使う準備が整いました。
ターミナルを使ったことがない方は、この機会に最低限の知識を学習しましょう。
parcel
npm
コマンドが使えるようになったところで本題に入ります。
フロントエンド開発用のコード変換ツールにはいろいろありますが、まずはparcelを試してみるのがいいと思います。
parcelは「こまけぇこたぁいいから、いい感じにやっといてくれやぁ」という要望を叶えていい感じにやっておいてくれるツールです。
一方、その対極にあるのがwebpackです。
webpack.config.js
とかいう設定ファイルを使って思い通りに細か〜く動作をチューニングできます。
でも、何が一番正しいかなんていちいち勉強してたらいつになってもアプリケーションを作れないじゃないですか。
しかも、webpackを使っている人の多くは、既存の「俺が考えた最強のwebpack.config.js
」みたいなのを流用するんです。
だったら、大きなこだわりがある詳しい人じゃなければ、偉い人たちが考えたparcelの挙動にまかせておけば良いんです。
parcelのインストール
ということで、巨人の肩の上に乗って簡単に変換しちゃいましょう。
parcelの公式ドキュメントでは以下のコマンドでparcelをインストールするように書いてあります。
# これはお勧めしないインストール方法だよ!
# まだ実行しないでね!
$ npm install -g parcel-bundler
コメントしてあるとおり、さくらちゃんはこれをお勧めしません。
代わりにローカルインストールを好んで使います。
あと、宮崎完熟マンゴーもさくらちゃんは大好きです🐐
ふるさと納税したら食べさせてください。
公式サイトが採用している-g
オプション付きのインストール方法はグローバルインストールといって、システム全体で使うようにインストールします。
一方で、-g
オプション無しのインストールはローカルインストールと呼ばれ、そのプロジェクトでだけ使えるようにインストールします。
グローバルインストールの場合は、最初にインストールしたparcelが、どのプロジェクトでも共有されます。
そのため、一度インストールすればプロジェクトごとに都度インストールすることを省けます。
しかし、parcelのバージョンをプロジェクトごとに固定できません。
ローカルインストールの場合は、そのプロジェクトでインストールしたバージョンがpackage.json
に記録されます。
このpackage.json
を共有しさえすれば、別のパソコンでも同じバージョンのparcelをインストールできます。
package.json
を共有された人は、そのpackage.json
が置いてあるディレクトリで以下のコマンドを実行するだけです。
$ npm install
これだけで、package.json
に記録されているライブラリー群が、自動的にローカルインストールされるのです。
もちろん、バージョンもpackage.json
に記録されているものが選択されます。
別の開発者と一緒に開発するときや、パソコンを買い替えたとき、自宅と外出先のパソコンで共有するときなどに便利です。
これでもう、「うまく動かない? parcelのバージョン教えて?」みたいな会話とはおさらばです。
ということで、ここではローカルインストールを採用します。
$ npm install parcel-bundler
ローカルインストールされたコマンドはnpx
を頭につけることで呼び出せます。
たとえば、今インストールしたparcel
コマンドを使うには以下のようにします。
$ npx parcel --version
ビルド
リポジトリに用意したシンプルな例のようにsrc
ディレクトリ内に
index.html
index.js
index.css
の3つのファイルを設置した状態でビルドしてみましょう。
$ npx parcel build src/index.html
# 以下はターミナルに表示される内容だよ
# コピペしないでね!
✨ Built in 937ms.
dist/src.e1b9d829.js.map 1.68 KB 1ms
dist/src.e1b9d829.js 1.47 KB 100ms
dist/src.07131960.css.map 1.22 KB 2ms
dist/src.07131960.css 478 B 363ms
dist/index.html 425 B 528ms
dist
ディレクトリが作成され、その中にファイルが生成されているはずです。
まずは、dist/index.html
を確認してみましょう。
<!DOCTYPE html><html lang="ja"><head><meta content="IE=edge" http-equiv="X-UA-Compatible"><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"><meta content="width=device-width,initial-scale=1" name="viewport"><title>Camera App</title><link rel="stylesheet" href="/src.0ec720f5.css"></head><body> <div class="camera_view" data-shutter-status="init"></div> <script src="/src.e1b9d829.js"></script> </body></html>
しっかりminifyされています。
また、CSSの読み込み部分とJavaScriptの読み込み部分が以下のように変換されています。
<link rel="stylesheet" href="/src.0ec720f5.css">
<script src="/src.e1b9d829.js"></script>
href
とsrc
がオリジナルファイル名から、今回生成されたファイル名に変換されています。
これで、正しく変換後のファイルを読み込むことができます。
ブラウザーキャッシュへの対策
ところで、dist
ディレクトリに出力されたCSSファイルとJavaScriptファイルは、なぜ元のファイル名と同じではないのでしょうか。
index.html
だけ変換前のファイルと同じ名前なのは不思議ではありませんか?
実は、これはブラウザーのキャッシュに対する対策なのです。
ブラウザーには、読み込みを速くするために前回読み込んだCSSファイルとJavaScriptファイルを記憶するキャッシュ機能があります。
そのため、サーバー上のCSSファイルを変更しても手元のブラウザーに反映されないことがあります。
HTMLとCSSを同時に変更したら、CSSだけ古いものが読み込まれて表示崩れが起きてしまう可能性もあります。
これを防ぐための仕組みがこの変なファイル名なのです。
CSSファイルやJavaScriptファイルを変更してから再度ビルドすると、変更したファイルのファイル名が更新されます。
このとき、index.html
内でそのファイルを呼び出している部分もあわせて変更されます。
するとどうでしょうか。
ブラウザーにとっては見たことがない名前のファイルなのでキャッシュを使うことができません。
その結果として、CSSファイルを変更したら必ずブラウザーが読み込み直すことになるのです。
mapファイル
もう1つ気になるところがあります。
dist/src.07131960.css.map
のように.map
で終わるファイルが生成されていることです。
これはソースマップファイルと呼ばれるもので、変換前のコードと変換後のコードがどのように対応しているかを示すものです。
このファイルがあるおかげで、ウェブブラウザーの開発者ツールで元のソースを表示していろいろ試すことができます。
開発用サーバー
本題からずれますが、parcelには開発用サーバーを立ち上げる便利な機能があります。
以下のコマンドを実行して、ターミナルに表示されるURLをウェブブラウザーで開いてみてください。
$ npx parcel src/index.html
Server running at http://localhost:1234
✨ Built in 47ms.
この場合はhttp://localhost:1234
を開きます。
この状態でsrc
ディレクトリ内のファイルを書き換えると、即時に変更がウェブブラウザーに反映されます。
都度ビルドし直したり、ブラウザーをリロードする必要がないのです。
npmスクリプト
こちらも本題からずれた内容ですが、npmスクリプトと呼ばれるものを活用すると便利に開発を進められます。
まずはpackage.json
のscripts
というフィールドを以下のように、書き換えてみましょう。
{
...
"scripts": {
"build": "parcel build src/index.html",
"start": "parcel src/index.html"
},
...
}
以上の設定で、開発サーバーの起動やビルド作業が次のように行えるようになりました。
# ビルド
$ npm run build
# 開発サーバー
$ npm run start
# 開発サーバー(省略した呼び出し方)
$ npm start
これはコマンドが短くなるだけではなく、実際に実行するコマンドを隠蔽できるというメリットがあります。
ターミナルに打ち込むコマンドを変更することなく、実際に内部でやっていることを変更できるのです。
例えばparcelに新しいコマンドラインオプションを渡したいときもpackage.json
書き換えるだけです。
ターミナルではいつも通りにnpm run build
などとコマンドを打てば新しいコマンドが内部的に実行されます。
たとえparcelからwebpackに乗り換えたとしても、実際にターミナルで打ち込むコマンドを変えないで済むのです。
postcss
parcelができるのはこれだけではありません。
postcssという仕組みを併用すると、CSS向けに便利な処理を追加することができます。
併用方法の詳細はparcelの公式ドキュメントを参照いただくとして、ここでは具体的な利用例を紹介します。
リポジトリに用意した例のように、.postcssrc
というファイルと、.browserlistrc
というファイルを用意します。
{
"plugins": {
"autoprefixer": {},
"postcss-flexbugs-fixes": {}
}
}
last 2 versions
あとは、いつものようにnpm run build
を実行するだけです。
これだけで、以下の変換を行ってくれます。
-
autoprefixerが自動でベンダープレフィックスを付与
-
.browserlistrc
にしたがって、各種ブラウザーの最新の2バージョンに対応できるように付与
-
- postcss-flexbugs-fixesが自動的にflexboxのバグに対応したコードに変換
めちゃんこ便利!
SCSS
現代のウェブアプリケーション開発ではSassのSCSS記法がよく使われます。
SCSS記法はCSSを便利に拡張したものですが、そのままではブラウザーに理解されません。
そこで、parcelによってCSSに変換する必要があります。
parcelでCSSに変換する
parcelはSCSS記法にも対応しているので、以下の手順で簡単にSCSS記法を使えます。
-
index.css
のようなファイルがあったら、拡張子をscss
に変更してindex.scss
に変更します - HTMLファイル内で
index.css
などを読み込んでいるところを、上記に合わせてindex.scss
などに変更します
驚くべきことに、これだけでparcelがなんかいい感じに判断して、なんかいい感じに変換してくれるのです。
SCSSの使いかた
ここでも言えるのは、「全ての仕様を理解する必要もなければ、使い切る必要もない」です。
SCSSだけでも何本も記事が書けてしまうのでここでは実例をお見せすることにとどめますが、mixin
もpartial
もextend
も使わなくて良いのです。
さくらちゃんは、SCSSを使う本質的な理由は以下の2点だけだと考えています。
- 変数を別のファイルに書き出して一括管理できる
- 入れ子によって1つの要素に関する情報をまとめて管理しやすくなる
サンプルとして、テキスト入力欄やボタンなどの画面要素を並べたアプリケーションを作成しました。
<div class="styleGuide">
<div class="styleGuide_item">
<input type="text" class="formControl formControl-text" placeholder="Sample input">
</div>
<div class="styleGuide_item">
<button type="button" class="formButton formButton-main">main button</button>
</div>
<div class="styleGuide_item">
<button type="button" class="formButton formButton-mainInvert">main inverted button</button>
</div>
<div class="styleGuide_item">
<button type="button" class="formButton formButton-sub">sub button</button>
</div>
<div class="styleGuide_item">
<button type="button" class="formButton formButton-subInvert">sub inverted button</button>
</div>
</div>
/* Color */
$main-color0: #333;
$main-color1: #888;
$sub-color0: #599;
$sub-color1: #8cc;
$main-background0: #eee;
$main-background1: #ccc;
$sub-background0: #cee;
$sub-background1: #9cc;
/* Width */
$border-width: 2px;
$button-min-width: 6em;
$control-min-width: 10em;
/* Padding */
$button-padding: 1em;
$control-padding: 0.4em;
/* Other sizes */
$button-radius: 100vmax;
$control-radius: 0.6em;
@use 'variables' as *;
/* Form buttons */
.formButton {
border: $border-width solid;
border-radius: $button-radius;
padding: $button-padding;
min-width: $button-min-width;
cursor: pointer;
font-weight: bold;
&-main {
border-color: $main-color0;
background-color: $main-background0;
color: $main-color0;
}
&-mainInvert {
border-color: $main-color0;
background-color: $main-color0;
color: $main-background0;
}
&-sub {
border-color: $sub-color0;
background-color: $sub-background0;
color: $sub-color0;
}
&-subInvert {
border-color: $sub-color0;
background-color: $sub-color0;
color: $sub-background0;
}
}
/* Form controls */
.formControl {
border: $border-width solid;
border-radius: $control-radius;
padding: $control-padding;
min-width: $control-min-width;
&-text {
border-color: $main-color0;
background-color: $main-background0;
color: $main-color0;
}
}
/* Specific to the page */
$_default-padding: 0.4em;
.styleGuide {
padding: $_default-padding;
}
.styleGuide_item {
padding: $_default-padding;
}
とりあえずこれだけを使って開発しましょう。
どうしても他の意味わからんpartialみたいな機能を使いたくなったら、まず「CSSの設計が悪いんじゃないか?」と疑うべきです。
それでもやはり必要だと思ったときにはじめて、そのよくわからんやつの使いかたを詳しく学んだらいいのです。
だって、仕様ばっかり記憶してマウンティングするのに忙しい人って実際に開発させると大して役に立たなかったりするじゃんか🐐
垂直分割から水平分割の時代へ
さて、時代というのは変わるもので、実はここまで話していたことの根本が覆るような変化が数年前から続いています。
最初にご紹介した、文章構造と表示方法の分離という原則が覆りつつあるのです。
さくらちゃんは、文章構造をHTML、表示方法をCSS、画面制御をJavaScriptとファイルを分ける従来の分割方法を垂直分割と呼んでいます。
それに対して昨今台頭してきた概念が、水平分割です。
水平分割が市民権を得るようになったきっかけは、なんと言ってもReactの登場でしょう。
ReactはウェブアプリケーションのView(見た目)部分を主に担当する、JavaScriptライブラリーです。
以下の例のように、JSXと呼ばれる拡張構文によって、JavaScriptの中にHTMLが埋め込まれているように見えます。
class HelloMessage extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('hello-example')
);
これは垂直分割の基礎概念に反するものであり、登場当初は「なんかきもちわるい」と批判されたものです。2
しかし、ここには大きな思想の転換があります。
ウェブページにおいては今までどおりの垂直分割で構わないのですが、ウェブアプリケーションにおいては状況が異なるのです。
それは、ウェブアプリケーションでは「コンポーネント」という概念があることです。
コンポーネントとは、入力欄やボタンなど、何度も使われる要素であり、どこで使っても同じ見た目・挙動であることが期待されるもののことです。
コンポーネントという概念の導入は、ウェブアプリケーション開発にとって今では欠かせないものです。
従来の垂直分割に対して、ページをコンポーネント単位に区切るのが水平分割です。
各コンポーネントが独立してHTML、CSS、JavaScriptに相応するものを持っているのです。
これによって、そのコンポーネントがアプリケーションのどこで使われても、同じ見た目・挙動を実現することができます。
もちろん、この概念が濫用された結果「コンポーネントはむやみに作るべきではない」という意見もよく聞かれるようになりました。
それでも、用法用量を守って活用すれば、コンポーネントという考え方はウェブアプリケーション開発にとって強力な武器になります。
今後のCSS
水平分割の時代が到来し、これまで培われたCSSに関するテクニックの多くが、古いものとなってしまいました。
その中で、インラインスタイルの見直しや、そこから派生したCSS in JS、Scoped CSS、CSS Modulesの登場など、混迷を極めています。
また、ElmのようにHTMLを関数として扱う言語によって、さらに新しい局面も見えてきました。
例えばelm-uiというライブラリは、HTMLとCSSの代替となる関数群を組み合わせることで画面を構築します。
さくらちゃん自身も、画面構築の新しい未来を切り開くために、elm-neat-layoutというライブラリを開発中です。
Elmにも興味がある方は、ぜひElmはどんな人にオススメできないかなどの記事もご覧ください。
むすび
ここで紹介した内容はとてもシンプルで素直で味気なく感じたかもしれません。
しかし、ごてごてしていて複雑でいかにもすごそうなものは、実際には大したことがないのです。
そういうごてごてしたものから何度もアクをとって透き通った染み渡るようなスープこそが、結局もっとも優れているのです。
この記事を読み終わった読者の皆さんは、現代のウェブアプリケーション開発におけるCSSの話題についていくための基礎的な知識が身についているはずです。
「必要になるまで学びすぎないこと」「具体的な手段ではなく、なぜそうなったのかを重視すること」などを忘れずに、
さらに知見を広げてご活躍いただければ幸いです。
ぶめぇ