Tooltip
要素がキーボードフォーカスを受けたとき、またはマウスがホバーしたときに、要素に関連する情報を表示するポップアップ。
🤖 AI Implementation Guideデモ
アクセシビリティ
WAI-ARIA ロール
-
tooltip- 要素の説明を表示するコンテキストポップアップ
WAI-ARIA tooltip role (opens in new tab)
WAI-ARIA ステート & プロパティ
aria-describedby
トリガー要素にアクセシブルな説明を提供するために、ツールチップ要素を参照します。
| 適用先 | トリガー要素(ラッパー) |
| タイミング | ツールチップが表示されている時のみ |
| 参照 | aria-describedby (opens in new tab) |
aria-hidden
ツールチップが支援技術から隠されているかどうかを示します。
| 値 | true(非表示)| false(表示)
|
| デフォルト | true |
| 参照 | aria-hidden (opens in new tab) |
キーボードサポート
| キー | アクション |
|---|---|
| Escape | ツールチップを閉じる |
| Tab | 標準のフォーカスナビゲーション。トリガーがフォーカスを受け取るとツールチップが表示される |
フォーカス管理
- ツールチップはフォーカスを受け取らない - APGに従い、ツールチップはフォーカス可能であってはいけません。インタラクティブなコンテンツが必要な場合は、DialogまたはPopoverパターンを使用してください。
- フォーカスが表示をトリガーする - トリガー要素がフォーカスを受け取ると、設定された遅延後にツールチップが表示されます。
- ぼかしがツールチップを非表示にする - フォーカスがトリガー要素を離れると、ツールチップは非表示になります。
マウス/ポインター動作
- ホバーが表示をトリガーする - ポインターをトリガー上に移動すると、遅延後にツールチップが表示されます。
- ポインター離脱で非表示 - ポインターをトリガーから離すと、ツールチップが非表示になります。
重要な注意事項
注意: APG Tooltipパターンは現在WAIによって「作業中」とマークされています。この実装は文書化されたガイドラインに従っていますが、仕様は進化する可能性があります。 View APG Tooltip Pattern (opens in new tab)
ビジュアルデザイン
この実装は、ツールチップの視認性のベストプラクティスに従っています:
- 高コントラスト - 暗い背景に明るいテキストで可読性を確保
- ダークモード対応 - ダークモードで色が適切に反転
- トリガーの近くに配置 - ツールチップはトリガー要素に隣接して表示
- 設定可能な遅延 - カーソル移動中の誤作動を防止
ソースコード
Tooltip.astro
---
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
export interface Props {
/** Tooltip content */
content: string;
/** Default open state */
defaultOpen?: boolean;
/** Delay before showing tooltip (ms) */
delay?: number;
/** Tooltip placement */
placement?: TooltipPlacement;
/** Custom tooltip ID */
id?: string;
/** Whether the tooltip is disabled */
disabled?: boolean;
/** Additional class name for the wrapper */
class?: string;
/** Additional class name for the tooltip content */
tooltipClass?: string;
}
const {
content,
defaultOpen = false,
delay = 300,
placement = 'top',
id,
disabled = false,
class: className = '',
tooltipClass = '',
} = Astro.props;
const tooltipId = id ?? `tooltip-${crypto.randomUUID().slice(0, 8)}`;
const placementClasses: Record<TooltipPlacement, string> = {
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
};
---
<apg-tooltip
class:list={['apg-tooltip-trigger', 'relative inline-block', className]}
data-delay={delay}
data-disabled={disabled ? 'true' : undefined}
data-tooltip-id={tooltipId}
data-default-open={defaultOpen ? 'true' : undefined}
>
<slot />
<span
id={tooltipId}
role="tooltip"
aria-hidden="true"
class:list={[
'apg-tooltip',
'absolute z-50 px-3 py-1.5 text-sm',
'rounded-md bg-gray-900 text-white shadow-lg',
'dark:bg-gray-100 dark:text-gray-900',
'pointer-events-none whitespace-nowrap',
'transition-opacity duration-150',
placementClasses[placement],
'invisible opacity-0',
tooltipClass,
]}
>
{content}
</span>
</apg-tooltip>
<script>
class ApgTooltip extends HTMLElement {
private timeout: ReturnType<typeof setTimeout> | null = null;
private isOpen = false;
private tooltipEl: HTMLElement | null = null;
private delay: number;
private disabled: boolean;
private tooltipId: string;
constructor() {
super();
this.delay = 300;
this.disabled = false;
this.tooltipId = '';
}
connectedCallback() {
this.delay = parseInt(this.dataset.delay ?? '300', 10);
this.disabled = this.dataset.disabled === 'true';
this.tooltipId = this.dataset.tooltipId ?? '';
this.tooltipEl = this.querySelector(`#${this.tooltipId}`);
if (this.dataset.defaultOpen === 'true') {
this.showTooltip();
}
this.addEventListener('mouseenter', this.handleMouseEnter);
this.addEventListener('mouseleave', this.handleMouseLeave);
this.addEventListener('focusin', this.handleFocusIn);
this.addEventListener('focusout', this.handleFocusOut);
document.addEventListener('keydown', this.handleKeyDown);
}
disconnectedCallback() {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.removeEventListener('mouseenter', this.handleMouseEnter);
this.removeEventListener('mouseleave', this.handleMouseLeave);
this.removeEventListener('focusin', this.handleFocusIn);
this.removeEventListener('focusout', this.handleFocusOut);
document.removeEventListener('keydown', this.handleKeyDown);
}
private handleMouseEnter = () => {
this.scheduleShow();
};
private handleMouseLeave = () => {
this.hideTooltip();
};
private handleFocusIn = () => {
this.scheduleShow();
};
private handleFocusOut = () => {
this.hideTooltip();
};
private handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && this.isOpen) {
this.hideTooltip();
}
};
private scheduleShow() {
if (this.disabled) return;
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.showTooltip();
}, this.delay);
}
private showTooltip() {
if (this.disabled || !this.tooltipEl) return;
this.isOpen = true;
this.tooltipEl.setAttribute('aria-hidden', 'false');
this.tooltipEl.classList.remove('opacity-0', 'invisible');
this.tooltipEl.classList.add('opacity-100', 'visible');
this.setAttribute('aria-describedby', this.tooltipId);
}
private hideTooltip() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (!this.tooltipEl) return;
this.isOpen = false;
this.tooltipEl.setAttribute('aria-hidden', 'true');
this.tooltipEl.classList.remove('opacity-100', 'visible');
this.tooltipEl.classList.add('opacity-0', 'invisible');
this.removeAttribute('aria-describedby');
}
}
customElements.define('apg-tooltip', ApgTooltip);
</script> 使い方
Example
---
import Tooltip from './Tooltip.astro';
---
<Tooltip
content="Save your changes"
placement="top"
delay={300}
>
<button>Save</button>
</Tooltip> API
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
content | string | - | ツールチップの内容(必須) |
defaultOpen | boolean | false | デフォルトの開閉状態 |
delay | number | 300 | 表示までの遅延時間(ミリ秒) |
placement | 'top' | 'bottom' | 'left' | 'right' | 'top' | ツールチップの位置 |
id | string | auto-generated | カスタム ID |
disabled | boolean | false | ツールチップを無効化 |
この実装は、クライアント側のインタラクティブ性のために Web Component(<apg-tooltip>)を使用しています。
テスト
テスト概要
ToolチップコンポーネントのテストはAPG準拠要件に基づいて優先度レベルに分類されています。
テストカテゴリ
高優先度: APGコア準拠
| テスト | APG要件 |
|---|---|
| role="tooltip" が存在する | ツールチップコンテナはtooltipロールを持つ必要がある |
| 閉じている時はaria-hidden | 非表示のツールチップは支援技術から読み取られてはならない |
| 表示時はaria-describedby | トリガーは表示時のみツールチップを参照する必要がある |
| Escapeキーでツールチップを閉じる | キーボードによる解除サポート |
| フォーカスでツールチップを表示 | キーボードアクセシビリティ |
| ぼかしでツールチップを非表示 | フォーカス管理 |
中優先度: アクセシビリティ検証
| テスト | WCAG要件 |
|---|---|
| axe違反なし(非表示状態) | WCAG 2.1 AA準拠 |
| axe違反なし(表示状態) | WCAG 2.1 AA準拠 |
| ツールチップはフォーカス可能ではない | APG: ツールチップはフォーカスを受け取ってはならない |
低優先度: Props & 拡張性
| テスト | 機能 |
|---|---|
| placement propで位置を変更 | 配置のカスタマイズ |
| disabled propで表示を防ぐ | 無効化機能 |
| delay propでタイミングを制御 | 遅延のカスタマイズ |
| id propでカスタムIDを設定 | SSR/カスタムIDサポート |
| 制御されたopen状態 | 外部状態制御 |
| onOpenChangeコールバック | 状態変更通知 |
| className継承 | スタイルのカスタマイズ |
テストの実行
# すべてのToolチップテストを実行
npm run test -- tooltip
# 特定のフレームワークのテストを実行
npm run test -- Tooltip.test.tsx # React
npm run test -- Tooltip.test.vue # Vue
npm run test -- Tooltip.test.svelte # Svelte リソース
- WAI-ARIA APG: Tooltip パターン (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist