カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

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-itemsstretchになっており、コンテナ内の同じ行の中で最も高い要素に合わせてコンテナの高さが設定されるというものです。複数の行が必要な場合は、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が不可欠な役割を果たすことが多いことに気付きました。

次の記事

配置に役立つ擬似クラス

前の記事

2022 Advent Of Code: カソード線管

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください