メニューUIの誤操作を減らせ!
CSSで作るヒットエリア「セーフ・トライアングル」

はてなブックマークへ投稿
85
Xへポスト
URLをコピー
共有

ウェブサイトのメニューUIには、「セーフ・トライアングル」と呼ばれる手法があります。フロントエンドエンジニアがメニューを実装するときに意識すれば、階層メニューのマウス操作でのユーザビリティーを高められます

ウェブでのメニューUIでは、目的のサブメニューへ斜めに進むと、途中の別項目に反応してサブメニューが切り替わることが多々あります。ユーザーが誤反応を避けるには、いったん右へ進んでから垂直に動かすしかありません

▲対策していない階層メニュー。左側の例では、斜め移動で途中の項目が反応。誤反応を避けるには、右側の例のように、格子上の道のり(マンハッタン距離)で動かすしかない。

セーフ・トライアングルとは

この問題を実装で解決するには、マウスポインターと、サブメニューの左上・左下を結んで三角形を作ります。ヒットエリアを拡張することで、斜め方向にマウスを動かしても、誤反応を避けられます。

▲セーフ・トライアングルによって、①で別のメニュー項目が誤反応しない

この手法は「セーフ・トライアングル」のほか、「セーフ・エリア」「セーフ・ポリゴン」「menu-aim」「Prediction cone」などとさまざまな名称で呼ばれています。

CSSのclip-pathで三角形を作る

CSSのclip-path: polygon()とCSS変数を組み合わせると、セーフ・トライアングルをシンプルに実装できます。次のデモで、マウスを斜め方向に動かして試してみてください。

デモ上のEnableでセーフ・トライアングルの有効・無効を、Showで表示・非表示を切り替えられます。

※モバイルではセーフ・トライアングルは無効にしています(理由は後述)。デモはデスクトップでご覧ください。

デモでは、サブメニューの配置にCSSアンカーポジショニングを使っています。ある要素を基準に別の要素を配置できる仕組みで、階層メニューの配置に便利です。

clip-path: polygon()で三角形を指定

疑似要素はposition: fixedで配置します。長方形の位置と大きさはCSS変数で受け取り、clip-pathで三角形に切り抜きます。

.action-item {
  /* ポインターとサブメニューの上下端を結ぶ三角形。 */
  &::before {
    content: "";
    position: fixed;
    /* トリガーやサブメニューより背面に重ねる。 */
    z-index: 2;
    top: var(--safe-top);
    left: var(--safe-left);
    width: var(--safe-width);
    height: var(--safe-height);
    /* ホバー中だけ有効にするため、初期状態では非表示。 */
    display: none;
    clip-path: polygon(
      0 var(--safe-y), /* ① */
      100% 0,  /* ② */
      100% 100% /* ③ */
    );
  }
}

▲三角形の疑似要素

::before疑似要素は長方形ですが、clip-pathによって切り抜かれた三角形の内側だけがマウスに反応します(W3Cの仕様)。

マウスの位置をCSS変数へ

JavaScriptではmousemoveイベントのたびに、疑似要素を囲む長方形の位置・幅・高さと、マウス側の頂点をCSS変数へ渡します。

▲始点はトリガー上のマウス付近。終点はサブメニュー左上と左下

trigger.addEventListener("mousemove", (event) => {
  // 配置済みのサブメニューの矩形から三角形を計算する。
  // (省略)

  // 始点を少し重ね、ポインターが三角形の境界から抜けるのを防ぐ。
  item.style.setProperty("--safe-top", `${menuRect.top}px`);
  item.style.setProperty("--safe-left", `${safeLeft}px`);
  item.style.setProperty("--safe-width", `${Math.max(menuRect.left - safeLeft, 0)}px`);
  item.style.setProperty("--safe-height", `${menuRect.height}px`);
  item.style.setProperty("--safe-y", `${safeY}px`);
});

マウスデバイスだけで有効にする

セーフ・トライアングルはホバー操作にだけ役立ちます。モバイル環境では三角形を配置しないように、CSSのメディアクエリー内で制御します。

.action-item {
  @media (hover: hover) and (pointer: fine) {
    &:hover::before {
      /* 透明な三角形のヒットエリアを有効にする。 */
      display: block;
    }
  }
}

それぞれの意味は次の通りです。

  • (hover: hover)
    • 主な入力機器でホバー操作ができる
  • (pointer: fine)
    • 主な入力機器が、細かい位置を指せるポインターである

コラム:Popover APIとinterestforを使う

ここまでは:hoverをCSSで監視し、サブメニューの表示を切り替えました。2026年現在のHTMLとCSSを使えば、もう少しシンプルに実装できそうです。

クリックから開く処理はpopovertarget属性、ホバーから開く処理はinterestfor属性で代用できます。両方の属性から同じpopover="hint"要素を参照すれば、サブメニューを開閉できます。

次のデモをご覧ください。

<!-- クリック/タップとホバー/フォーカスから同じidを参照する -->
<button
  popovertarget="share-submenu"
  interestfor="share-submenu"
>Share</button>
<ul id="share-submenu" popover="hint">
  <!-- サブメニュー項目 -->
</ul>

interestfor属性の詳細は記事『HTMLのcommandとinterestfor属性を使って、JSを減らすスマートなUI開発』も参照してください。

作例

メガメニュー

左側にカテゴリーがあり、右側に一覧があるようなUIにもセーフ・トライアングルが利用できます。

下側へ開く場合

下方向に配置することも可能です。

ライブラリだと実装が楽に

自前で実装するのが手間だと思った方も多いでしょう。便利なことに、UIライブラリのなかには、セーフ・トライアングルに対応しているものがあります

React Aria

Adobeが開発するReact Ariaは、アクセシブルなUIを組み立てるためのヘッドレスUIライブラリです。ヘッドレスUIのライブラリとしては一番成功を収めているのではないでしょうか(Adobeが開発するウェブのライブラリとしては珍しく成功したもののひとつでしょう)。これもセーフ・トライアングルに似た仕組みが提供されています。

▼実装コード(抜粋)

<SubmenuTrigger delay={0}>
  {/* 第1子が親項目、第2子がサブメニューになる。 */}
  <MenuItem id="share">Share</MenuItem>
  <Popover>
    <Menu>
      <MenuItem id="email">Email</MenuItem>
      <MenuItem id="sms">SMS</MenuItem>
    </Menu>
  </Popover>
</SubmenuTrigger>

React Ariaでは、DOMで三角形を作らないのが特徴です。JavaScriptでポインターの移動方向とタイムアウトを判定し、サブメニューへ向かっている間は親項目を開いたままにします。

本記事では疑似要素で透明なヒットエリアを設ける手法を紹介しましたが、それでも誤判定があるため、JavaScriptでより高い精度を求める方法が検討されたようです。

つまり、セーフ・トライアングルの実装方法はひとつではなく、さまざまな検討がなされている奥深い手法というわけです。

Floating UI

ライブラリ「Floating UI」のReact向けパッケージには、safePolygon()関数とuseHover()関数が用意されています。関数名のとおり、ホバー時にセーフ・トライアングル(このライブラリではセーフ・ポリゴンという名前)としての動的な多角形を使ってくれます。

▼実装コード(抜粋)

// contextはuseFloating()から受け取る。
const hover = useHover(context, {
  mouseOnly: true,
  // 移動経路の背後にある別項目のhoverも防ぐ。
  handleClose: safePolygon({ blockPointerEvents: true }),
});
const { getReferenceProps, getFloatingProps } =
  useInteractions([hover]);

safePolygon()はポインターの移動方向も考慮し、必要なときだけ多角形を作ります。

他にもMUIshadcn/uiなども、セーフ・トライアングルの対応がなされています(内部で組み込まれているサブメニューを扱うライブラリが対応しているため)。

セーフ・トライアングルは昔からある手法

セーフ・トライアングルの考え方は古く、1980年代半ばのMacの階層メニューに採用されていました。当時のMacのインターフェイス設計を振り返った記事に、「a buffer zone shaped like a <」といった記載があります。

ウェブでは2013年にAmazonのメガメニューを分析した記事が話題を呼びました。階層メニューにおける誤反応の対策のひとつが遅延表示だったのにたいして、セーフ・トライアングルを使えば即座に反応できるため、即応性が良いと説明されています。同時に公開されたjQueryプラグイン「jQuery-menu-aim」も注目を集めました。

まとめ

HTMLとCSSで実装すると、矩形でレイアウトを考えがちです。斜め方向にヒットエリアを確保するのは、思い至らないことが多いことでしょう。モバイルファーストと言われて久しいですが、タブレット端末もマウス操作できるなど、マウス操作は依然として役立つ場面が多いです。

小さな改善かもしれませんが、ユーザーの使いやすさのために、ウェブでもセーフ・トライアングルはぜひ検討してみてください!

Google検索でICS MEDIAの記事を見つけやすくしよう

優先するソースに追加すると、GoogleのトップニュースやAI検索でICS MEDIAの記事が表示されやすくなります。

Googleで優先するソースとして追加
SNSでシェアしよう
シェアいただくと、サイト運営の励みになります!
はてなブックマークへ投稿
Xへポスト
URLをコピー
共有
池田 泰延

ICS代表。筑波大学 非常勤講師。ICS MEDIA編集長。個人実験サイト「ClockMaker Labs」のようなビジュアルプログラミングとUIデザインが得意分野です。

この担当の記事一覧