クリック要素が重なり合うUI実装のベストプラクティス
はじめに
クリック要素が重なり合うデザインやUIはよくあると思うのですが、そのような実装のベストプラクティス(主観)を考えてみたので記事にしてみました!
作成するもの
実装方針
様々な実装方法がありますが、今回は以下のルールに沿って作成していきます。
-
<a/>
や<button/>
などのインタラクティブ・コンテンツは入れ子にしない。- 理由:HTMLのルールだから
-
z-index
を使用しない。- 理由:1つ使えば乱用が始まる危険なプロパティだから
-
position
を使わない。- 理由:スタックコンテキストを発生させるから
デザインを見ると 重なっている という理由から position
や z-index
を使いたくなります。
ただ、本当に使わないといけない箇所以外でスタックコンテキストが発生するプロパティを使用すると、意図しない重なりが発生する場合や z-index
の指定について考える必要が出てきたりと副作用が多く極力避けるべきだと考えるからです。
また、HTMLを理解すれば自然に生じる重なりで十分に実現できるので、できる限りそれに任せたいとも考えています。(詳しくは以下の記事をご覧ください)
実装していく
今回の実装において、一番のポイントは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
の指定が必要です。
.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ですが、grid
とpointer-events
を上手く使うことで、HTMLのルールに準拠しながらもコードリーディングもしやすい実装ができるのではないでしょうか?
実装の際の選択肢の一つになれば幸いです。最後までお読みいただいありがとうございました。
Discussion