はじめに
Web ページを作っている皆さん!
「is-* クラス」や「data-state」をうまく活用していますか?
「そんなの聞いたことないよ」という人も多いかもしれません。私もそうでした。
しかし、これらは HTML・CSS・JavaScript の責務を整理し、
変更に強いコードを書くうえで、とても役立つ表現方法です!
この記事では、実際のコードを通じて、
HTML・CSS・JavaScript の 責務分離 について考えていきます。
あなたが Web ページの「美しい」設計について知る、きっかけになれば幸いです!
本編
0. 単純な実装
例えば、以下のような実装を考えます。
ボタンを押したあとはローディング状態に移行し、次のような UI に変更する。
- ボタン押下を無効にする
- ボタンの色を薄くする
- カーソルを「待機中」の状態にする
単純に実装する場合、以下のように処理をそのまま JavaScript に書けば十分です。
myButton.onclick = () => {
myButton.disabled = true;
myButton.style.opacity = '0.5';
myButton.style.cursor = 'wait';
}
しかし、機能が増えるにつれて、
「ボタンの下に文章を表示したい」や「終わったら元に戻したい」など、
JavaScript から操作する見た目や要素が増えていくことがあります。
ひとつひとつは単純な処理でも、
それぞれの状態に応じて複数の要素を操作するようになると、
コードは徐々に複雑になりますね。
そうなると、いずれ Web ページの設計を見つめ直したくなることがあるでしょう。
そのとき考えたいのが、
HTML・CSS・JavaScript の責務をどのように分けるか という問題です。
この 責務分離 に役立つのが、is-* クラスや data-state 属性です。
この記事では、これらを使った実例を通じて、
UI の状態と見た目を分離する方法を紹介します。
1. JavaScript が見た目まで管理している?
先ほどの例を考えましょう。
HTML, JavaScript は以下です。
<button id="check-button">チェック開始</button>
const checkButton = document.querySelector('#check-button');
function startChecking() {
checkButton.disabled = true;
checkButton.style.opacity = '0.5';
checkButton.style.cursor = 'wait';
}
function finishChecking() {
checkButton.disabled = false;
checkButton.style.opacity = '1';
checkButton.style.cursor = 'pointer';
}
checkButton.addEventListener('click', async () => {
startChecking();
try {
await runCheck(); // チェック処理関数(省略)
} finally {
finishChecking();
}
});
基本的な動作は実現できていますが、このコードには少し問題があるかもしれません。
というのは、JavaScript が次のことを知っているからです。
- チェック中はボタンを無効にする
- 透明度を
0.5にする - カーソルを
waitにする - 処理が終わったら、それぞれの値を元に戻す
これらは、「処理中にどのような見た目にするか」という指定です。
例えばこのあと、チェック中のボタンの背景色を変更することになった場合、
JavaScript の修正が必要になりますね。
function startChecking() {
checkButton.disabled = true;
checkButton.style.opacity = '0.5';
checkButton.style.backgroundColor = '#ccc'; // ここを追加
checkButton.style.cursor = 'wait';
}
この程度の変更であれば、大きな問題ではありませんが、
同じような見た目の指定が複数の関数に書かれていたらどうでしょうか。
変更箇所が増え、修正漏れが起こる可能性もあります。
見た目を変更するたびに、
JavaScript の処理(ロジック)まで修正しなければならないのです。
これは、責務の分け方として、あまり良い状態とは言えません。
2. CSS に見た目を任せよう
そこで、見た目に関する指定を CSS に移してみましょう。
まず、JavaScript 側では、
処理中であることを is-loading というクラスの追加で表現します。
function startChecking() {
checkButton.disabled = true;
checkButton.classList.add('is-loading');
}
function finishChecking() {
checkButton.disabled = false;
checkButton.classList.remove('is-loading');
}
そして、CSS を用意します。
#check-button.is-loading {
opacity: 0.5;
cursor: wait;
}
これで、役割が少し分かれました。
JavaScript は、チェック中に is-loading というクラスを付け、終了時に取り除きます。
CSS は、そのクラスが付いているときの見た目を決めます。
これにより、背景色など見た目を変更したい場合も、
基本的には CSS だけを修正すれば済みます。
#check-button.is-loading {
opacity: 0.5;
background-color: #ccc; /* ここを追加 */
cursor: wait;
}
JavaScript は、ローディング中のボタンの見た目を知る必要がありません。
CSS にコードを移すことで、
JavaScript が表現するのは UI の状態 だけになり、
その状態を どう見せるか は CSS が担当するという設計に変わったのです。
3. 状態を表現する is-* クラス
今回使用した is-loading は、「ローディング状態」を表現するためのクラス名です。
このように、状態を表現するのには、is-* という名前のクラス が役に立ちます。
is-* という命名自体は、HTML や CSS が定める特別な機能ではありません。
状態を表すクラスだと分かりやすくするための、命名上の慣習です。
このクラスにより、「どのような状態か」というのがわかりやすくなりますね。
同じ考え方で、is-active、is-open、is-disabled なども使用できます。
もちろん、このようなクラスを使用する場合は、
見た目に関する指定は CSS 側に集約しましょう。
これにより、責務分離が進むわけです。
なお、disabled は見た目を指定するためのものではなく、
ボタンなどの操作を無効にするための HTML 属性です。
操作については JavaScript で制御し、そのときの見た目は CSS で指定する、というように
役割を分けて考えるとよいでしょう。
整理すると、以下のようになります。
| 処理 | 担当 |
|---|---|
| ボタンやそのラベルなどの構造を定義する | HTML |
| ボタンを押したときにチェックを開始する(処理を行う) | JavaScript |
| 処理中であることを表現する | JavaScript |
| 処理中の見た目を決める | CSS |
このように分けることで、変更に強くなるという利点があります。
4. 状態を表す data-state 属性
ここまでの例では、is-loading というクラスの有無で、状態を表現しました。
しかし、UI の状態が増えてくると、これだけでは管理が難しくなってきます。
例えば、「処理中」だけでなく、次のような4つの状態を管理したい、となったとします。
-
idle:待機中 -
loading:処理中 -
success:処理成功 -
error:処理失敗
このような状態を複数のクラス(is-idle, is-loading, ...)で表現することもできますが、
別々のクラスが同時に付いてしまったりなど、エラーが起こる可能性が上がります。
そこで利用したいのが、HTML の data-state 属性 です。
data-state は、HTML の data-* 属性のひとつで、
独自のデータを HTML 要素に持たせることができます。
この data-state を使えば、現在の状態をひとつの値で表現することができるのです。
先ほどの例で考えてみましょう。
例えば、チェック処理を管理する要素に、現在の状態を設定するとします。
<section id="checker" data-state="idle">
<button id="check-button">チェック開始</button>
<p id="result-message"></p>
</section>
今回は section に、data-state="idle" という指定を加えました。
これによって、チェック処理が待機中であることを表しています。
この属性の変更は、JavaScript から dataset を使って行えます。
const checker = document.querySelector('#checker');
function setState(state) {
checker.dataset.state = state;
}
例えば、チェックを開始するときは次のようにします。
setState('loading');
HTML の状態は、次のように変わります。
<section id="checker" data-state="loading">
このように、data-state の値を変更することで、現在の状態を明示できるようになります。
data-state の値は、CSS の属性セレクターで参照できます。
/* 処理中はボタンを半透明にする */
#checker[data-state="loading"] #check-button {
opacity: 0.5;
cursor: wait;
}
/* 成功時はメッセージを緑色にする */
#checker[data-state="success"] #result-message {
color: green;
}
/* 失敗時はメッセージを赤色にする */
#checker[data-state="error"] #result-message {
color: red;
}
これで、JavaScript は状態を決め、CSS はその状態に応じた見た目を決める、という
責務分離ができるわけです。
5. 状態の変更を、ひとつの関数にまとめよう
以下のように、状態を変更する処理をひとつの関数にまとめておくと、
状態が管理しやすくなります。
function setState(state) {
checker.dataset.state = state; // UIの状態の更新
checkButton.disabled = state === 'loading'; // 状態に応じた制御の更新
}
例えば、上のように書いておけば、チェックを開始するときは次の一行で済みます。
ボタンの無効化を、あちこちのイベント処理に書く必要はありません。
setState('loading');
6. data-state か、is-* クラスか?
ここまで、状態を管理する方法の例として、
is-* クラスと data-state 属性について説明してきました。
ここで注意したいのが、
data-state のほうが is-* クラスより優れているとは限らないという点です。
例えば、単純に「要素がアクティブかどうか」を表現したいだけなら、
is-active クラスを使う次のコードのほうが簡潔です。
element.classList.toggle('is-active', isActive);
.tab.is-active {
font-weight: bold;
}
反対に、UI が複数の状態を持ち、状態の名前を明示したい場合は、
data-state が役立ちます。
<section data-state="loading">
つまり、どちらを選ぶかは、状況によるということです。
どちらかが優れている、ということではありません。
-
is-*クラス:ある状態や条件が成立していることを表現する。 -
data-state:現在の状態を、名前付きの値として表現する。
7. 状態遷移のロジックも分離してみよう
ここまで、is-* クラスや data-state を使って、
UI の状態と見た目を分離する方法を紹介してきました。
ここで重要なのが、
状態を表現することと、状態の遷移を正しく管理することは別の問題だということです。
例えば、JavaScript に次のような関数 setState() を実装したとします。
function setState(state) {
checker.dataset.state = state;
}
このJavaScriptは、どの状態からどの状態へ移行してよいかは制御していません。
そこで、状態遷移を管理する処理を別に用意することも検討できます。
function getNextState(state, event) {
switch (event.type) {
case 'CHECK_STARTED':
if (state !== 'idle') return state;
return 'loading';
case 'CHECK_SUCCEEDED':
if (state !== 'loading') return state;
return 'success';
case 'CHECK_FAILED':
if (state !== 'loading') return state;
return 'error';
default:
return state;
}
}
この getNextState() は、現在の状態とイベントから、次の状態を決定する関数です。
見た目も変更しませんし、HTML の data-state を直接変更するわけでもありません。
状態の遷移に関する判断だけを担当していますね。
このように、状態遷移の判断と、その結果を画面に反映する処理を分けることもできます。
8. 責務分離のまとめ
最後に、責務分離を考えるうえで重要な視点を紹介します。
それは、 「何が変わったときに、どこを修正するのか」 です。
例えば、以下のようになっていれば、責務分離ができていると考えられるでしょう。
| 変更したい内容 | 主に見直す・修正する範囲 |
|---|---|
| ボタンの色や余白を変更する | CSS |
| 処理開始の条件を変更する | JavaScript の振る舞い |
| 成功・失敗の遷移ルールを変更する | 状態遷移の処理 |
| 警告の文章を変更する | 文章生成・表示の処理 |
| ボタンや結果領域の構造を変更する | HTML と関連する処理 |
責務分離とは、
変更の理由に対して、修正すべき場所を見つけやすくする ための考え方なのです。
おわりに
この記事では、is-* クラスや data-state 属性の利用を通じて、
Web ページを作成するときの責務分離について解説しました。
皆さんの Web ページも今一度、設計を見直してみてはいかがでしょうか?
読んでみて、「なるほどな」と思ったら、ぜひストック・いいねをお願いします。
この記事が、皆さんの役に立てば幸いです!
※この記事は、一部生成AIを利用して作成しました。