Tooltip

New

Overview

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.

Phoenix LiveView
HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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.

HEEx
<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

terminal
mix dizzy.add tooltip

Component API

Tooltip.tooltip

Attributes

Attributes for Tooltip.tooltip
NameTypeDefault 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 :flipnil, :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

Slots for Tooltip.tooltip
NameRequired
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.

Explore Dizzy