Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/docs/src/examples/components/tooltip/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { Tooltip } from '@vuetify/v0'
</script>

<template>
<div class="flex justify-center p-12">
<Tooltip.Root :close-delay="200" :open-delay="500">
<Tooltip.Activator
class="px-3 py-1 rounded border border-divider bg-surface text-on-surface hover:bg-surface-tint"
>
Hover me
</Tooltip.Activator>

<Tooltip.Content
class="px-2 py-1 rounded text-xs bg-on-surface text-surface shadow-md"
>
Helpful description
</Tooltip.Content>
</Tooltip.Root>
</div>
</template>
22 changes: 22 additions & 0 deletions apps/docs/src/examples/components/tooltip/interactive.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { Tooltip } from '@vuetify/v0'
</script>

<template>
<div class="flex justify-center p-12">
<Tooltip.Root :close-delay="300" interactive :open-delay="500">
<Tooltip.Activator
class="px-3 py-1 rounded border border-divider bg-surface text-on-surface hover:bg-surface-tint"
>
Hover for actions
</Tooltip.Activator>

<Tooltip.Content
class="flex gap-2 px-3 py-2 rounded bg-on-surface text-surface text-xs shadow-md"
>
<button class="underline">Edit</button>
<button class="underline">Delete</button>
</Tooltip.Content>
</Tooltip.Root>
</div>
</template>
51 changes: 51 additions & 0 deletions apps/docs/src/examples/composables/use-tooltip/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { Tooltip, useTooltip } from '@vuetify/v0'

const region = useTooltip()
</script>

<template>
<div class="flex flex-col gap-4 items-center">
<div class="flex gap-2 text-xs">
<div
class="px-2 py-1 rounded"
:class="region.isAnyOpen.value
? 'bg-success text-on-success'
: 'bg-surface-variant text-on-surface-variant'"
>
isAnyOpen: {{ region.isAnyOpen.value }}
</div>

<div class="px-2 py-1 rounded bg-surface-variant text-on-surface-variant">
openDelay: {{ region.openDelay.value }}ms
</div>

<div class="px-2 py-1 rounded bg-surface-variant text-on-surface-variant">
skipDelay: {{ region.skipDelay.value }}ms
</div>
</div>

<div class="flex gap-3">
<Tooltip.Root v-for="i in 4" :key="i">
<Tooltip.Activator
class="px-3 py-1 rounded border border-divider bg-surface text-on-surface hover:bg-surface-tint"
>
Item {{ i }}
</Tooltip.Activator>

<Tooltip.Content
class="px-2 py-1 rounded text-xs bg-on-surface text-surface shadow-md"
>
Description for item {{ i }}
</Tooltip.Content>
</Tooltip.Root>
</div>

<p class="text-xs text-on-surface-variant max-w-md text-center">
Hover the first item — wait {{ region.openDelay.value }}ms for the tooltip.
Move to a neighbor while one is still open — the next appears instantly.
Leave all four. After {{ region.skipDelay.value }}ms of idle, the next hover
pays the full open delay again.
</p>
</div>
</template>
112 changes: 112 additions & 0 deletions apps/docs/src/pages/components/disclosure/tooltip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: Tooltip - Headless Description Tooltip with Hover and Focus Triggers
meta:
- name: description
content: Headless tooltip component with hover and focus activation, region-scoped delay coordination, configurable interactive-content mode, and WAI-ARIA compliant aria-describedby semantics.
- name: keywords
content: tooltip, hover, focus, popover, ARIA, accessibility, v-bind, slots, Vue 3, headless
features:
category: Component
label: 'C: Tooltip'
github: /components/Tooltip/
renderless: true
level: 2
related:
- /composables/plugins/use-tooltip
- /composables/system/use-popover
- /components/disclosure/popover
---

# Tooltip

Headless description tooltip with hover and focus triggers, configurable open/close delays, region-scoped skip-window coordination, and optional interactive-content mode.

<DocsPageFeatures :frontmatter />

## Usage

::: example
/components/tooltip/basic
:::

## Anatomy

```vue Anatomy playground
<script setup lang="ts">
import { Tooltip } from '@vuetify/v0'
</script>

<template>
<Tooltip>
<Tooltip.Root>
<Tooltip.Activator />
<Tooltip.Content />
</Tooltip.Root>
</Tooltip>
</template>
```

The bare `<Tooltip>` is the optional scope wrapper — it overrides delay defaults for descendants. Skip it when the plugin defaults are sufficient.

## Architecture

```mermaid "Tooltip lifecycle"
flowchart LR
Closed -- "pointerenter (mouse) / focus (keyboard)" --> OpenScheduled
OpenScheduled -- "openDelay elapses" --> Open
OpenScheduled -- "skip window active" --> Open
Open -- "pointerleave / blur / click / Escape" --> CloseScheduled
CloseScheduled -- "closeDelay elapses" --> Closed
CloseScheduled -- "pointerenter (interactive content)" --> Open
```

## Examples

::: example
/components/tooltip/interactive

### Interactive content

Set `interactive` on `<Tooltip.Root>` to let the user move the cursor from the activator into the content without dismissing the tooltip. Useful for tooltips that surface secondary actions or links.

The strict WAI-ARIA APG tooltip pattern forbids interactive content; if you need a richer hover surface with focusable controls, consider whether a future `HoverCard` component is a better fit.

| File | Role |
|------|------|
| `interactive.vue` | Demonstrates the `interactive` flag with two action buttons inside the content |

:::

## Accessibility

| Concern | Behavior |
|---------|----------|
| Role | Content renders `role="tooltip"` |
| Linkage | Activator always carries `aria-describedby={contentId}` so screen readers announce the description on focus |
| Keyboard | Focus opens instantly (no delay), Escape closes; Enter / Space activate the underlying control, which closes via click |
| Touch | Tooltips are not shown on touch interactions per the WAI-ARIA APG |
| Hoverable content | Off by default; opt-in with `interactive` on `<Tooltip.Root>` |

## FAQ

::: faq

??? Why don't tooltips show on touch?

Touch devices have no hover state, and showing a tooltip on tap competes with whatever action the underlying control performs. Both React Aria and the WAI-ARIA Authoring Practices Guide recommend skipping tooltips on touch and ensuring the UI is usable without them. v0 follows this guidance.

??? How do I share delay defaults across an app?

Install the plugin: `app.use(createTooltipPlugin({ openDelay: 500 }))`. Every `<Tooltip.Root>` reads from the region — wrap a subtree in `<Tooltip>` for region-specific overrides.

??? Why doesn't Tooltip.Activator open when I focus it via mouse click?

The activator suppresses focus-driven opens that arrive within ~50 ms of a `pointerdown`, so a click doesn't double-trigger as both click-close and focus-open. Keyboard-driven focus (Tab) opens instantly.

??? How do I render a non-button activator?

The activator defaults to `as="button"`; pass `as="a"`, `as="div"`, etc. to render a different element. Always ensure the activator is keyboard-focusable (`tabindex="0"` on a non-button if needed).

:::

<DocsApi />
1 change: 1 addition & 0 deletions apps/docs/src/pages/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,6 @@ Components for showing/hiding content.
| [ExpansionPanel](/components/disclosure/expansion-panel) | Accordion-style collapsible panels |
| [Popover](/components/disclosure/popover) | CSS anchor-positioned popup content |
| [Tabs](/components/disclosure/tabs) | Tab panel navigation with keyboard support and lazy content rendering |
| [Tooltip](/components/disclosure/tooltip) | Description tooltip with hover/focus triggers and shared delay coordination |
| [Treeview](/components/disclosure/treeview) | Hierarchical tree with nested selection and expand/collapse |

1 change: 1 addition & 0 deletions apps/docs/src/pages/composables/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ Application-level features installable via Vue plugins.
| [useStack](/composables/plugins/use-stack) | Overlay z-index stacking with automatic calculation and scrim integration |
| [useStorage](/composables/plugins/use-storage) | Reactive browser storage interface |
| [useTheme](/composables/plugins/use-theme) | Theme management with CSS custom properties |
| [useTooltip](/composables/plugins/use-tooltip) | Region-scoped tooltip delay coordination plugin |

## Data

Expand Down
107 changes: 107 additions & 0 deletions apps/docs/src/pages/composables/plugins/use-tooltip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: useTooltip - Region-Scoped Tooltip Delay Coordination
meta:
- name: description
content: Plugin trinity that coordinates tooltip open/close delays across a region. Holds shared defaults and a skip-window registry so neighboring tooltips open instantly once one is visible.
- name: keywords
content: tooltip, delay, hover, region, plugin, skip window, warmup, Vue 3, composable
features:
category: Composable
label: 'E: useTooltip'
github: /composables/useTooltip/
level: 2
related:
- /components/disclosure/tooltip
- /composables/system/use-delay
- /composables/system/use-popover
---

# useTooltip

Region-scoped coordination plugin for tooltip open/close delays. Holds shared `openDelay` / `closeDelay` / `skipDelay` defaults plus a registry of currently-open tooltip tickets so neighboring `<Tooltip.Root>` instances can skip their open delay during the warmup window.

<DocsPageFeatures :frontmatter />

## Usage

```ts collapse
import { createTooltipPlugin, useTooltip } from '@vuetify/v0'

// App-wide defaults
app.use(createTooltipPlugin({
openDelay: 500,
closeDelay: 150,
skipDelay: 300,
}))

// Inside a component
const region = useTooltip()

region.openDelay.value // 500
region.isAnyOpen.value // false
region.shouldSkipOpenDelay() // false until a tooltip opens
```

`<Tooltip.Root>` reads from `useTooltip()` automatically; you do not call this composable yourself unless you're building a non-component consumer.

## Architecture

```mermaid "Skip-window coordination"
flowchart LR
T1[Tooltip 1 opens] -- "register()" --> Registry
T2[Tooltip 2 hovers] -- "shouldSkipOpenDelay?" --> Registry
Registry -- "any registered → true" --> Skip[Open instantly]
T1 -- "close()" --> LastClosed[lastClosedAt = now]
T3[Tooltip 3 hovers within skipDelay] -- "shouldSkipOpenDelay?" --> LastClosed
LastClosed -- "now - lastClosedAt < skipDelay" --> Skip
```

## Reactivity

| Property | Type | Description |
|----------|------|-------------|
| `openDelay` | `Readonly<Ref<number>>` | Default open delay in ms (700) |
| `closeDelay` | `Readonly<Ref<number>>` | Default close delay in ms (150) |
| `skipDelay` | `Readonly<Ref<number>>` | Skip-window after last close in ms (300) |
| `disabled` | `Readonly<Ref<boolean>>` | Region-wide disabled flag |
| `isAnyOpen` | `Readonly<Ref<boolean>>` | True when any registered tooltip is currently open |
| `shouldSkipOpenDelay` | `() => boolean` | Whether the next open should bypass the delay |
| `register` | `(input?: Partial<RegistryTicketInput>) => RegistryTicket` | Track a newly-opened tooltip |
| `unregister` | `(id: ID) => void` | Untrack a closed tooltip |

## Examples

::: example
/composables/use-tooltip/basic

### Region inspection

The example surfaces the live `isAnyOpen` flag and the resolved delay defaults. Click the button to register a synthetic tooltip ticket for one second; rapid clicks keep `isAnyOpen` true and demonstrate how `shouldSkipOpenDelay` behaves through the skip-window after a close.

Reach for `useTooltip()` directly only when you're wiring a tooltip surface that doesn't go through `<Tooltip.Root>` — most consumers should use the component family and let it call this composable internally.

| File | Role |
|------|------|
| `basic.vue` | Inspects region state and exercises register / unregister |

:::

## FAQ

::: faq

??? Why is the registry global instead of per-region?

Skip-window coordination is most useful when neighbors across UI regions cooperate — once any tooltip in the app is open, you want toolbar tooltips and content tooltips to all skip their delay. Splintering the registry per `<Tooltip>` scope-wrapper would force consumers to choose between scoped defaults and shared coordination; the current design gives you both.

??? Can I install useTooltip without using `<Tooltip.Root>`?

Yes. The plugin is just a small shared state object — register and unregister tickets manually if you're building a custom tooltip surface and want it to coordinate with v0 tooltips on the page.

??? What if I never install the plugin?

`useTooltip()` returns synthesized fallback defaults (700 / 150 / 300) so `<Tooltip.Root>` works without an `app.use(createTooltipPlugin())` call.

:::

<DocsApi />
5 changes: 3 additions & 2 deletions apps/docs/src/pages/composables/system/use-popover.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ flowchart TD
| `positionArea` | `string` | `'bottom'` | CSS `position-area` value — controls where the content appears relative to the anchor |
| `positionTry` | `string` | `'most-width bottom'` | CSS `position-try-fallbacks` value — fallback positions when the primary area overflows |
| `isOpen` | `Ref<boolean>` | — | External ref for bidirectional open state (e.g., from `defineModel`) |
| `showDelay` | `number` | `0` | Milliseconds to wait before showing the popover (hover/focus use cases) |
| `hideDelay` | `number` | `0` | Milliseconds to wait before hiding the popover (prevents premature close on mouse leave) |
| `openDelay` | `MaybeRefOrGetter<number>` | `0` | Milliseconds to wait before opening the popover |
| `closeDelay` | `MaybeRefOrGetter<number>` | `0` | Milliseconds to wait before closing the popover |

## Reactivity

Expand All @@ -91,6 +91,7 @@ flowchart TD
| `open()` | - | Open the popover |
| `close()` | - | Close the popover |
| `toggle()` | - | Toggle open/close |
| `cancel()` | - | Cancel any pending open or close transition |
| `attach(el)` | - | Wire native show/hide watch + toggle event sync to a content element |
| `anchorStyles` | <AppSuccessIcon /> | Readonly Ref, CSS `anchor-name` for the activator element |
| `contentAttrs` | <AppSuccessIcon /> | Readonly Ref, `id` and `popover` attribute for the content element |
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/src/plugins/zero.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Framework
import { createBreakpointsPlugin, createDatePlugin, createFeaturesPlugin, createHydrationPlugin, createLocalePlugin, createLoggerPlugin, createPermissionsPlugin, createRtlPlugin, createStackPlugin, createStoragePlugin, createThemePlugin, IN_BROWSER, useFeatures, V0UnheadThemeAdapter } from '@vuetify/v0'
import { createBreakpointsPlugin, createDatePlugin, createFeaturesPlugin, createHydrationPlugin, createLocalePlugin, createLoggerPlugin, createPermissionsPlugin, createRtlPlugin, createStackPlugin, createStoragePlugin, createThemePlugin, createTooltipPlugin, IN_BROWSER, useFeatures, V0UnheadThemeAdapter } from '@vuetify/v0'
import { V0DateAdapter } from '@vuetify/v0/date'

// Composables
Expand All @@ -22,6 +22,7 @@ export default function zero (app: App) {
app.use(createBreakpointsPlugin({ mobileBreakpoint: 768 }))
app.use(createStoragePlugin())
app.use(createStackPlugin())
app.use(createTooltipPlugin())
app.use(createDiscoveryPlugin())

app.use(
Expand Down
2 changes: 2 additions & 0 deletions packages/0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import { ... } from '@vuetify/v0/date' // Date adapter and utilities
| [ExpansionPanel](https://0.vuetifyjs.com/components/disclosure/expansion-panel) | Accordion-style collapsible panels |
| [Popover](https://0.vuetifyjs.com/components/disclosure/popover) | CSS anchor-positioned popup content |
| [Tabs](https://0.vuetifyjs.com/components/disclosure/tabs) | Tab panel navigation with keyboard support and lazy content rendering |
| [Tooltip](https://0.vuetifyjs.com/components/disclosure/tooltip) | Description tooltip with hover/focus triggers |
| [Treeview](https://0.vuetifyjs.com/components/disclosure/treeview) | Hierarchical tree with nested selection and expand/collapse |

#### Semantic
Expand Down Expand Up @@ -247,6 +248,7 @@ Plugin-capable composables following the trinity pattern:
- [`useStack`](https://0.vuetifyjs.com/composables/plugins/use-stack) - Overlay z-index stacking with automatic scrim coordination
- [`useStorage`](https://0.vuetifyjs.com/composables/plugins/use-storage) - Storage adapter (localStorage/sessionStorage/memory)
- [`useTheme`](https://0.vuetifyjs.com/composables/plugins/use-theme) - Theme management with CSS variable injection
- [`useTooltip`](https://0.vuetifyjs.com/composables/plugins/use-tooltip) - Region-scoped tooltip delay coordination

## Design Principles

Expand Down
Loading
Loading