ツールチップはpopover, flip-block, border-shapeを使用するのが最適

popover, flip-block, border-shapeでツールチップを作成

ツールチップはUIの中でもよく使われる要素ですが、実装方法によっては「位置ずれ」「はみ出し」「矢印の調整」など、細かい問題に悩まされがちです。

昔はJavaScriptを使用していましたが、現在はCSSとHTMLの進化により、よりシンプルかつ堅牢に実装できるようになっています。

その中でも特に有効なのが以下の3つです。

  • popover
  • flip-block
  • border-shape

これらを組み合わせることで、軽量かつ柔軟なツールチップを実現できます。

popoverで表示制御をシンプルに

popoverはネイティブでポップアップ表示を扱える仕組みです。

従来のようにposition: absoluteやdisplay: noneを切り替える必要がなく、アクセシビリティも考慮されています。

HTML
<button popovertarget="tooltip-sample">?</button>

<div id="tooltip-sample" class="tooltip" popover>
  これはツールチップです
</div>
これはツールチップです

これだけでクリック時に表示されるポップアップが完成します。

この状態だとまだ見た目がツールチップとは言えませんが、ほかのCSSを組み合わせることでツールチップになります。

flip-blockで画面端のはみ出しを防ぐ

ツールチップでよくある問題が「画面端ではみ出す」ことです。

これを解決するのがCSS Anchor Positioningでposition-tryプロパティにflip-blockを指定する方法です。

これについては説明すると長くなるため、詳しく知りたい方は以下の記事をご覧ください。

CSS Anchor Positioning 仕様の紹介

border-shapeで矢印を作る

ツールチップの矢印は、従来は擬似要素や三角形ハックで実装していました。

border-shapeを使うと、より直感的に自由な形状の矢印を作れます。

新しいツールチップのコード

popover, flip-block, border-shapeで作成したツールチップのコードは以下のようになります。

HTML
<div class="tooltip-wrap">
  <div id="my-tooltip" class="tooltip" popover>
    <p class="content">ツールチップの内容が入ります。スクロール位置によってツールチップの表示位置が変わります。<p>
  </div>
  <button popovertarget="my-tooltip">
    <div>?</div>
  </button>
</div>
CSS
[popover] {
  border: none;
}

[popovertarget] {
  anchor-name: --hint;
  font-size: 1rem;
  font-weight: bold;
  font-family: monospace;
  border-radius: 100%;
  aspect-ratio: 1/1;
  width: 1.5rem;
  padding: 0;
  line-height: 0;
  border: 2px solid #000;
  cursor: pointer;
}

#my-tooltip {
  position-anchor: --hint;
  inset: auto;
  background-color: transparent;
  bottom: anchor(top);
  position-try: flip-block;
  justify-self: anchor-center;
}

.tooltip-wrap {
  display: flex;
  justify-content: center;
  width: 200px;
}

.tooltip {
  container-type: anchored;
  overflow: visible;

  .content {
    --bg-color: orange;
    --border-color: darkorange;
    --r: 10px;
    --ap: 50%;
    --ah: 10px;
    --aw: 10px;

    font-family: system-ui, sans-serif;
    margin: 0;
    background: var(--bg-color);
    color: white;
    font-weight: 400;
    padding: 1rem;
    max-width: 200px;
    line-height: 1.5;
    width: 200px;
    border: 3px solid var(--border-color);
    animation: animate-up 0.1s ease forwards;
    transform-origin: bottom;
    padding: var(--ah) var(--ah) calc(var(--ah) * 2) var(--ah);
    border-shape: shape(from var(--r) 0,
      hline to calc(100% - var(--r)),
      curve to right var(--r) with right top,
      vline to calc(100% - (var(--r) + var(--ah))),
      curve to calc(100% - var(--r)) calc(100% - var(--ah)) with right calc(100% - var(--ah)),
      hline to calc(var(--ap) + var(--aw)),
      line by calc(var(--aw) * -1) var(--ah),
      line by calc(var(--aw) * -1) calc(var(--ah) * -1),
      hline to var(--r),
      curve to left calc(100% - (var(--r) + var(--ah))) with left calc(100% - var(--ah)),
      vline to var(--r),
      curve to var(--r) top with left top);
    }

  @container anchored(fallback: flip-block) {
    .content {
      animation: animate-down 0.1s ease-in forwards;
      transform-origin: top;
      padding: calc(var(--ah) * 2) var(--ah) var(--ah);
      border-shape: shape(from var(--r) var(--ah),
        hline to calc(var(--ap) - var(--aw)),
        line by var(--aw) calc(var(--ah) * -1),
        line by var(--aw) var(--ah),
        hline to calc(100% - var(--r)),
        curve to right calc(var(--ah) + var(--r)) with right calc(var(--ah)),
        vline to calc(100% - var(--r)),
        curve to calc(100% - var(--r)) bottom with right bottom,
        hline to var(--r),
        curve to left calc(100% - var(--r)) with left bottom,
        vline to calc(var(--ah) + var(--r)),
        curve to var(--r) var(--ah) with left var(--ah));
    }
  }
}

@keyframes animate-up {
  from {
    opacity: 0.1;
    scale: 0.8;
  }
  to {
    opacity: 1;
    scale: 1;
    translate: 0 -5px;
  }
}

@keyframes animate-down {
  from {
    opacity: 0.1;
    filter: blur(1px);
    scale: 0.8;
  }
  to {
    opacity: 1;
    filter: blur(0);
    scale: 1;
    translate: 0 5px;
  }
}

ツールチップの内容が入ります。スクロール位置によってツールチップの表示位置が変わります。

↑こちらの「?」をクリックするとツールチップが表示され、スクロールした際に見切れそうになると表示位置が変わることが確認できます。

hoverでツールチップを表示させているWebサイトがありますが、意図せず表示されたり、パソコンとスマートフォンとで表示方法が異なってしまうなどのデメリットがあるので、好ましくないです。

ツールチップの吹き出し形状は、border-shapeの指定が難しそうに見えますが、吹き出しを作るWebツールがあるため、こちらを利用すれば簡単に作れます。

See the Pen Border-shape demo by Una Kravets (@una) on CodePen.

参考: border-shape: the future of the non-rectangular web

まとめ

ツールチップは小さなUIですが、実装の質がUXに直結します。

この方法であれば従来のようなJavaScriptの使用から解放され、アクセシビリティやメンテナンス性も大きく向上します。

今後のツールチップ実装は、この構成がスタンダードになっていくでしょう。