アプリの使い方を説明する操作ガイドを実装したいです。
本物のコンポーネントの見た目を使いながら、特定の要素をハイライトし、ツールチップで操作説明を表示するイメージです。
(↓ フォームコンポーネントに操作ガイドを表示する例)
ここで実装上の悩みがあります。
- ガイド内の見た目は対象コンポーネントと同じにしたい
(コンポーネントを再利用したい) - しかし、対象コンポーネントにはガイド用のコードを追加したくない
(コンポーネントの責務を増やしたくない)
そんなときに操作ガイドを外からセレクタで後付けするアプローチが有効かもしれません。
実装方針
コンポーネントの外側にオーバーレイコンテナを設置し、そこにハイライトとツールチップを描画します。
- 対象のコンポーネントをそのまま表示する
- セレクタでターゲット要素を指定する
- Tailwind クラスを付与してハイライトする
- ツールチップをターゲット要素のまわりに配置する
開発環境:
- Vue3
- Tailwind CSS
操作ガイド部分の実装:
<!-- オーバーレイ -->
<GuideOverlay>
<!-- フォームコンポーネント(操作ガイド用の実装なし) -->
<MyForm />
<!-- ハイライト・ツールチップ -->
<template #guides>
<GuideSpot
selector=".name-input"
highlight-class="ring-4 ring-rose-500 bg-rose-100"
placement="right"
>
<div class="flex items-center gap-2">
<div class="text-rose-500 text-3xl">←</div>
<div class="bg-rose-500 text-white px-4 py-3 rounded-xl shadow-xl">
フルネームで入力してください
</div>
</div>
</GuideSpot>
<GuideSpot
selector=".submit-btn"
highlight-class="ring-4 ring-amber-500"
placement="bottom"
>
<div class="flex flex-col items-center gap-1">
<div class="text-amber-500 text-3xl">↑</div>
<div class="bg-amber-500 text-white px-5 py-3 rounded-xl shadow-xl">
入力が終わったら送信をクリックしてください
</div>
</div>
</GuideSpot>
</template>
</GuideOverlay>
対象コンポーネント(MyForm)には手を加えずに、外からセレクタで指定してハイライトとツールチップを追加します。
実装方法
2 つのコンポーネントで構成されています。
1. GuideOverlay(コンテナ)
コンテナの参照を provide で子コンポーネントに共有します。
GuideOverlay.vue
<script setup lang="ts">
import { ref, provide } from 'vue';
const containerRef = ref<HTMLElement | null>(null);
provide('guideContainer', containerRef);
</script>
<template>
<div ref="containerRef" class="relative">
<div class="pointer-events-none">
<slot />
</div>
<slot name="guides" />
</div>
</template>
2. GuideSpot(ハイライト&ツールチップ)
セレクタから要素を見つけ、クラスを付与し、ツールチップを配置します。
ツールチップの内容は slot にすることで内容を柔軟にカスタムできます。
GuideSpot.vue
<script setup lang="ts">
import { ref, inject, onMounted, onUnmounted, type Ref } from 'vue';
import { useFloating, offset, flip, autoUpdate, type Placement } from '@floating-ui/vue';
const props = withDefaults(defineProps<{
selector: string;
highlightClass?: string;
placement?: Placement;
}>(), {
placement: 'top',
});
const targetElement = ref<Element | null>(null);
const tooltipRef = ref<HTMLElement | null>(null);
const { floatingStyles } = useFloating(targetElement, tooltipRef, {
placement: props.placement,
middleware: [
offset(12),
flip(),
],
whileElementsMounted: autoUpdate,
});
const containerRef = inject<Ref<HTMLElement | null>>('guideContainer');
const addedClasses = ref<string[]>([]);
onMounted(() => {
const target = containerRef?.value?.querySelector(props.selector);
if (target && props.highlightClass) {
const classes = props.highlightClass.split(/\s+/).filter(Boolean);
target.classList.add(...classes);
addedClasses.value = classes;
}
targetElement.value = target ?? null;
});
onUnmounted(() => {
if (targetElement.value && addedClasses.value.length > 0) {
targetElement.value.classList.remove(...addedClasses.value);
}
});
</script>
<template>
<div
v-if="targetElement"
ref="tooltipRef"
class="absolute z-50"
:style="floatingStyles"
>
<slot />
</div>
</template>
※ 要素の配置には floating-ui を利用しています。
ポイント
対象のコンポーネントは変更せずにそのまま表示するだけで OK です。
ガイド機能を外付けできており、対象のフォームコンポーネントをシンプルに保つことができます。
まとめ
操作ガイドを実装するために以下の方針を取りました。
- セレクタで後付け … コンポーネントを汚さずに済む
- クラス付与でハイライト ... Tailwind で自由にスタイリングできる
- スロットを使った実装 ... 操作ガイドやツールチップの内容を柔軟にカスタマイズできる
もし何かの参考になれば幸いです。もっとよい方法があれば、ぜひ教えてください!
