2017年には、Effective前端1:能使用html/css解决的问题就不要使用JSという記事を読んだことがあります。初めて読んだ時は共感し、まだ知らなかった技術も学ぶことができましたので、皆さんにもぜひ読んでいただきたいです。JavaScriptはほとんどの問題を解決できますが、アクセシビリティの観点やパフォーマンス、バンドルサイズの観点から見ると、CSSで解決できる場合は必ずそちらを選ぶべきです。ただし、JSを使用しないように心がけることは、完全にJSを使用しないことを意味しません。これらはまだいくつかの違いがあります。この記事では、上記の記事を再度読み直し、改善できると思われる点を指摘します。
:hover
を使用してホバースタイルを作成する
確かに、:hover
を使用してユーザーにUI要素が対話可能であることを示すことは、フロントエンドエンジニアにとって基本的な知識です。記事では、ドロップダウンメニューを作成するために:hover
を使用する方法も紹介されています。
<div data-editor data-tab="css" hidden>
<div data-style>
.dropdown {
position: relative;
display: inline-flex;
justify-content: space-around;
gap: 1em;
}
.item { display: none; }
.dropdown-item:hover + .item {
display: block;
position: absolute;
top: 30px;
left: 0;
padding: 10px;
background-color: #efefef;
}
</div>
<div data-body>
<div class="dropdown">
<a href="#" class="dropdown-item">聯絡我</a>
<div class="item">
<ul>
<li>Item1</li>
<li>Item2</li>
</ul>
</div>
</div>
</div>
</div>
hover
時にdisplay: block
に変更し、通常の状態ではdisplay: none
にします。これには大きな問題はなさそうですが、もしユーザーがマウスではなくキーボードでナビゲーションを行う場合はどうでしょうか?キーボードでナビゲーションする場合、:hover
は効果がありません。また、DOMの構造に制約があり、トリガーするUI要素はドロップダウンリストと隣接する必要があります。
そのため、ユーザーに要素が対話的であることを示すために:hover
を使用する場合、ユーザーがマウスでナビゲーションしない場合の対話方法を考慮する必要があります。具体的には、以下のようなことが含まれます。
:focus
を追加するか、クリックイベントを監視してユーザーがドロップダウンメニューをトリガーできるようにする。aria-expanded
を追加し、スクリーンリーダーにメニューの開閉状態を通知し、キーボードでオプションを上下に制御できるようにする。
<div data-editor hidden data-tab="markup">
<div data-style>
.dropdown {
position: relative;
display: inline-flex;
justify-content: space-around;
gap: 1em;
}
.item {
display: none;
}
.dropdown-item:hover + .item,
.dropdown-item:focus + .item {
display: block;
position: absolute;
top: 30px;
left: 0;
padding: 10px;
background-color: #efefef;
}
</div>
<div data-body>
<div class="dropdown">
<button id="toggle" href="#" class="dropdown-item">聯絡我</button>
<div class="item" id="dropdown-1" aria-expanded="false">
<ul>
<li>Item1</li>
<li>Item2</li>
</ul>
</div>
</div>
</div>
<div data-js>
function toggleExpanded() {
const node = document.querySelector('#dropdown-1');
const prev = node.getAttribute('aria-expanded');
node.setAttribute('aria-expanded', prev === 'true' ? 'false' : 'true');
}
const toggle = document.querySelector('#toggle');
toggle.addEventListener('focus', toggleExpanded);
toggle.addEventListener('blur', toggleExpanded);
toggle.addEventListener('mouseover', toggleExpanded);
toggle.addEventListener('mouseleave', toggleExpanded);
</div>
</div>
この例では、ホバーでドロップダウンをトリガーするだけでなく、JavaScriptを使用してfocus
およびmouseover
イベントを監視してaria-expanded
を調整しています。キーボードナビゲーションはこの記事の主題とは異なるため、実装は省略しています。さらに、aria-expanded
を使用する場合、CSSを次のように調整することもできます。
.dropdown-item:hover + .item,
.item[aria-expanded="true"]
{
/* style */
}
:checked
と隣接セレクタを使用したカスタムスタイル
カスタムチェックボックスやラジオボタンを実装する場合、記事で紹介されているテクニックを使用する必要があると思います。疑似クラス:checked
を使用すると、クラスのトグルのために追加のイベントリスナーを登録する必要がなくなります。カスタムチェックボックスやラジオボタンを作成する場合、できるだけこの方法を優先して使用し、<div>
を使用してゼロから作成するよりも優れた方法です。考慮すべき点は次のとおりです。
aria-label
を使用してスクリーンリーダーがこのチェックボックスまたはラジオボタンの目的を理解できるようにします(またはaria-labelledby
を使用します)。<div role="status"></div>
または他の方法を使用して値の変化を通知します(必要な場合)。- focusのスタイル処理を実装します。
スクリーンリーダーはチェックボックスを読み上げる際、ラベルの名前と選択状態のみを読み上げます。チェックボックスの目的が選択と非選択以外の場合(例:ダークテーマの切り替えなど)、追加のヒントを追加すると、スクリーンリーダーが理解しやすくなります。
<div data-editor data-tab="markup" hidden>
<div data-style>
.label:has(input:focus-visible) {
outline: 2px solid blue;
}
.track {
position: relative;
display: inline-block;
width: 50px;
height: 1.2rem;
border-radius: 9999px;
background-color: #778da9;
cursor: pointer;
}
.track .cursor {
position: absolute;
left: 0;
top: 1px;
display: inline-block;
width: 1.1rem;
height: 1.1rem;
border-radius: 50%;
border: 1px solid #efefef;
transition: transform 0.3s ease-in;
background-color: #fff;
}
.checkbox .cursor {
transform: translateX(0);
}
.checkbox:checked~.track .cursor {
transform: translateX(1.3rem);
}
.checkbox:checked~.track {
background-color: #778da9;
}
/* keep hidden but focusable */
.hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
</div>
<div data-body>
<label for="toggle" class="label">
<input type="checkbox" class="checkbox hidden" id="toggle" aria-label="toggle"/>
<span class="track" ><span class="cursor"></span></span>
</label>
<div role="status" id="status"></div>
</div>
<div data-js>
toggle.addEventListener('change', function(e) {
const node = document.getElementById('status')
node.innerHTML = '目前主題已切換'
})
</div>
</div>
input
を非表示にするためにdisplay: none
ではなく、他のCSSプロパティを使用して非表示にしています。また、キーボードナビゲーション時にフォーカススタイルがあるようにするために、:focus-visible
を追加しています。これにより、キーボードナビゲーション(たとえば、タブ)を使用する場合にのみルールが適用され、クリック時にフォーカスの枠が表示されなくなります。
等しい高さの複数の列
この記事は2016年に書かれたもので、紹介されている方法は有効ですが、少し古い方法です。2022年では、Flexboxのサポートが非常に高いため、直接Flexboxを使用して問題を解決することができます。さらに細かい制御が必要な場合は、Gridを使用することもできます。
原理は、Flexboxのレイアウト特性を利用して、デフォルトではalign-items
がstretch
になっており、コンテナ内の同じ行の中で最も高い要素に合わせてコンテナの高さが設定されるというものです。複数の行が必要な場合は、flex-wrap: wrap
を追加することを忘れないでください。デフォルトでは、Flexboxは同じ行に収まるように試みます。
<div data-editor data-tab="css" hidden>
<div data-style>
.container {
max-width: 1200px;
width: 95%;
margin: 0 auto;
display: flex; /* comment me */
flex-wrap: wrap;
justify-content: flex-start;
gap: 1em;
}
.card {
width: 30%;
// display: inline-block; /* comment me */
padding: 8px;
background-color: #efefef;
}
.card img {max-width: 100%}
</div>
<div data-body>
<div class="container">
<div class="card">
<img src="https://unsplash.com/photos/hwLAI5lRhdM/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8Mnx8amFwYW58amF8MHx8fHwxNjY5NTE4NjA1&force=true&w=640" />
<h3>Information</h3>
<p>This photo is taken by Unsplash Clay Banks</p>
</div>
<div class="card">
<img src="https://unsplash.com/photos/alY6_OpdwRQ/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8OHx8amFwYW58amF8MHx8fHwxNjY5NTE4NjA1&force=true&w=640"/>
<h3>Tokyo</h3>
<p>This photo is taken by Unplash Jezael Melgoza.</p>
<p>I like Tokyo because it's a such good place where I can see people meltdown</p>
</div>
</div>
</div>
</div>
フォームの送信
記事で指摘されているように、多くの人々が実際にはネイティブの<form>
を無視してしまっていることには同意します。<form>
を使用してフォームの内容を定義することで、大量のJavaScriptコードを節約することができます。
記事では、ブラウザのネイティブのフォームバリデーション機構と:invalid
疑似クラスを組み合わせて、スタイルを適用しています。例では、無効な状態の場合に送信ボタンをopacity: 0.5
に設定していますが、実際の実装では<button>
を使用し、JavaScriptで入力が無効な場合にdisabled
を追加することになるでしょう。
フォームにまだ慣れていない場合は、次の2つの記事を参考にしてください。
疑似クラスの活用
記事では、:checked
、:focus
、:invalid
などを活用することが重要であると述べています。これらの疑似クラスをうまく活用することで、不要なJavaScriptを節約することができ、読みやすさも向上します。疑似クラスについては、以下の記事でより新しい疑似クラスのいくつかを紹介していますので、興味があれば参考にしてください。排版時有用的 Pseudo 類別
結論
2017年は私がフロントエンドに初めて入った時期であり、細部の処理に関する理解度はまだ高くありませんでした。今回改めて振り返ってみると、優れたユーザーエクスペリエンスを実現するためには、CSSを適用するだけではなく、アクセシビリティの考慮からJavaScriptが不可欠な役割を果たすことが多いことに気付きました。