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

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

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

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

▲対策していない階層メニュー。左の斜め移動では途中の項目が反応し、右の逆L字のマウス移動では元の項目を保てる

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

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

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

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

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

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

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

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, 50%), /* ① */
      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 (any-hover: hover) and (any-pointer: fine) {
    &:hover::before {
      /* 透明な三角形のヒットエリアを有効にする。 */
      display: block;
    }
  }
}

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

  • (any-hover: hover)
    • 利用可能な入力機器のいずれかでホバー操作ができる
  • (any-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>

サブメニューの配置にはCSSアンカーポジショニングを使います。

ポップオーバーは開いた後にはじめて位置を計測できます。そのためJavaScriptでは直近のポインター位置を保存し、toggleイベントで開いたことを確認してから三角形を更新します。

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

作例

メガメニュー

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

下側へ開く場合

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

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

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

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()はポインターの移動方向も考慮し、必要なときだけ多角形を作ります。

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でより高い精度を求める方法が検討されたようです。

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

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

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

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

ウェブでは2013年にAmazonのメガメニューを分析した記事が話題を呼びました。同時に公開されたjQueryプラグイン「jQuery-menu-aim」も人気があり、セーフ・トライアングルが広まりました。

まとめ

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

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

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

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

この担当の記事一覧