HextaUI

Tooltip

ホバーまたはキーボードフォーカスで、少し待ってから開く短いヒントです。隣り合う要素の間では即座に切り替わり、ショートカットも表示できます。

import {
  IconArrowBackUp,
  IconArrowForwardUp,
  IconShare2,
  IconTrash,
} from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Kbd } from "@/components/ui/kbd"
import { TooltipGroup, TooltipTrigger } from "@/components/ui/tooltip"

export function TooltipDemo() {
  return (
    <TooltipGroup>
      <div className="flex items-center gap-1">
        <TooltipTrigger
          content={
            <>
              Undo
              <Kbd keys="mod+z" />
            </>
          }
          render={<Button variant="ghost" size="icon" aria-label="Undo" />}
        >
          <IconArrowBackUp />
        </TooltipTrigger>
        <TooltipTrigger
          content={
            <>
              Redo
              <Kbd keys="mod+shift+z" />
            </>
          }
          render={<Button variant="ghost" size="icon" aria-label="Redo" />}
        >
          <IconArrowForwardUp />
        </TooltipTrigger>
        <TooltipTrigger
          content="Share"
          render={<Button variant="ghost" size="icon" aria-label="Share" />}
        >
          <IconShare2 />
        </TooltipTrigger>
        <TooltipTrigger
          content="Move to trash"
          render={
            <Button variant="ghost" size="icon" aria-label="Move to trash" />
          }
        >
          <IconTrash />
        </TooltipTrigger>
      </div>
    </TooltipGroup>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/tooltip.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
<Tooltip>
  <TooltipTrigger render={<Button variant="ghost" size="icon" aria-label="Undo" />}>
    <IconArrowBackUp />
  </TooltipTrigger>
  <TooltipContent>Undo</TooltipContent>
</Tooltip>

tooltip は、トリガー上で少し静止した後、またはトリガーにキーボードフォーカスが当たるとすぐに開きます。1つが表示されている間は隣の tooltip が即座に、アニメーションなしで開くため、ツールバーを走査するときも、1つずつ待つのではなくラベルを読むように感じられます。

この即時の切り替えは、TooltipProvider を共有する tooltip の間で機能します。遅延を共有するには、アプリ(またはツールバー)を1つで囲んでください。プロバイダーのない tooltip も、同じデフォルトで単独で機能します。

<TooltipProvider>
  <App />
</TooltipProvider>
TooltipProvider
└── Tooltip
    ├── TooltipTrigger
    └── TooltipContent

TooltipGroup
└── TooltipTrigger

サイド

TooltipContent に side を設定します。論理的な側は文字の読み方向に従い、空間が足りないときは tooltip が反転します。

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

const sides = [
  { side: "top", cell: "col-start-2 row-start-1" },
  { side: "inline-start", cell: "col-start-1 row-start-2" },
  { side: "inline-end", cell: "col-start-3 row-start-2" },
  { side: "bottom", cell: "col-start-2 row-start-3" },
] as const

export function TooltipSides() {
  return (
    <TooltipProvider>
      <div className="grid grid-cols-3 grid-rows-3 place-items-center gap-2">
        {sides.map(({ side, cell }) => (
          <div key={side} className={cell}>
            <Tooltip>
              <TooltipTrigger render={<Button variant="outline" />}>
                {side}
              </TooltipTrigger>
              <TooltipContent side={side}>{side}</TooltipContent>
            </Tooltip>
          </div>
        ))}
      </div>
    </TooltipProvider>
  )
}

矢印付き

arrow は、tooltip が画面に収まるようにずれても、トリガーを指し続けるポインターを追加します。

import { IconBell } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipWithArrow() {
  return (
    <Tooltip>
      <TooltipTrigger
        render={
          <Button variant="outline" size="icon" aria-label="Notifications" />
        }
      >
        <IconBell />
      </TooltipTrigger>
      <TooltipContent arrow>Notifications</TooltipContent>
    </Tooltip>
  )
}

キーボードショートカット

ラベルの後に Kbd または KbdGroup を置きます。tooltip の色を引き継いで末尾に置かれ、Apple デバイスでは ⌘、それ以外では Ctrl が表示されます。

import { IconDeviceFloppy } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { KbdGroup } from "@/components/ui/kbd"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipShortcut() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          <IconDeviceFloppy data-icon="inline-start" />
          Save
        </TooltipTrigger>
        <TooltipContent>
          Save changes
          <KbdGroup keys="mod+s" size="sm" />
        </TooltipContent>
      </Tooltip>
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          Go to inbox
        </TooltipTrigger>
        <TooltipContent>
          Go to inbox
          <KbdGroup keys="g i" size="sm" />
        </TooltipContent>
      </Tooltip>
    </div>
  )
}

ツールバー

1つのボタンで静止してから、グループに沿ってスライドします。1つの TooltipProvider の下にある tooltip は、移動する間も即座に切り替わります。

import {
  IconBold,
  IconItalic,
  IconLink,
  IconStrikethrough,
  IconUnderline,
} from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import { Kbd } from "@/components/ui/kbd"
import { TooltipGroup, TooltipTrigger } from "@/components/ui/tooltip"

const tools = [
  { label: "Bold", keys: "mod+b", icon: IconBold },
  { label: "Italic", keys: "mod+i", icon: IconItalic },
  { label: "Underline", keys: "mod+u", icon: IconUnderline },
  { label: "Strikethrough", keys: "mod+shift+x", icon: IconStrikethrough },
  { label: "Insert link", keys: "mod+k", icon: IconLink },
]

export function TooltipToolbar() {
  return (
    <TooltipGroup side="bottom">
      <ButtonGroup aria-label="Formatting">
        {tools.map(({ label, keys, icon: Icon }) => (
          <TooltipTrigger
            key={label}
            content={
              <>
                {label}
                <Kbd keys={keys} size="sm" />
              </>
            }
            render={<Button variant="outline" size="icon" aria-label={label} />}
          >
            <Icon />
          </TooltipTrigger>
        ))}
      </ButtonGroup>
    </TooltipGroup>
  )
}

無効

無効なボタンでも、focusableWhenDisabled を使えば理由を説明できます。ホバー可能でタブ順に残ります。Tooltip の disabled は tooltip をオフにします。

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipDisabled() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Tooltip>
        <TooltipTrigger render={<Button disabled focusableWhenDisabled />}>
          Publish
        </TooltipTrigger>
        <TooltipContent>Add a title before publishing</TooltipContent>
      </Tooltip>
      <Tooltip disabled>
        <TooltipTrigger render={<Button variant="outline" />}>
          No tooltip
        </TooltipTrigger>
        <TooltipContent>You won’t see this</TooltipContent>
      </Tooltip>
    </div>
  )
}

長いコンテンツ

テキストは読みやすい幅で折り返され、長い URL でも画面の端を超えることはありません。

import { IconInfoCircle } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipLongContent() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          <IconInfoCircle data-icon="inline-start" />
          Retention
        </TooltipTrigger>
        <TooltipContent>
          Deleted projects stay in the trash for 30 days. After that they’re
          removed for good, along with their deployments and logs.
        </TooltipContent>
      </Tooltip>
      <Tooltip>
        <TooltipTrigger render={<Button variant="outline" />}>
          Webhook URL
        </TooltipTrigger>
        <TooltipContent>
          https://api.example.com/v1/hooks/8f3a2c91d7e64b0f9a1c5e2d7b8a4f60/deliveries
        </TooltipContent>
      </Tooltip>
    </div>
  )
}

制御

tooltip を自分で制御するには、open と onOpenChange を渡します。reason が、何が開閉したかを伝えます。

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function TooltipControlled() {
  const [open, setOpen] = React.useState(false)
  const [reason, setReason] = React.useState("none")

  return (
    <div className="flex flex-col items-center gap-3">
      <div className="flex items-center gap-2">
        <Tooltip
          open={open}
          onOpenChange={(next, details) => {
            setOpen(next)
            setReason(details.reason)
          }}
        >
          <TooltipTrigger render={<Button variant="outline" />}>
            Hover or focus me
          </TooltipTrigger>
          <TooltipContent>Controlled tooltip</TooltipContent>
        </Tooltip>
        <Button
          variant="ghost"
          onClick={() => {
            setOpen((value) => !value)
            setReason("button")
          }}
        >
          {open ? "Hide" : "Show"}
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">
        Open: {String(open)} · last reason: {reason}
      </p>
    </div>
  )
}

1つの tooltip、複数のトリガー

createTooltipHandle でハンドルを作成し、各トリガーから payload を渡すと、1つの tooltip を共有できます。

"use client"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  createTooltipHandle,
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

type Person = { name: string; initials: string; status: string }

const people: Person[] = [
  { name: "Ada Lovelace", initials: "AL", status: "Online" },
  { name: "Alan Turing", initials: "AT", status: "In a meeting" },
  { name: "Grace Hopper", initials: "GH", status: "Away" },
]

const presence = createTooltipHandle<Person>()

export function TooltipDetached() {
  return (
    <TooltipProvider>
      <div className="flex items-center gap-2">
        {people.map((person) => (
          <TooltipTrigger
            key={person.name}
            handle={presence}
            payload={person}
            aria-label={person.name}
            render={
              <button className="rounded-full outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden" />
            }
          >
            <Avatar>
              <AvatarFallback>{person.initials}</AvatarFallback>
            </Avatar>
          </TooltipTrigger>
        ))}
        <Tooltip handle={presence}>
          {({ payload }) => (
            <TooltipContent side="bottom">
              {payload ? `${payload.name} · ${payload.status}` : null}
            </TooltipContent>
          )}
        </Tooltip>
      </div>
    </TooltipProvider>
  )
}

右から左

dir="rtl" の内側では、inline-end は左に開き、ショートカットは反対の端に移ります。

import { IconBookmark, IconHeart, IconShare2 } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Kbd } from "@/components/ui/kbd"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

const actions = [
  { label: "أعجبني", icon: IconHeart },
  { label: "حفظ", icon: IconBookmark, keys: "mod+d" },
  { label: "مشاركة", icon: IconShare2 },
]

export function TooltipRtl() {
  return (
    <div dir="rtl">
      <TooltipProvider>
        <div className="flex items-center gap-1">
          {actions.map(({ label, icon: Icon, keys }) => (
            <Tooltip key={label}>
              <TooltipTrigger
                render={
                  <Button variant="ghost" size="icon" aria-label={label} />
                }
              >
                <Icon />
              </TooltipTrigger>
              <TooltipContent side="inline-end" arrow>
                {label}
                {keys && <Kbd keys={keys} size="sm" />}
              </TooltipContent>
            </Tooltip>
          ))}
        </div>
      </TooltipProvider>
    </div>
  )
}
キーアクション
Tabキーボードでトリガーにフォーカスすると、すぐに tooltip が開きます。
Esctooltip を閉じ、フォーカスをトリガーに残します。
EnterSpaceトリガーを実行して tooltip を閉じるため、次に起きることを覆い隠しません。
  • tooltip は視覚的なヒントであり、ラベルではありません。アイコンのみのトリガーには、tooltip のテキストと一致する aria-label を付けてください。
  • タッチでは何も開かず、タップはトリガー本来の動作だけを行います。必要な情報を tooltip に入れないでください。それには popover を使います。
  • ポインターを使うユーザーは、tooltip の上に移動しても閉じることなく、読んだり選択したりできます。
  • 視差効果の軽減が有効な場合、tooltip は拡大せずにフェードします。

Base UI の tooltip 上に構築されています。すべてのパーツは、ラップしているプリミティブの props を受け付けます。

プロパティ型デフォルト
delay開くまでにトリガー上で静止するミリ秒。
number300
closeDelay離れてから閉じるまでのミリ秒。
number0
timeout1つが閉じてから、次のものが即座に開く猶予時間。
number400
プロパティ型デフォルト
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open, details) => void–
onOpenChangeComplete開閉アニメーションの後に呼ばれます。
(open) => void–
disabledtooltip をオフにします。
booleanfalse
disableHoverablePopupポインターがトリガーを離れた瞬間に閉じます。
booleanfalse
trackCursorAxis
"none" | "x" | "y" | "both""none"
handlecreateTooltipHandle から。
TooltipHandle<Payload>–
children
ReactNode | ({ payload }) => ReactNode–
プロパティ型デフォルト
delayこのトリガーについて、プロバイダーの遅延を上書きします。
number–
closeDelay
number–
closeOnClick
booleantrue
disabledこのトリガーが tooltip を開かないようにします。要素自体は有効のままです。
booleanfalse
handle
TooltipHandle<Payload>–
payload
Payload–
render
ReactElement | (props, state) => ReactElement<button>
属性説明
data-slot="tooltip-trigger"トリガー。
data-popup-openその tooltip が開いている間付きます。
data-trigger-disabledトリガーが tooltip を開けないときに付きます。
プロパティ型デフォルト
side
"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end""top"
align
"start" | "center" | "end""center"
sideOffset
number | (data) => number6, or 8 with arrow
alignOffset
number0
arrow
booleanfalse
collisionPadding
number8
arrowPadding
number8
portalPropscontainer や keepMounted など。
TooltipPortal props–
属性説明
data-slot="tooltip-content"ポップアップ。
data-slot="tooltip-positioner"ポップアップを囲む配置用のラッパー。
data-side反転した後に開いた側。
data-align配置。
data-instantアニメーションなしで開閉するときは "delay"、"focus"、"dismiss"。
data-starting-style表示アニメーションの間存在します。
data-ending-style非表示アニメーションの間存在します。
--available-widthトリガーと画面の端との間隔。
--transform-origintooltip が拡大する基点。

arrow によって自動で描画されます。独自のコンテンツパーツを組み立てる場合にだけ直接使ってください。

属性説明
data-slot="tooltip-arrow"矢印。
data-uncenteredトリガーの中心を指せないときに付きます。

使用しているブロック

Tooltip の上に構築されるブロック。