Tooltip
NewOverview
Show a little tooltip on hover.
Put exactly one element in trigger
and the tooltip text or presentational markup in content. The component creates the accessible relationship between them, so the trigger does not need to share an ID with the tooltip.
<div class="tooltip-examples flex gap-2.5 items-center flex-wrap">
<Tooltip.tooltip id="example-tip-save-tooltip">
<:trigger>
<Button.button
id="example-tip-save"
kind={:outline}
phx-click={JS.push("demo-action", value: %{action: "Saved to collection"})}
>
Save
</Button.button>
</:trigger>
<:content>Save to your collection</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="example-tip-info-tooltip">
<:trigger>
<Button.button id="example-tip-info" kind={:outline}>Autosave</Button.button>
</:trigger>
<:content>Your changes are saved automatically</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="example-tip-code-tooltip" side={:bottom}>
<:trigger>
<Link.link id="example-tip-code" color={:blue} href="https://hexdocs.pm/phoenix_live_view/">
Phoenix LiveView
</Link.link>
</:trigger>
<:content>Open the Phoenix LiveView documentation</:content>
</Tooltip.tooltip>
</div>
Positioning strategy
Set strategy
to choose the matching CSS positioning strategy described in Floating UI’s positioning documentation. :fixed, the Dizzy default, is usually positioned relative to the viewport and can escape a clipping ancestor.
:absolute
is positioned relative to its nearest positioned ancestor and usually requires less work during updates.
<div class="mx-auto grid max-w-xl gap-4 sm:grid-cols-2">
<div
id="tooltip-strategy-absolute-boundary"
class="relative flex h-28 items-end justify-center overflow-hidden rounded-lg border border-border p-3"
>
<Tooltip.tooltip
id="tooltip-position-absolute"
side={:bottom}
placement_strategy={nil}
shift={false}
strategy={:absolute}
>
<:trigger><Button.button id="tooltip-position-absolute-trigger" kind={:outline}>Absolute</Button.button></:trigger>
<:content>Clipped by the positioned container</:content>
</Tooltip.tooltip>
</div>
<div
id="tooltip-strategy-fixed-boundary"
class="relative flex h-28 items-end justify-center overflow-hidden rounded-lg border border-border p-3"
>
<Tooltip.tooltip
id="tooltip-position-fixed"
side={:bottom}
placement_strategy={nil}
shift={false}
strategy={:fixed}
>
<:trigger><Button.button id="tooltip-position-fixed-trigger" kind={:outline}>Fixed</Button.button></:trigger>
<:content>Escapes the clipping container</:content>
</Tooltip.tooltip>
</div>
</div>
Shift
The shift
option uses Floating UI’s
shift middleware
to move a tooltip along its placement axis when it would otherwise overflow a clipping boundary. It is enabled by default; set it to
false
when preserving the exact alignment matters more than keeping the complete tooltip visible.
<div id="tooltip-shift-boundary" class="relative mx-auto grid w-full max-w-xl justify-items-end gap-12 overflow-hidden rounded-lg border border-border p-6">
<Tooltip.tooltip id="tooltip-shift-disabled" side={:top_start} placement_strategy={nil} shift={false}>
<:trigger><Button.button id="tooltip-shift-disabled-trigger" kind={:outline}>Without shift</Button.button></:trigger>
<:content>This tooltip keeps its exact alignment even when it overflows.</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="tooltip-shift-enabled" side={:top_start} placement_strategy={nil}>
<:trigger><Button.button id="tooltip-shift-enabled-trigger" kind={:outline}>With shift</Button.button></:trigger>
<:content>This tooltip shifts along the boundary to remain visible.</:content>
</Tooltip.tooltip>
</div>
Flip and auto placement
Set placement_strategy
to nil, :flip, or :auto_placement. Floating UI’s
flip middleware
starts with side
and tries fallback placements when that side overflows. Its
auto placement middleware
instead chooses the placement with the most available space. Floating UI does not allow the two middleware strategies together, so Dizzy exposes them as one option. The default is :flip.
<div class="mx-auto grid max-w-xl grid-cols-3 gap-3">
<Tooltip.tooltip id="tooltip-strategy-none" side={:top} placement_strategy={nil}>
<:trigger><Button.button id="tooltip-strategy-none-trigger" kind={:outline} class="w-full">None</Button.button></:trigger>
<:content>Keep the requested top placement</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="tooltip-strategy-flip" side={:top} placement_strategy={:flip}>
<:trigger><Button.button id="tooltip-strategy-flip-trigger" kind={:outline} class="w-full">Flip</Button.button></:trigger>
<:content>Try the opposite side when needed</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="tooltip-strategy-auto" side={:top} placement_strategy={:auto_placement}>
<:trigger><Button.button id="tooltip-strategy-auto-trigger" kind={:outline} class="w-full">Auto placement</Button.button></:trigger>
<:content>Choose the side with the most room</:content>
</Tooltip.tooltip>
</div>
Arrow
Set arrow
to render a caret positioned by Floating UI’s arrow middleware. The caret follows the resolved placement after flipping or automatic placement.
<div class="flex flex-wrap items-center justify-center gap-3">
<Tooltip.tooltip
:for={{label, side} <- [{"Top", :top}, {"Right", :right}, {"Bottom", :bottom}, {"Left", :left}]}
id={"tooltip-arrow-#{side}"}
side={side}
arrow
>
<:trigger><Button.button id={"tooltip-arrow-#{side}-trigger"} kind={:outline}>{label}</Button.button></:trigger>
<:content>{label} tooltip with an arrow</:content>
</Tooltip.tooltip>
</div>
Hide when detached
Set hide
when a tooltip should disappear after its trigger leaves its clipping context. Floating UI’s
hide middleware
reports when the reference is hidden, preventing an otherwise detached tooltip from remaining above content that clips or covers its trigger. Focus the button in the example, then scroll its container until the button is no longer visible.
The trigger is now outside this scroll container’s visible area.
<div
id="tooltip-hide-scroll-container"
class="relative mx-auto h-44 max-w-sm overflow-y-auto overscroll-contain rounded-lg border border-border"
>
<div class="flex min-h-96 flex-col items-center justify-between p-6">
<Tooltip.tooltip id="tooltip-hide-detached" side={:right} hide>
<:trigger><Button.button id="tooltip-hide-trigger" kind={:outline}>Focus, then scroll</Button.button></:trigger>
<:content>This tooltip hides when its trigger is clipped</:content>
</Tooltip.tooltip>
<p class="text-center text-sm text-muted-foreground">
The trigger is now outside this scroll container’s visible area.
</p>
</div>
</div>
Placement
Set side
to choose a preferred placement. Floating UI flips or shifts the tooltip when that placement would leave the viewport. The
start
and end
alignments follow the writing direction.
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Tooltip.tooltip
:for={{label, side} <- [{"Top", :top}, {"Right", :right}, {"Bottom", :bottom}, {"Left", :left}, {"Top start", :top_start}, {"Top end", :top_end}, {"Bottom start", :bottom_start}, {"Bottom end", :bottom_end}]}
id={"tooltip-#{side}"}
side={side}
>
<:trigger>
<Button.button
id={"tooltip-#{side}-trigger"}
kind={:outline}
class="aspect-square h-auto! w-full"
>{label}</Button.button>
</:trigger>
<:content>{label} placement</:content>
</Tooltip.tooltip>
</div>
Distance
Use offset
to set the distance in pixels between the trigger and tooltip. This value is passed to Floating UI’s
offset
middleware.
<div class="flex flex-wrap items-center justify-center gap-3">
<Tooltip.tooltip id="tooltip-offset-tight" offset={2}>
<:trigger><Button.button id="tooltip-offset-tight-trigger" kind={:outline}>Tight</Button.button></:trigger>
<:content>2 pixel offset</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="tooltip-offset-default">
<:trigger><Button.button id="tooltip-offset-default-trigger" kind={:outline}>Default</Button.button></:trigger>
<:content>8 pixel offset</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="tooltip-offset-spacious" offset={16}>
<:trigger><Button.button id="tooltip-offset-spacious-trigger" kind={:outline}>Spacious</Button.button></:trigger>
<:content>16 pixel offset</:content>
</Tooltip.tooltip>
</div>
Hover delay
Set delay_ms
when a tooltip should wait before opening on hover. Keyboard focus opens it immediately.
<div class="flex flex-wrap items-center justify-center gap-3">
<Tooltip.tooltip id="tooltip-delay-none">
<:trigger><Button.button kind={:outline}>Immediate</Button.button></:trigger>
<:content>No delay</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="tooltip-delay-short" delay_ms={300}>
<:trigger><Button.button kind={:outline}>300 ms</Button.button></:trigger>
<:content>Short delay</:content>
</Tooltip.tooltip>
<Tooltip.tooltip id="tooltip-delay-long" delay_ms={700}>
<:trigger><Button.button kind={:outline}>700 ms</Button.button></:trigger>
<:content>Long delay</:content>
</Tooltip.tooltip>
</div>
Delay groups
Give related tooltips the same group
value to apply the configured delay only when entering the group. Once one tooltip is open, moving to another member opens it immediately and closes the previous tooltip. Set
group_timeout_ms
to keep the group warm briefly after the current tooltip closes; otherwise it cools down immediately.
<div class="flex flex-wrap items-center justify-center gap-2">
<Tooltip.tooltip
:for={action <- ["Archive", "Duplicate", "Share"]}
id={"tooltip-group-#{String.downcase(action)}"}
group="document-actions"
delay_ms={500}
group_timeout_ms={300}
>
<:trigger><Button.button kind={:outline}>{action}</Button.button></:trigger>
<:content>{action} this document</:content>
</Tooltip.tooltip>
</div>
Rich content
The content
slot accepts HEEx, including multiple lines and presentational markup. Keep actions and other interactive controls in a popover instead of a tooltip.
<div class="flex justify-center">
<Tooltip.tooltip id="tooltip-shortcut" side={:bottom}>
<:trigger><Button.button kind={:outline}>Keyboard shortcut</Button.button></:trigger>
<:content>
<span class="flex items-center gap-3">
<span>Open command menu</span>
<kbd class="rounded bg-primary-foreground/15 px-1.5 py-0.5 font-mono text-[0.9em]">⌘K</kbd>
</span>
</:content>
</Tooltip.tooltip>
</div>
Add this module
mix dizzy.add tooltip
Component API
Tooltip.tooltip
Attributes
| Name | Type | Default | Values |
|---|---|---|---|
arrow |
:boolean
| false | — |
delay_ms |
:integer
| 0 | — |
group |
:string
| nil | — |
group_timeout_ms |
:integer
| 0 | — |
hide |
:boolean
| false | — |
id |
:string
| Required | — |
offset |
:integer
| 8 | — |
placement_strategy |
:atom
| :flip | nil, :flip, :auto_placement |
shift |
:boolean
| true | — |
side |
:atom
| :top | :top, :top_start, :top_end, :bottom, :bottom_start, :bottom_end, :left, :left_start, :left_end, :right, :right_start, :right_end |
strategy |
:atom
| :fixed | :absolute, :fixed |
Slots
| Name | Required |
|---|---|
content | Required |
trigger | Required |
JavaScript hook
DizzyTooltip
Connects the component’s explicit trigger and content slots, adding their accessible description relationship without replacing or styling the trigger. It shows on hover or focus, dismisses with Escape, coordinates optional delay groups, and uses Floating UI to position the content while avoiding clipping at edges.
mix dizzy.add includes the hook and performs
any required asset setup automatically.