Day 8: webアプリのボタンをスクリーン上のどこに、どうやって置くか
要約 (TL;DR)
Google マップを画面全体に埋め込んだ web アプリを制作するにあたり、スクリーンの左上にメニューボタン、右上に検索ボタン、右下にアプリの主要機能のための残り二つのボタンを配置するデザインにした(見出し画像参照)。
この UI デザインを実装するために、Styled Components と "design tokens" 及び data 属性セレクターを組み合わせて使うことで、CSSコードを読みやすくし、アプリの画面表示を(若干)速くした。
はじめに
My Ideal Map App という、Google マップのユーザー体験を改善する web アプリを制作している。このアプリのメイン画面に表示するボタンを雲のような形にデザインし、HTML/CSSコードを書いた(前回の記事参照)。
次のステップは、このボタンをどのような機能のために幾つメイン画面に表示させるか、及び、それぞれの機能にふさわしいラベルとしてどのアイコンを用いるか、を決めること(第1節参照)。
そして、これらのボタンをどのようにスクリーン上に配置するかを、ユーザーの無意識レベルでの期待になるべく抗わずにデザインすること(第2節参照)。
最後に、ボタンの配置を実装するCSSコードの書き方を、コードの読みやすさとUI画面表示の速さを担保するように、工夫すること(第3節参照)。
この記事では、以上三つの作業をするにあたって学んだことを詳しく説明する。誰かの役に立つかもしれないので。
1. 四つのボタンと、それぞれのラベル
ユーザーが My Ideal Map App にログインして最初に表示される画面には、四つのボタンを表示させる。四つを超えると、どのボタンを押すべきなのか、すぐに判断できなくなるから。多すぎる選択肢は、我々を不幸せにする(Schwartz 2005)。
多すぎるボタンに頭を抱えるホーマー・シンプソン(画像元: Babich 2018)
この四つのボタンの機能、及び、ラベルとして用いるアイコンは、以下の通り。
1.1 検索ボタン
まずは、検索ボタン。押すと、検索ボックスが表示され、場所を検索できる。場所の検索は、My Ideal Map App のコアとなるユーザー体験に欠かせない。興味を持った場所を保存するためには、まず、その場所を検索する必要がある。また、どこかへ出かける前に、過去に保存した場所が目的地の周辺にあるかどうかを確認したいならば、まず、その目的地を検索する必要がある。(このユーザー体験の一例は、過去記事 Day 2 を参照)。
検索ボタンではなく、最初から検索ボックスを表示させるというてもある(Googleマップアプリがその一例)。しかし、その分だけスクリーン上部の地図が見えなくなり、ユーザーが興味を持って保存していた場所がそこに隠れてしまうかもしれない。なので、デフォルトでは地図を目一杯見せたい。検索ボタンを押したら自動的に検索ボックスにフォーカスするように実装すれば、最初から検索ボックスがある場合と、検索を始めるまでのタップの数は同じであり、余計な手間が増えるわけでもない。
検索ボタンであるとわかってもらうためのラベルは、虫眼鏡アイコン以外にない。多くのウェブサイトやアプリで使われているので、押したら何が起こるか、ほとんどのユーザーが理解していると言える(Sherwin 2014)。
Material Icons の検索アイコン(画像元: Google Fonts)
1.2 保存ボタン
次に、My Ideal Map App のコア機能である、場所を保存するためのボタン。雑誌などを読んで行ってみたいなと思った場所を、将来忘れないようにするためのボタン。(このユーザー体験の一例は、過去記事 Day 3 を参照)。
これを地図上に常に表示させることで、デフォルトの画面ではどうやって場所を保存すればいいかわかりにくいGoogleマップアプリとの差別化を図る。
このボタンのラベルとしては、何かを追加する、という意味を端的に表す、プラス記号のアイコンを用いる。
Material Icons の追加アイコン(画像元: Google Fonts)
1.3 現在位置表示ボタン
三つ目は、GPSを取得してユーザーが今いる場所を地図に表示するためのボタン。このボタンも、My Ideal Map App のコア機能の一つ。今自分がいる場所の周辺に、行ってみたいと思っていた場所があるかどうかを確認するために押す。(このユーザー体験の一例は、過去記事 Day 1 を参照)。
Googleマップアプリは、方位磁針アイコンを使っているが、My Ideal Map App では、空港で出発ゲートのサインによく使われる、離陸アイコンを用いる。地図上に現在位置を表示するということは、空に飛び上がって自分の今いる場所を確認する、と解釈するのが、「空から地図を自分色に染める」という My Ideal Map App のデザイン・コンセプトにふさわしいから(詳しくは過去記事 Day 2 を参照)。
Material Icons の離陸アイコン(画像元: Google Fonts)
1.4 メニューボタン
最後に、その他の機能をメニューとして表示させるためのボタン。重要ではないアプリの機能は、ボタンを押さないと表示されないメニューに格納して、ユーザーがアプリの主要機能を用いるのを視覚的に邪魔しないようにする。
メニューボタンのラベルとしては、いわゆる「ハンバーガーアイコン」しかない。2010年台半ばにスマホの普及と同時に登場した時は、物議を醸したが(例えば、Pernice and Budiu 2016)、非常に多くのサイトが採用したアイコンなので、2020年代となった現在、ほとんどのユーザーが、押すとメニューが表示される、というメンタルモデルを持っていると考えられる。また、メイン機能ではなく、副次的な機能のメニューとして使う分には、問題ない(Seno 2019)。
Material Icons のメニューアイコン(画像元: Google Fonts)
1.5 Material Icons
以上、4つのアイコンが全て揃っているのは、数あるアイコン・リポジトリの中でも Font Awesome と Material Icons しかなかった(特に離陸アイコンが他に見つからない)。My Ideal Map App が Google マップを UI に埋め込んでいることを考えると、Google が作った Material Icons を用いるのが、UIデザインの一貫性のためにはベストである。
というわけで、Material Icons のSVGデータを、Google Fonts からダウンロードし、Sketch にインポートして、雲ボタン(過去記事 Day 7 参照)の上に重ねる。縦横共に中央揃えにして、できたボタンをSVG画像としてエクスポート。(このSVGデータをもとに、ボタンを表示させるためのHTMLコードを作る方法は、過去記事 Day 7 を参照。)
Sketch アプリで作成した雲形ボタン(筆者作成)
これでボタンが用意できた。次はこの四つのボタンをスクリーン上のどこに配置するか、である。
2. ボタンの配置:デザイン編
まず、検索ボタンはスクリーンの右上にする。Nielsen Norman Group による UX リサーチ(Sherwin 2014) によれば、ユーザーは検索したいときにまずスクリーンの右上を見る。古い研究結果だが、私が知る限り、2021年現在も多くのサイトは右上に検索ボタンを置いているので、今も当てはまるだろう。
次にメニューボタン。これは左上に置く。右上にあることも多いが、検索ボタンを右上に置くのであれば、メニューボタンは左上にするのがわかりやすい。検索ボタンの隣に置くのは、あまり見ないパターンなので、ユーザーを混乱させる可能性がある。
残り二つのボタンは、右下に置く。この二つのボタンを押すかどうか判断するには、スクリーン上に今何が表示されているかをまず確認する必要があるから。
Syzonenko (2019) によれば、スクリーン右下のボタンは、ユーザーがスクリーン上の情報をZ字型にスキャンするときに望ましい。他方、スクリーン左下のボタンは、スクリーン上の情報が左寄せで一列に並んでいるときに好ましい(この場合、ユーザーの目の動きはF字型になる)。
この考え方を地図アプリに応用すると、ユーザーは、地図を見るときにZ字型にスキャンするか、左上から右下へスキャンすると考えられる。その結果、現在位置とは別の場所が表示されていると理解すれば、ボタンを押して現在位置表示させようとするだろう。もしくは、保存したい場所が表示されていると理解すれば、ボタンを押して保存作業を始めようとするだろう。したがって、視線の終着点であるスクリーン右下に、この二つのボタンを配置するのが望ましい。
他方、メニューボタンや検索ボタンを押すかどうかの判断は、今、スクリーン上に地球上のどの場所が表示されているか、に無関係である。したがって、スクリーン上部に配置するのが望ましい。
最後に、スクリーン右下の二つのボタンのうち、現在位置表示ボタンを保存ボタンの上に配置することにした。現在位置を表示させることは、空を飛んで移動することに似ている。だからこそ、現在位置表示ボタンには、離陸する飛行機のアイコンが表示されている。その上に他の何かが表示されていれば、空を飛ぶのを邪魔するように見えてしまう。
* * *
大まかなデザインはこれで固まった。ここからはコーディングしつつ、細部を詰める。
3. ボタンの配置:コーディング編
過去記事 Day 7 (第5節)で紹介した通り、CSSコードは Styled Components を用いて JavaScript で書いている。ボタン用のCSSコードは、以下のように指定する。
import styled from 'styled-components';
const Button = styled.button`
${styleCloudButton}
${positionButton}
};
ここで、`styleCloudButton` は、ボタンの色や影などの CSS コードを全てひとまとめにした変数。ボタンの配置については、`positionButton` という変数にまとめる。この変数をどう定義するかが、当記事の残りの内容となる。
3.1 ボタンをGoogleマップ上に表示させる
まず最初に、四つのボタンを埋め込んだ Google マップの上に表示させる。そのために、まず、My Ideal Map App の HTML コードを以下のような構造にする。
<body>
<button></button> <--! Menu button -->
<button></button> <--! Search button -->
<button></button> <--! Current location button -->
<button></button> <--! Save-a-place button -->
<div id="map"></div> <--! Embedded Google Maps -->
</body>
最後の <div id="map"> の子要素として、Googleマップが埋め込まれる。マップをスクリーンいっぱいに表示させるために、CSSコードは
html,
body,
#map {
height: 100%;
}
となっている(Google Maps Platform 公式ドキュメント参照)。
このスクリーンいっぱいに表示された Google マップの上、つまり、<div id="map">要素の上にボタンを表示するためのCSSコードは、
position: absolute;
z-index: 1;
となる。`position: absolute;`とすることで、他のHTML要素の上に重ねることができる。しかし、`z-index: 1;`も付け加えないと、Googleマップの下に隠れてしまう。
なぜ z-index が必要なのか、最初わからなかったのだが、Chrome DevTools で <div id="map"> のスタイルを調べていると、
position: relative;
となっていた。Google Maps JavaScript API が勝手に設定するらしい。その結果、Googleマップも、他のHTML要素の上に重なるようになる。すると、CSSの "stacking context" と呼ばれるルールによって、HTMLコード上で後に来る要素の方が上に重なるので、<button>要素よりも後に来る<div id="map">が上に重なる。つまり、ボタンが Google マップの下に隠れてしまう。これを防ぐために、<button>要素に `z-index:1`と指定する必要がある。
この z-index というのは、非常に厄介で、ウェブ制作での悩みの種。仕組みを上手に説明できる人も少ない。私自身は、Walton (2013) を読んで、初めて理解した。最近では、Comeau (2021b) もわかりやすい説明である。
3.2 "design tokens" を用いて CSS コードを読みやすくする
次のステップは、ボタンを置く場所をスクリーンの端から何px離すか、を決めること。ボタンのタップできる範囲が縦48px、横56pxなので、それぞれ 4 で割って、縦方向に 12px 、横方向に 14px 、スクリーンから離すようにする。寸法が倍数の関係になっていると、見た目にスッキリするから。
以上の数字をCSSコードに直接書き込んでも良いのだが、そうすると、なぜ 12px なのか、といったことがわからなくなる。特定の値を選んだ理由をコードに埋め込んでしまう方法はないのだろうか、といつも悩んでいたのだが、最近習った design tokens という考え方を応用するとうまくいくことに気が付いた。(design tokens については、Rendle 2019 を参照。具体例として、Adobe の Specify というアプリが design tokens をどのようにして利用しているかは、Chenais 2021 を参照。)
具体的には、以下のコードを `designtokens.js` として保存する。
export const dimension = {
button: {
'height 100': '48px',
'height 25': '12px',
'width 100': '56px',
'width 25': '14px',
},
};
こうすると、どうやって 12px や 14px といった数字を導いたかを、コードそのものが説明してくれる。例えば、48px には `height 100` という名前が付き、12px には `height 25` という名前が付いているので、ボタンの縦の長さを25%にしたのが 12px とすぐわかる。
なお、このコードの書き方は、design tokens の標準書式として提案されているものとは異なるのだが、Amazon の Style Dictionary のような自動変換ツールを使うわけではないので、問題はない。もし、webアプリだけじゃなく、iOSアプリやAndroidアプリも作る、ということになれば、話は別だが、今のところその予定はない。
以上の "design tokens" を用いれば、例えば、ボタンのタップできる範囲を指定する CSS コード(過去記事 Day 7 第5節を参照)は、以下のように書き換えることができる。
import {css} from 'styled-components';
import {dimension} from './designtokens';
const setClickableArea = css`
height: ${dimension.button['height 100']};
width: ${dimension.button['width 100']};
`;
3.3 各ボタンをスクリーンの隅に配置する
メニューボタンをスクリーンの左上に配置するには、
const positionButton = css`
position: absolute;
z-index: 1;
top: ${dimension.button['height 25']};
left: ${dimension.button['width 25']};
`;
とする。このCSSコードによって、ボタンの左上の隅が、スクリーン上端から12px、左端から14px離れたところに来る。
他のボタンについては、`top` と `left` プロパティーのところを書き換えればいい。
検索ボタンをスクリーンの右上に配置するには、
top: ${dimension.button['height 25']};
right: ${dimension.button['width 25']};
とする。ボタンの右上の隅が、スクリーン上端から12px、右端から14px離れたところに来る。
右下に配置する保存ボタンについては、まず、埋め込んだ Google マップの右下に、以下のような横に細長いボタンが表示されることを考慮する必要がある(非表示にすることはできないっぽい)。
webページに埋め込んだGoogleマップの右下隅に表示されるボタン(筆者によるスクリーンショット)
そこで、スクリーン下端からの距離をボタンの縦の長さの 50% にする。designtokens.js を以下のように書き直し、
export const dimension = {
button: {
'height 100': '48px',
'height 50': '24px', // ADDED
'height 25': '12px',
'width 100': '56px',
'width 25': '14px',
},
};
CSSコードは以下のようにする。
bottom: ${dimension.button['height 50']};
right: ${dimension.button['width 25']};
結果として、ボタンの右下の隅が、スクリーン下端から12px、右端から14pxの場所に配置される。
最後に、現在位置表示ボタンは、
bottom: ${dimension.button['height 175']};
right: ${dimension.button['width 25']};
とする。ここで、`height 175` は、designtokens.js において
export const dimension = {
button: {
'height 175': '84px', // ADDED
'height 100': '48px',
'height 50': '24px',
'height 25': '12px',
'width 100': '56px',
'width 25': '14px',
},
};
と定義する。84px は、保存ボタンの高さ 48px と、そのスクリーン下端までの距離 24px と、保存ボタンと現在位置表示ボタンの間のスペース 12px を足し合わせた数字。
以上の CSS コード(正確には CSS-in-JS コード)によって、四つのボタンは以下のように表示される。
My Ideal Map App の現段階でのユーザーインターフェース(筆者によるスクリーンショット)
3.4 Styled Components を用いてボタンを配置する
しかし、まだコードは完成していない。各ボタンを同じ <Button> コンポーネントとして作りつつ、配置場所についての CSS コードだけ、ボタンごとに異なるようにしたい。
方法1: Props
Styled Components において、一部の CSS コードだけを変更するためには、props を使うのが一般的である(詳しくは Styled Components 公式ドキュメントを参照)。例えば、メニューボタンと検索ボタンを、同じ <Button> コンポーネントとして作り、それぞれ左上と右上に配置するには、
// Button.js
const positionButton = css`
position: absolute;
z-index: 1;
${({$topLeft}) => $topLeft && `
top: ${dimension.button['height 25']};
left: ${dimension.button['width 25']};
`}
${({$topRight}) => $topRight && `
top: ${dimension.button['height 25']};
right: ${dimension.button['width 25']};
`}
`;
...
export const Button = styled.button`
${styleCloudButton}
${positionButton}
};
として、<Button>コンポーネントを作り、検索ボタンとして React コンポーネントを作るときに読み込んで $topRight という prop を指定する。
// SearchButton.js
import {Button} from './Button';
const SearchButton = () => {
return (
<Button $topRight />
);
};
export default SearchButton;
メニューボタンは、代わりに $topLeft という prop を指定する。
最初、このやり方で各ボタンを実装したのだが、その後、Arvanitakis (2019) が、Styled Components で props を使うと Webページの表示が遅くなる、と指摘していたのを思い出した。最近、Styled Components を始めとする CSS-in-JS (JavaScript で CSS コードを書くツールの総称)のデメリットとして Web ページ表示速度がよく話題になっている(Dodds 2020, Comeau 2021a)。
でも、どうやって props を使わずに、CSSコードを変更するのか。
方法2: CSS Variables と Style 属性
Comeau (2021a) が、CSS Variables を style属性で指定する、という方法を提案している(Comeau 2021c には、この方法がページの表示をなぜ速くするのかが詳しく書いてある)。まず、<Button>コンポーネントを以下のようにする。
// Button.js
const positionButton = css`
position: absolute;
z-index: 1;
top: var(--button-top, auto); /* REVISED */
left: var(--button-left, auto); /* REVISED */
right: var(--button-right, auto); /* REVISED */
bottom: var(--button-bottom, auto); /* REVISED */
`;
export const Button = styled.button`
${styleCloudButton}
${positionButton}
};
`top`, `left`, `right`, `bottom`が、CSS variables で指定されている。もし、CSS variables の値が、コードの他の場所で指定されないのであれば、デフォルトである `auto` という値が用いられるようにする。
そして、例えば、検索ボタンを React コンポーネントとして作るときに、CSS variables の値を style属性として定義する。
// SearchButton.js
import {Button} from '.Button';
const SearchButton = () => {
return (
<Button style={{ // REVISED
'--button-top': dimension.button['height 25'], // REVISED
'--button-right': dimension.button['width 25'] // REVISED
}} />
);
};
これまで、CSS variables を使ったことがなかった。3年前に CSS を初めて習ったときは話題にもならなかったが、ここ1年ぐらいで急速に普及している(Dodds 2020 を参照)。 また、上記のコード方法は、CSS variables と Styled Components が代替関係にあるのではなく、両者が相互に補完できる関係だということに気づかせてくれた。
そろそろ CSS variables について勉強すべき頃だな、と思い、半日かけて Coyier (2021) を読んで、基本的な使い方をマスターした。
しかし、上記のやり方だと、ボタンについてのCSSコードが、Button.js と SearchButton.js に分散してしまう。Button.js に全て書き込んで、後でボタンのデザインを変更したくなった時にどこを見ればいいか一発でわかるようにしたい。
さて、どうしたものか。
方法3: data 属性セレクター
ダークモード用に色を変更する際に用いられる方法として、data 属性セレクターを使うというのがある(Adhuham 2020, Dodds 2020)。例えば、body要素のスタイルとして
body[data-darkmode="true"] {
...
}
として、ダークモード用の色を指定し、ユーザーがダークモードをオンにするボタンを押したら、body要素に data-darkmode="true" という属性を追加する。(このように data 属性を用いてCSSを指定する方法については、Coyier (2020) を参照)。
このやり方を応用できると気づいた。
検索ボタンを例にとると、まず、SearchButton.js を以下のように書き換える。
// SearchButton.js
import {Button} from '.Button';
const SearchButton = () => {
return (
<Button data-position="top-right" /> {/* REVISED */}
);
};
<Button>コンポーネントの属性として、data-position="top-right" を追加する。
メニューボタンについては、data-position="top-left" とする。
そして、<Button>コンポーネントを以下のようにスタイルする。
// Button.js
const positionButton = css`
position: absolute;
z-index: 1;
/* REVISED FROM HERE */
&[data-position='top-left'] {
top: ${dimension.button['height 25']};
left: ${dimension.button['width 25']};
}
&[data-position='top-right'] {
top: ${dimension.button['height 25']};
right: ${dimension.button['width 25']};
}
/* REVISED UNTIL HERE */
`;
export const Button = styled.button`
${styleCloudButton}
${positionButton}
};
残り二つのボタンについても同様にする。
こうすることで、data-position 属性の値によって、`top`, `bottom`, `left`, `right` プロパティーの値が変わり、結果として、ボタンが表示される場所が変わる。
これで、ボタンについての CSS コードは、Button.js に全て記入されることになる。そして、ボタンの位置を指定するCSSコードは、JavaScriptコードを実行することなく変更され、その分、ボタンが表示されるまでの時間が短縮される。
ページ表示速度テスト結果
実際には、どれだけページが表示されるまでの時間が短縮されるのか。Props を使う方法、data属性セレクターを使う方法、それぞれで制作した web アプリを、Cloudflare Pages でアップし、WebPageTest を用いて、テスト結果を比較してみた。
Props を使った場合のページ表示速度テスト結果(画像元:WebPageTest)
Data 属性セレクターを使った場合のページ表示速度テスト結果(画像元:WebPageTest)
(キャプションのリンクをクリックすると、Cloudflare Pages でホスティングした web アプリを見ることができます。)
Data 属性セレクターを使った方が、わずかに速い。LCP (ページ上で最も大きい UI 要素が表示されるまでの時間)が 0.2秒ほど短縮されている。LCP は、Core Web Vitals の一つの指標として、Google の検索ランキングに影響する (Subramanian 2020) ので、少しでも短い方が望ましい。
次のステップ
前回と今回で、ボタンについて書いてきたが、実はまだ書き終わってない。ダークモード用の配色が残っている。また、ダークモードの切り替えが、静的サイトジェネレータ (Static Site Generator) を使って web アプリを制作しているが故に、なかなかうまくいかなかった。次回の記事で紹介したい。
注: この記事は、英語で書いた同内容の記事を和訳すると同時に日本人向けに編集したものです。
引用文献
Adhuham (2020) “A Complete Guide to Dark Mode on the Web”, CSS-Tricks, Jul 1, 2020.
Arvanitakis, Aggelos (2019) “The unseen performance costs of modern CSS-in-JS libraries in React apps”, Web Performance Calendar, Dec 9, 2019.
Chenais, Louis (2021) “Introduction to design tokens”, Specify Blog, May 27, 2021.
Comeau, Josh (2021a) “The styled-components Happy Path”, joshwcomeau.com, Feb 21, 2021.
Comeau, Josh (2021b) “What the heck, z-index??”, joshwcomeau.com, Jun 9, 2021.
Comeau, Josh (2021c) “Demystifying styled-components”, joshwcomeau.com, Jun 27, 2021.
Coyier, Chris (2020) “A Complete Guide to Data Attributes”, CSS-Tricks, Feb 17, 2020.
Coyier, Chris (2021) “A Complete Guide to Custom Properties”, CSS-Tricks, Apr 27, 2021.
Dodds, Kent C. (2020) “Use CSS Variables instead of React Context”, Epic React, Oct 2020.
Pernice, Kara, and Raluca Budiu (2016) “Hamburger Menus and Hidden Navigation Hurt UX Metrics”, Nielsen Norman Group, Jun 26, 2016.
Rendle, Robin (2019) “What Are Design Tokens?”, CSS-Tricks, Apr 3, 2019.
Schwartz, Barry (2005) “The paradox of choice”, TED, July 2005.
Seno, Broto (2019) “The state of the hamburger menu”, UX Collective, Apr 5, 2019., Apr 5, 2019.
Sherwin, Katie (2014) “The Magnifying-Glass Icon in Search Design: Pros and Cons", Nielsen Norman Group, Feb 23, 2014.
Subramanian, Sowmya (2020) “Evaluating page experience for a better web”, Google Search Central Blog, May 28, 2020.
Syzonenko, Artem (2019) “Buttons on the web: placement and order”, UX Collective, May 26, 2019.
Walton, Philip (2013) “What No One Told You About Z-Index”, philipwalton.com, Jan 15, 2013.
この記事が気に入ったらサポートをしてみませんか?