表示例
アイコン: Font Awesome 7 Free(fa-solid / fa-regular)
別名・英語表記
- ワッフルメニュー
- アプリ切り替え
- サービス切り替え
- お弁当メニュー
- アプリ一覧ボタン
- 9つの点のメニュー
- App launcher
実装例
<div class="x-app-launcher">
<div class="x-app-launcher__bar">
<span class="x-app-launcher__logo">Puchi Works</span>
<span class="x-app-launcher__w">
<button class="x-app-launcher__b" type="button" aria-label="アプリ一覧" aria-expanded="true" aria-controls="x-app-launcher-p">
<svg class="x-app-launcher__dots" viewBox="0 0 18 18" width="18" height="18" aria-hidden="true" focusable="false"><circle cx="2" cy="2" r="2" fill="currentColor"/><circle cx="9" cy="2" r="2" fill="currentColor"/><circle cx="16" cy="2" r="2" fill="currentColor"/><circle cx="2" cy="9" r="2" fill="currentColor"/><circle cx="9" cy="9" r="2" fill="currentColor"/><circle cx="16" cy="9" r="2" fill="currentColor"/><circle cx="2" cy="16" r="2" fill="currentColor"/><circle cx="9" cy="16" r="2" fill="currentColor"/><circle cx="16" cy="16" r="2" fill="currentColor"/></svg>
</button>
<!-- 見本なので開いた状態。閉じるときは hidden 属性を付け、aria-expanded を false にする。
中身は行き先のリンクなので role="menu" は付けない -->
<div class="x-app-launcher__p" id="x-app-launcher-p">
<ul class="x-app-launcher__grid" role="list">
<li><a class="x-app-launcher__a" href="/mail" aria-current="page"><i class="fa-regular fa-envelope x-app-launcher__ic x-app-launcher__ic--1" aria-hidden="true"></i>メール</a></li>
<li><a class="x-app-launcher__a" href="/calendar"><i class="fa-regular fa-calendar x-app-launcher__ic x-app-launcher__ic--2" aria-hidden="true"></i>カレンダー</a></li>
<li><a class="x-app-launcher__a" href="/drive"><i class="fa-regular fa-folder x-app-launcher__ic x-app-launcher__ic--3" aria-hidden="true"></i>ドライブ</a></li>
<li><a class="x-app-launcher__a" href="/docs"><i class="fa-regular fa-file-lines x-app-launcher__ic x-app-launcher__ic--4" aria-hidden="true"></i>文書</a></li>
<li><a class="x-app-launcher__a" href="/chat"><i class="fa-regular fa-comment x-app-launcher__ic x-app-launcher__ic--5" aria-hidden="true"></i>チャット</a></li>
<li><a class="x-app-launcher__a" href="/photos"><i class="fa-regular fa-image x-app-launcher__ic x-app-launcher__ic--6" aria-hidden="true"></i>写真</a></li>
</ul>
</div>
</span>
</div>
</div>
/* 貼った先のリセットに頼らないよう、この部品の中だけ border-box にする */
.x-app-launcher,
.x-app-launcher *:not([class*="fa-"]),
.x-app-launcher *:not([class*="fa-"])::before,
.x-app-launcher *:not([class*="fa-"])::after {
box-sizing: border-box;
/* 貼った先の既定の書体に引きずられないようにする。書体が変わると
文字の幅と高さが変わり、枠のサイズまでずれる */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", "Yu Gothic UI", Meiryo, sans-serif;
}
/* 外枠はヘッダーの右上を切り取った見本。パネルが下へ重なって開く */
.x-app-launcher {
width: 300px;
max-width: 100%;
min-height: 210px;
color: #1b2028;
}
.x-app-launcher__bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
padding: 0 8px 0 14px;
border: 1px solid #7f8a99;
border-radius: 8px;
background: #fff;
}
.x-app-launcher__logo {
font-size: 14px;
font-weight: 700;
}
.x-app-launcher__w {
position: relative;
display: inline-flex;
}
.x-app-launcher__b {
display: grid;
place-items: center;
width: 36px;
height: 36px;
padding: 0;
border: 0;
border-radius: 50%;
background: none;
color: #3c4452;
cursor: pointer;
}
.x-app-launcher__b:hover,
.x-app-launcher__b[aria-expanded="true"] {
background: #e3e6ea;
}
.x-app-launcher__b:focus-visible,
.x-app-launcher__a:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 1px;
}
/* 3×3 の点(ワッフル)。1つの点を box-shadow で9つに増やして描く。
左上の点を中央からずらし、影で残りの8つを置く */
.x-app-launcher__dots {
display: block;
}
.x-app-launcher__p {
position: absolute;
top: calc(100% + 10px);
right: -4px;
z-index: 10;
width: 252px;
padding: 10px;
border: 1px solid #7f8a99;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 20px rgba(27, 32, 40, 0.16);
}
.x-app-launcher__p[hidden] {
display: none;
}
.x-app-launcher__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
}
.x-app-launcher__a {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px 2px 8px;
border-radius: 8px;
color: #1b2028;
font-size: 11.5px;
text-decoration: none;
}
.x-app-launcher__a:hover {
background: #f1f3f6;
}
/* いま使っているアプリ。色だけでなく枠でも示す */
.x-app-launcher__a[aria-current="page"] {
background: #eef2fb;
box-shadow: inset 0 0 0 1px #1d4ed8;
}
.x-app-launcher__ic {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 10px;
color: #fff;
font-size: 17px;
}
/* アイコンの地は白い絵柄と 4.5:1 以上になる濃さにそろえる */
.x-app-launcher__ic--1 { background: #b91c1c; }
.x-app-launcher__ic--2 { background: #1d4ed8; }
.x-app-launcher__ic--3 { background: #a16207; }
.x-app-launcher__ic--4 { background: #0369a1; }
.x-app-launcher__ic--5 { background: #15803d; }
.x-app-launcher__ic--6 { background: #7e22ce; }
AIへの指示文
「アプリランチャー(App launcher)」を実装してください。
## これは何か
ヘッダーの右上にある、点が3×3に並んだボタン。押すと同じ会社のほかのアプリやサービスが、アイコンと名前のタイルで並んで開く。
## 使いどころ
- メール・カレンダー・ドライブのように、1つのアカウントで使う複数のサービスを行き来させたいとき
- どのサービスの画面にいても、同じ場所から他のサービスへ移れるようにしたいとき
## 実装の要件
- button[aria-label][aria-expanded][aria-controls] + div[id][hidden] > ul > li > a の構成にし、パネルはボタンの直後に置く(Tab の順がボタン → タイルになる)
- パネルはボタンの入れ物を position: relative; にし、position: absolute; top: 100%; right: 0; で右端をそろえて下へ重ねる。タイルは display: grid; grid-template-columns: repeat(3, 1fr); で並べる
- パネルに display を指定したら、パネルのクラスに絞って .パネル[hidden] { display: none; } も書く。書かないと hidden 属性を付けても閉じない([hidden] 全体に書くと hidden="until-found" が検索で開かなくなる)
- 3×3 の点は Font Awesome の無料版に無いので、SVG か、1つの点を box-shadow で9つに増やして描く
- ハンバーガーメニュー(navigation/hamburger)はいまのサイトの中の行き先をしまうもの、アプリランチャーは別のアプリやサービスへ移るためのもの。1つのヘッダーに両方置くときは、アイコンの形を変えて取り違えないようにする
- メガメニュー(navigation/mega-menu)はグローバルナビの項目から同じサイトの下の階層を並べるもの。アプリランチャーは中身がサービスの一覧で、階層を持たない
- サービスが3つ以下なら、パネルにしまわずヘッダーにリンクを並べたほうが早い
- 並び順を利用状況で勝手に入れ替えない。場所で覚えている人が迷う。いま使っているサービスは見て分かる印を付ける
## アクセシビリティの要件
- WAI-ARIA APG の Disclosure の形にする。button に aria-expanded と aria-controls を付け、パネルは hidden 属性で出し入れする
- 中身は別のサービスへの行き先なので、ul > li > a のリンクの一覧にし、role="menu" は付けない。Tab でタイルを順に移れる
- 点のアイコンだけのボタンなので、aria-label で「アプリ一覧」のように名前を付ける
- いまのサービスのタイルには aria-current="page" を付け、色だけでなく枠でも示す(WCAG 1.4.1 色の用途)
- Esc でパネルを閉じてボタンへフォーカスを戻す。フォーカスがパネルの外へ出たときにも閉じる
## 参考にする実装
この形を出発点にして、プロジェクトの命名規則・色・余白に合わせて調整してください。
```html
<div class="x-app-launcher">
<div class="x-app-launcher__bar">
<span class="x-app-launcher__logo">Puchi Works</span>
<span class="x-app-launcher__w">
<button class="x-app-launcher__b" type="button" aria-label="アプリ一覧" aria-expanded="true" aria-controls="x-app-launcher-p">
<svg class="x-app-launcher__dots" viewBox="0 0 18 18" width="18" height="18" aria-hidden="true" focusable="false"><circle cx="2" cy="2" r="2" fill="currentColor"/><circle cx="9" cy="2" r="2" fill="currentColor"/><circle cx="16" cy="2" r="2" fill="currentColor"/><circle cx="2" cy="9" r="2" fill="currentColor"/><circle cx="9" cy="9" r="2" fill="currentColor"/><circle cx="16" cy="9" r="2" fill="currentColor"/><circle cx="2" cy="16" r="2" fill="currentColor"/><circle cx="9" cy="16" r="2" fill="currentColor"/><circle cx="16" cy="16" r="2" fill="currentColor"/></svg>
</button>
<!-- 見本なので開いた状態。閉じるときは hidden 属性を付け、aria-expanded を false にする。
中身は行き先のリンクなので role="menu" は付けない -->
<div class="x-app-launcher__p" id="x-app-launcher-p">
<ul class="x-app-launcher__grid" role="list">
<li><a class="x-app-launcher__a" href="/mail" aria-current="page"><i class="fa-regular fa-envelope x-app-launcher__ic x-app-launcher__ic--1" aria-hidden="true"></i>メール</a></li>
<li><a class="x-app-launcher__a" href="/calendar"><i class="fa-regular fa-calendar x-app-launcher__ic x-app-launcher__ic--2" aria-hidden="true"></i>カレンダー</a></li>
<li><a class="x-app-launcher__a" href="/drive"><i class="fa-regular fa-folder x-app-launcher__ic x-app-launcher__ic--3" aria-hidden="true"></i>ドライブ</a></li>
<li><a class="x-app-launcher__a" href="/docs"><i class="fa-regular fa-file-lines x-app-launcher__ic x-app-launcher__ic--4" aria-hidden="true"></i>文書</a></li>
<li><a class="x-app-launcher__a" href="/chat"><i class="fa-regular fa-comment x-app-launcher__ic x-app-launcher__ic--5" aria-hidden="true"></i>チャット</a></li>
<li><a class="x-app-launcher__a" href="/photos"><i class="fa-regular fa-image x-app-launcher__ic x-app-launcher__ic--6" aria-hidden="true"></i>写真</a></li>
</ul>
</div>
</span>
</div>
</div>
```
```css
/* 貼った先のリセットに頼らないよう、この部品の中だけ border-box にする */
.x-app-launcher,
.x-app-launcher *:not([class*="fa-"]),
.x-app-launcher *:not([class*="fa-"])::before,
.x-app-launcher *:not([class*="fa-"])::after {
box-sizing: border-box;
/* 貼った先の既定の書体に引きずられないようにする。書体が変わると
文字の幅と高さが変わり、枠のサイズまでずれる */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", "Yu Gothic UI", Meiryo, sans-serif;
}
/* 外枠はヘッダーの右上を切り取った見本。パネルが下へ重なって開く */
.x-app-launcher {
width: 300px;
max-width: 100%;
min-height: 210px;
color: #1b2028;
}
.x-app-launcher__bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
padding: 0 8px 0 14px;
border: 1px solid #7f8a99;
border-radius: 8px;
background: #fff;
}
.x-app-launcher__logo {
font-size: 14px;
font-weight: 700;
}
.x-app-launcher__w {
position: relative;
display: inline-flex;
}
.x-app-launcher__b {
display: grid;
place-items: center;
width: 36px;
height: 36px;
padding: 0;
border: 0;
border-radius: 50%;
background: none;
color: #3c4452;
cursor: pointer;
}
.x-app-launcher__b:hover,
.x-app-launcher__b[aria-expanded="true"] {
background: #e3e6ea;
}
.x-app-launcher__b:focus-visible,
.x-app-launcher__a:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 1px;
}
/* 3×3 の点(ワッフル)。1つの点を box-shadow で9つに増やして描く。
左上の点を中央からずらし、影で残りの8つを置く */
.x-app-launcher__dots {
display: block;
}
.x-app-launcher__p {
position: absolute;
top: calc(100% + 10px);
right: -4px;
z-index: 10;
width: 252px;
padding: 10px;
border: 1px solid #7f8a99;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 20px rgba(27, 32, 40, 0.16);
}
.x-app-launcher__p[hidden] {
display: none;
}
.x-app-launcher__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
}
.x-app-launcher__a {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px 2px 8px;
border-radius: 8px;
color: #1b2028;
font-size: 11.5px;
text-decoration: none;
}
.x-app-launcher__a:hover {
background: #f1f3f6;
}
/* いま使っているアプリ。色だけでなく枠でも示す */
.x-app-launcher__a[aria-current="page"] {
background: #eef2fb;
box-shadow: inset 0 0 0 1px #1d4ed8;
}
.x-app-launcher__ic {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 10px;
color: #fff;
font-size: 17px;
}
/* アイコンの地は白い絵柄と 4.5:1 以上になる濃さにそろえる */
.x-app-launcher__ic--1 { background: #b91c1c; }
.x-app-launcher__ic--2 { background: #1d4ed8; }
.x-app-launcher__ic--3 { background: #a16207; }
.x-app-launcher__ic--4 { background: #0369a1; }
.x-app-launcher__ic--5 { background: #15803d; }
.x-app-launcher__ic--6 { background: #7e22ce; }
```
## 出力の約束
- HTML と CSS を分けて出す。JavaScript が要るときは素の JavaScript で書き、UIフレームワークは使わない。
- 参考実装がアイコンに Font Awesome のクラスを使っている場合は、プロジェクトで使っているアイコンかインライン SVG に置き換えてよい。
- 上のアクセシビリティ要件を満たす。満たせない箇所があるときは、理由を添えて先に知らせる。
- 色・角丸・余白は既存のデザイントークンがあればそれを使う。無ければ CSS 変数として切り出す。
使いどころ
- メール・カレンダー・ドライブのように、1つのアカウントで使う複数のサービスを行き来させたいとき
- どのサービスの画面にいても、同じ場所から他のサービスへ移れるようにしたいとき
注意点
- ハンバーガーメニュー(navigation/hamburger)はいまのサイトの中の行き先をしまうもの、アプリランチャーは別のアプリやサービスへ移るためのもの。1つのヘッダーに両方置くときは、アイコンの形を変えて取り違えないようにする
- メガメニュー(navigation/mega-menu)はグローバルナビの項目から同じサイトの下の階層を並べるもの。アプリランチャーは中身がサービスの一覧で、階層を持たない
- サービスが3つ以下なら、パネルにしまわずヘッダーにリンクを並べたほうが早い
- 並び順を利用状況で勝手に入れ替えない。場所で覚えている人が迷う。いま使っているサービスは見て分かる印を付ける
アクセシビリティの要点
- WAI-ARIA APG の Disclosure の形にする。button に aria-expanded と aria-controls を付け、パネルは hidden 属性で出し入れする
- 中身は別のサービスへの行き先なので、ul > li > a のリンクの一覧にし、role="menu" は付けない。Tab でタイルを順に移れる
- 点のアイコンだけのボタンなので、aria-label で「アプリ一覧」のように名前を付ける
- いまのサービスのタイルには aria-current="page" を付け、色だけでなく枠でも示す(WCAG 1.4.1 色の用途)
- Esc でパネルを閉じてボタンへフォーカスを戻す。フォーカスがパネルの外へ出たときにも閉じる