実務で使う、ツールチップ実装 第1回 ツールチップの形、位置、表示の実装

ブラウザの標準機能以上のカスタマイズされたツールチップの実装について解説します。一定のアクセシビリティを確保しつつ、形、位置、表示などをCSSで実装します。

発行

著者 坂巻 翔大郎 フロントエンド・エンジニア
実務で使う、ツールチップ実装 シリーズの記事一覧

はじめに

本シリーズではツールチップの作り方を解説します。ツールチップとは、マウスなどポインティングデバイスのカーソルを要素に重ね合わせたときに、その周辺に表示される注釈や補足説明のことです。身近なものでは、title属性を指定した要素にマウスカーソルを乗せると、その要素のtitle属性の値がツールチップとして表示されるというものがあります。

a要素のtitle属性

<a href="/favorites/" title="お気に入り一覧">⭐️</a>

title属性によるツールチップで事足りる場合もありますが、見た目に凝りたい・より多くの情報を表示したい場合もあるでしょう。本シリーズでは、そのような場合に役立つツールチップの作り方を解説します。

ツールチップの要件

まず、前提となるツールチップの実装要件ですが、本シリーズでのツールチップの要件は、次の資料を参考に、アクセシビリティを確保しつつ実装することにします。

これらの資料をもとにして、ツールチップの要件を整理します。

  • マウスカーソルを対象に重ね合わせたときに、その周辺(上下左右)に表示
  • マウスカーソルが離れたときに非表示
  • 対象にフォーカスしたときは、その周辺(上下左右)に表示
  • 対象からフォーカスが離れたときは非表示
  • 表示・非表示時には、アニメーションを行う
  • ツールチップが表示されているときにEscキーを押すと非表示
  • ツールチップは、対象要素の子要素として内包される場合(例1)と、兄弟要素として後ろに並んで配置する場合(例2)がある
    • 例1)<a href="#">リンク <span>ツールチップ</span></a>
    • 例2)<input type="text" /><span>ツールチップ</span>

以上の要件を満たすように、ツールチップを作ります。

JSを使用しないツールチップ

まずは、ツールチップを表示させるきっかけになる要素(以降、トリガー要素、あるいは呼び出す要素と呼びます)の子要素としてツールチップを配置する方法から解説します。JSを使用しないためEscキーで閉じることはできませんが、それ以外の要件は満たせます。

まずはJavaScriptを使用しないツールチップの完成形から見てみましょう。

HTMLとCSSの全体像も見ていきましょう。HTMLは次のようにします。

ツールチップのHTML全体像

<a class="TooltipContainer" href="#">
  リンク(ツールチップ上)
  <span class="Tooltip -top" role="tooltip">
    <span class="Tooltip_Body">
      テキストが入ります。テキストが入ります。
    </span>
  </span>
</a>

ツールチップ自身は、.Tooltipとしています。role="tooltipを指定することで、この要素がツールチップであることを示しています。ツールチップの位置は、.Tooltipのクラスに-top-right-bottom-leftのいずれかを指定します。これらのクラスは、ツールチップの位置を指定するためのものです。-topを指定すれば、トリガー要素の上に表示されます。

ツールチップは、トリガー要素を基点として、周囲の上下左右に表示されます。ですので、その基点となる要素には.TooltipContainerというクラスを指定します。

次にCSSです。CSSでは、ツールチップの上下左右の位置調整と、アニメーションの指定、ツールチップの表示・非表示について指定するため、記述が複雑になります。まずはCSS全体を見てみましょう。

ツールチップのCSS全体像

.Tooltip {
  --_bg: #000;
  --_color: #fff;
  --_triangle-size: 5px;
  --_gap: 3px;

  position: absolute;
  z-index: 1;
  display: block;

  /* ツールチップのみため */
  width: max-content;
  max-width: 150px;
  padding: 10px;
  background-color: var(--_bg);
  color: var(--_color);
  text-align: left;
  border-radius: 8px;
  font-size: 12px;

  /* 表示・非表示の指定 */
  visibility: hidden;
  opacity: 0;
}

/* ツールチップが上に表示される場合 */
.Tooltip.-top {
  bottom: calc(100% + var(--_triangle-size) + var(--_gap));
  left: 50%;
  translate: -50% var(--_y, 0);
}

/* ツールチップが下に表示される場合 */
.Tooltip.-bottom {
  top: calc(100% + var(--_triangle-size) + var(--_gap));
  left: 50%;
  translate: -50% var(--_y, 0);
}

/* ツールチップが右に表示される場合 */
.Tooltip.-right {
  bottom: 50%;
  left: calc(100% + var(--_triangle-size) + var(--_gap));
  translate: var(--_x, 0) 50%;
}

/* ツールチップが左に表示される場合 */
.Tooltip.-left {
  bottom: 50%;
  right: calc(100% + var(--_triangle-size) + var(--_gap));
  translate: var(--_x, 0) 50%;
}

/*
  △を擬似要素で作成
*/
.Tooltip::before {
  position: absolute;
  margin: auto;
  content: "";
  display: block;
  width: 0;
  height: 0;
  border-style: solid;
}

/* ツールチップが上に表示される場合の△ */
.Tooltip.-top::before {
  bottom: calc(var(--_triangle-size) * -1);
  right: 0;
  left: 0;
  border-width: var(--_triangle-size) var(--_triangle-size) 0 var(--_triangle-size);
  border-color: var(--_bg) transparent transparent transparent;
}

/* ツールチップが下に表示される場合の△ */
.Tooltip.-bottom::before {
  top: calc(var(--_triangle-size) * -1);
  right: 0;
  left: 0;
  border-width: 0 var(--_triangle-size) var(--_triangle-size) var(--_triangle-size);
  border-color: transparent transparent var(--_bg) transparent;
}

/* ツールチップが左に表示される場合の△ */
.Tooltip.-left::before {
  top: 0;
  bottom: 0;
  right: calc(var(--_triangle-size) * -1);
  border-width: var(--_triangle-size) 0 var(--_triangle-size) var(--_triangle-size);
  border-color: transparent transparent transparent var(--_bg);
}

/* ツールチップが右に表示される場合の△ */
.Tooltip.-right::before {
  top: 0;
  bottom: 0;
  left: calc(var(--_triangle-size) * -1);
  border-width: var(--_triangle-size) var(--_triangle-size) var(--_triangle-size) 0;
  border-color: transparent var(--_bg) transparent transparent;
}

/* 透明な要素を背面に敷くことでツールチップにマウスをのせやすくする */
.Tooltip::after {
  position: absolute;
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  z-index: -1;
}

/* ツールチップが上に表示される場合 */
.Tooltip.-top::after {
  top: 100%;
  left: 0;
  height: calc(var(--_triangle-size) + var(--_gap));
}

/* ツールチップが下に表示される場合 */
.Tooltip.-bottom::after {
  bottom: 100%;
  left: 0;
  height: calc(var(--_triangle-size) + var(--_gap));
}

/* ツールチップが左に表示される場合 */
.Tooltip.-left::after {
  top: 0;
  left: 100%;
  width: calc(var(--_triangle-size) + var(--_gap));
}

/* ツールチップが右に表示される場合 */
.Tooltip.-right::after {
  top: 0;
  right: 100%;
  width: calc(var(--_triangle-size) + var(--_gap));
}

/*
  ツールチップがあることを、支援技術に伝えるための非表示テキスト
*/
.Tooltip_Body {
  display: block;
}
.Tooltip_Body::before {
  content: "。ツールチップあり:";
  position: absolute;
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
}

/*
  対象の要素の周囲に表示されるツールチップを上下左右に配置するために親要素をrelativeにする
  ※親要素のpositionの値を自身で調整したい場合は不要
*/
.TooltipContainer {
  position: relative;
  display: inline-block;
}

/*
  ホバー、フォーカス、アクティブ時にツールチップを表示する
*/
.TooltipContainer:is(:hover, :focus-visible, :focus-within, :active) > .Tooltip {
  visibility: visible;
  opacity: 1;
}

/*
  no-preferenceはprefers-reduced-motionの設定を行なっていない場合
  アニメーションさせる
*/
@media (prefers-reduced-motion: no-preference) {
  /* フェードアウト用の指定 */
  .Tooltip {
    transition: visibility 0s linear 0.1s, opacity 0.1s, translate 0.1s;
  }
  /* フェードイン用の指定 */
  .TooltipContainer:is(:hover, :focus-visible, :focus-within, :active) > .Tooltip {
    transition: visibility 0s linear 0s, opacity 0.3s, translate 0.3s;
  }
  /*
    ホバー・フォーカス・アクティブ時にツールチップをCSS Transitionさせるため
    ホバー・フォーカス・アクティブ時でないときに、初期位置を指定する
  */
  .TooltipContainer:not(:is(:hover, :focus-visible, :focus-within, :active)) > .Tooltip.-top {
    --_y: var(--_gap);
  }
  .TooltipContainer:not(:is(:hover, :focus-visible, :focus-within, :active)) > .Tooltip.-bottom   {
    --_y: calc( var(--_gap) * -1);
  }
  .TooltipContainer:not(:is(:hover, :focus-visible, :focus-within, :active)) > .Tooltip.-left {
    --_x: var(--_gap);
  }
  .TooltipContainer:not(:is(:hover, :focus-visible, :focus-within, :active)) > .Tooltip.-right {
    --_x: calc(var(--_gap) * -1);
  }  
}

CSSが長くて、ちょっと面食らったかもしれませんが、ひとつひとつポイントを見ていけば大丈夫です。CSSのポイントとなる部分を解説します。

ツールチップのテキストが入る部分のスタイル

まずはツールチップのテキストが入る黒い角丸の部分です。

黒い角丸のCSS

.Tooltip {
  --_bg: #000;
  --_color: #fff;
  --_triangle-size: 5px;
  --_gap: 3px;

  position: absolute;
  z-index: 1;
  display: block;

  /* ツールチップのみため */
  width: max-content;
  max-width: 150px;
  padding: 10px;
  background-color: var(--_bg);
  color: var(--_color);
  text-align: left;
  border-radius: 8px;
  font-size: 12px;

  /* 表示・非表示の指定 */
  visibility: hidden;
  opacity: 0;
  transition: 0s visibility, 0.2s opacity, 0.2s translate;
}

ツールチップのテキストが入る部分は、Tooltipというクラス名で指定しています。冒頭で宣言されているカスタムプロパティは次の意味があります。

カスタムプロパティ名 役割
--_bg ツールチップの背景色
--_color 文字色
--_triangle-size ツールチップの△の大きさ
--_gap ツールチップと対象の要素の間の距離

これらのカスタムプロパティはツールチップ内で使用されます。--_color--_bgは、簡単に色を変えられるようカスタムプロパティとして宣言しています。

補足:カスタムプロパティの名前の_の意味

筆者の最近の流行りで、カスタムプロパティの名前の先頭に_をつけることがあります。_から始まるカスタムプロパティはこのUIでのみ使用され、他のUIとは共有しないことを示すものです。

width: max-content;とするとテキストが1行収まる幅になります。

テキストが1行に収まる(最大幅は150px)

width: max-content;
max-width: 150px;

ですが、どこまでも大きくなってほしくはないので最大幅を指定(max-width: 150px;)します。

三角形の作り方

次に、ツールチップの吹き出しのしっぽとなる三角形の部分です。三角形は擬似要素とborderプロパティを使用して作ります。

吹き出しの三角形部分

.Tooltip::before {
  position: absolute;
  margin: auto;
  content: "";
  display: block;
  width: 0;
  height: 0;
  border-style: solid;
}

/* ツールチップが上に表示される場合の△ */
.Tooltip.-top::before {
  bottom: calc(var(--_triangle-size) * -1);
  right: 0;
  left: 0;
  border-width: var(--_triangle-size) var(--_triangle-size) 0 var(--_triangle-size);
  border-color: var(--_bg) transparent transparent transparent;
}

このCSSでは、擬似要素で三角形を作成し、表示位置に応じて三角形の位置と向きを調整しています。三角形を作って配置する際に、bottomやborderプロパティに三角形の大きさを指定する必要があります。この三角形の大きさは何度も利用する値となるため、--_triangle-sizeというカスタムプロパティとして定義しています。

ツールチップの表示される位置によって、三角形の位置と向きが変わります。ツールチップがトリガー要素の上に表示される場合、三角形はツールチップの下側に下向きで配置されます。

三角形をborderプロパティを用いて作る場合、幅と高さを0にした要素に対して、borderプロパティを指定すると、要素の中心に向いた4つの三角形ができます。下向きの三角形を作りたい場合は、下側の線幅(border-bottom-width)を0にし、その左右の線の色を透明(transparent)にします。

筆者の場合は、borderプロパティを用いて三角形を作ることが多いですが、三角形を作る方法は他にもいくつかあります。clip-pathプロパティを使用する方法や、conic-gradientを使用する方法もあります。興味があれば調べてみてください。

borderプロパティを用いた三角形

次の記事でもborderプロパティと::before疑似要素を用いた三角形の作成方法を解説しています。参考にしてください。

ここまでで、ツールチップのテキストが入る角丸部分と、三角形の部分ができました。次は、ツールチップの表示位置についてです。

ツールチップの表示位置を決めるスタイル

ツールチップの表示位置は、上下左右の位置ごとに指定を書く必要があります。上に表示する場合(.-top)は次のようになります。

表示位置をトリガー要素の上部に

.Tooltip.-top {
  bottom: calc(100% + var(--_triangle-size) + var(--_gap));
  left: 50%;
  translate: -50% var(--_y, 0);
}

トリガー要素の上側に表示するための指定です。

bottomプロパティでは、トリガー要素の下側の位置からツールチップの下側までの距離を計算しています。calc内の指定は、トリガー要素の高さと、ツールチップに付随する三角形の高さツールチップとトリガー要素の間の距離を合計したものになります。

三角形は(position: absolute;のため)ツールチップ本体の高さに含まれません。なので、トリガー要素の高さ+ツールチップ+トリガー要素の間の余白を合計しても、次のようにちょっと三角形の分だけ、ずれて見えてしまいます。

さらにトリガー要素の水平中央に表示されてほしいので、left: 50%;とあわせてtranslateプロパティを指定します。

translateプロパティの1つ目の値は、x軸の指定で-50%とし、ツールチップ自身の幅の半分だけ位置をずらしています。2つ目の値で、y軸の指定はvar(--y, 0)としていて、これは後述するアニメーションのための指定です。

位置調整はわかりにくいかもしれませんので、段階的に説明する画像を用意しました。

absoluteのみでは、トリガー要素の左上に位置するだけです。

bottom: 100%;を指定すると、ツールチップの底辺が、トリガー要素の上側に接して配置されます。

三角形はツールチップ内でabsoluteで配置されているため、ツールチップの高さには含まれません。ですので、三角の高さ(--_triangle-size)の分だけ位置をずらす必要があります。さらに、ツールチップとトリガー要素の間には少しの距離を空けたいので、--_gapの分だけ位置をずらします。

トリガー要素の水平中心に配置するために、まずはトリガー要素の半分(50%)だけ位置をずらします。

そして、translateプロパティを使用して、自身の幅の半分だけ位置を戻します。leftプロパティとtranslateプロパティを組み合わせることで、ツールチップをトリガー要素の水平中央に配置できます。

ツールチップを上側に配置するための指定ができました。

残りも位置ごとに調整したCSSを書いていきますが、調整方法は同じような形になるため、ここでの解説は省略します。気になる方はコードの全体像を見てみてください。

ツールチップ表示・非表示用のスタイル

ツールチップの位置を定めるCSSを理解したところで、次はツールチップの表示・非表示のスタイルをCSSで実装しましょう。

表示・非表示はアニメーションを伴うため、displayプロパティによる表示・非表示ではなく、visibilityプロパティとopacityプロパティを使用します。非表示にしておくCSSは次のようになります。

ツールチップを非表示

.Tooltip {
  /* 省略... */
  visibility: hidden;
  opacity: 0;
}

そして、トリガー要素に、マウスカーソルが重なった・フォーカスした・アクティブになったときに、.Tooltipvisibilityopacityの値を変えて、ツールチップを表示するようにします。

:is()はセレクターリストの中の、いずれか1つでも合致する要素を選択します。詳しくは、次の記事を参照してください。

ツールチップを表示

.TooltipContainer:is(
  :hover,
  :focus-visible,
  :focus-within,
  :active
) > .Tooltip {
  visibility: visible;
  opacity: 1;
}

【ワンポイント】 書き方のバリエーション

筆者は:is()を使用して記述を簡単にしていますが、次のように書いても同様の意味となります。

.TooltipContainer:hover > .Tooltip,
.TooltipContainer:focus-visible > .Tooltip,
.TooltipContainer:focus-within > .Tooltip,
.TooltipContainer:active > .Tooltip{
  visibility: visible;
  opacity: 1;
}

ここまで解説してきたものをデモで確認してみましょう。デモではツールチップの表示位置をすべて確認できるように4つ並べています。

ツールチップの形を作るところから、ツールチップを表示するところまでを実装しました。

ここまでのまとめ

今回は、以下について解説しました。

  • ツールチップの要件の整理
  • JavaScriptを使用しないツールチップのコード全体の確認
  • 吹き出し部分の作り方
  • 表示位置の調整方法

次回は、ツールチップが表示・非表示される際のアニメーションの適用と、使い勝手を向上する方法を解説します。