Gemcook Tech Blog
🐙

クリック要素が重なり合うUI実装のベストプラクティス

に公開

はじめに

クリック要素が重なり合うデザインやUIはよくあると思うのですが、そのような実装のベストプラクティス(主観)を考えてみたので記事にしてみました!

作成するもの

実装方針

様々な実装方法がありますが、今回は以下のルールに沿って作成していきます。

  1. <a/><button/> などのインタラクティブ・コンテンツは入れ子にしない。
    • 理由:HTMLのルールだから
  2. z-indexを使用しない。
    • 理由:1つ使えば乱用が始まる危険なプロパティだから
  3. positionを使わない。
    • 理由:スタックコンテキストを発生させるから

デザインを見ると 重なっている という理由から positionz-index を使いたくなります。

ただ、本当に使わないといけない箇所以外でスタックコンテキストが発生するプロパティを使用すると、意図しない重なりが発生する場合や z-index の指定について考える必要が出てきたりと副作用が多く極力避けるべきだと考えるからです。

また、HTMLを理解すれば自然に生じる重なりで十分に実現できるので、できる限りそれに任せたいとも考えています。(詳しくは以下の記事をご覧ください)

https://developer.mozilla.org/ja/docs/Web/CSS/CSS_positioned_layout/Stacking_context

https://ics.media/entry/200609/

実装していく

今回の実装において、一番のポイントはDOM構造とCSSスタイルを適切に組み合わせることだと思うので、DOM構造とCSSに分けて説明していきます。

DOM構造

背景要素 を並列に置くことで、インタラクションなコンテンツは入れ子しないというHTMLのルールに従いながらも、スタイリングが当てやすいような以下のようなDOM構造で進めて行きます。

 return (
    <div className={styles.container} >
      {/* 背景 */}
      <a className={styles.link} />
      <div className={styles.body} >
        {/* Green */}
        <button className={styles.thumbnail} />
        {/* Blue */}
        <button className={styles.button} />
      </div>
    </div>
  )

CSS

不要なスタックコンテキストを生み出したく無いという思想のもと display: grid を使い実装していきます。

全体( .container )に display:grid; を指定して、並列に置いた子要素に grid-row: 1; grid-column: 1; を指定することで、それぞれ同じ場所で親の要素いっぱいのサイズになってくれます。

重なり順がどうなっているのか?という点ですが、.link.body の順番で重なってくれるので指定せずとも期待した重なりになってくれます。

.container {
  width: 200px;
  display: grid;
}

.link {
  display: grid;
  grid-row: 1;
  grid-column: 1;
}

.body {
  display: grid;
  grid-row: 1;
  grid-column: 1;
}

現状の指定だけでは、もちろん背景の要素( .link )を押すことができないので、次はクリック制御のために pointer-events の指定が必要です。

https://developer.mozilla.org/ja/docs/Web/CSS/pointer-events

.link はすでに押下することができるので特に指定せず、.link の上にある要素 .body に対して pointer-events:none; を指定することで、クリック時に .body は無視されて後ろにある要素の .link をクリックすることができます。

ただ、このままだと .body のクリックしたい要素( .thumbnail .button )も無視されてしまうので、それぞれにpointer-events: all;を指定することでクリックできる要素に戻します。
(上の要素はクリック判定を none で全消して、必要な箇所のみ all の指定で復活させるイメージです)

.container {
  width: 200px;
  display: grid;
}

.link {
  display: grid;
  grid-row: 1;
  grid-column: 1;
}

.body {
  display: grid;
  grid-row: 1;
  grid-column: 1;
  /* クリック制御 */
  pointer-events: none;
}

.thumbnail {
  /* クリック制御 */
  pointer-events: all;
}

.button {
  /* クリック制御 */
  pointer-events: all;
}

完成したコード

import styles from "./styles.module.css";
export const Card = () => {
  return (
    <div className={styles.container}>
      <a className={styles.link} onClick={() => console.log("背景:Link Clicked!")} />
      <div className={styles.body}>
        <div className={styles.thumbnail} onClick={() => console.log("要素:Green Clicked!")} />
        <button className={`${styles.fullWidth}`} onClick={() => console.log("要素:Blue Clicked!")} />
      </div>
    </div>
  );
};
.container {
  width: 200px;
  display: grid;
  gap: 10px;
}

.link {
  display: grid;
  grid-row: 1;
  grid-column: 1;
  border: solid 1px black;
}

.body {
  border: solid 1px black;
  display: grid;
  grid-row: 1;
  grid-column: 1;
  gap: 30px;
  padding: 10px;
  pointer-events: none;
}

.headerButtons {
  display: flex;
  gap: 10px;
}

.button {
  width: 20px;
  height: 20px;
  border-radius: unset;
  background-color: unset;
  border: solid 1px black;
  background-color: rgb(220, 220, 220);
}

.thumbnail {
  pointer-events: all;
  width: 100%;
  height: 100px;
  border: solid 1px black;
  background-color: rgba(0, 255, 64, 0.495);
}

.fullWidth {
  width: 100%;
  height: 30px;
  border-radius: unset;
  background-color: unset;
  border: solid 1px black;
  background-color: rgba(0, 106, 255, 0.495);
  pointer-events: all;
}

まとめ

調べても様々な実装方法 & 議論が出てくるUIですが、gridpointer-eventsを上手く使うことで、HTMLのルールに準拠しながらもコードリーディングもしやすい実装ができるのではないでしょうか?
実装の際の選択肢の一つになれば幸いです。最後までお読みいただいありがとうございました。

Gemcook Tech Blog
Gemcook Tech Blog

Discussion