Tooltip
A short label that appears on hover or keyboard focus. The panel renders in the browser's top layer, so no container can clip it.
Installation
bin/rails g shadcnrb:component tooltip
Example
The block is the panel. t.trigger marks what the panel
is attached to — you write that element yourself with any helper, and it
can sit anywhere in the block since it's hoisted to the top of the
wrapper. sui.hover_card takes the same shape.
<%= sui.tooltip do |t| %> <%= t.trigger { sui.button "Save", variant: :outline } %> Add to library <% end %>
Attaching to any helper
Give t.trigger a block and your markup is used
verbatim — every helper keeps its own signature. The tooltip never
intercepts it: hover is bound on the wrapper, so
button_to keeps its CSRF form and links keep their
paths.
<%= sui.tooltip do |t| %> <%= t.trigger { sui.link_to "Docs", "https://ui.shadcn.com", target: "_blank" } %> Opens in a new tab <% end %> <%= sui.tooltip side: :bottom do |t| %> <%= t.trigger do %> <%= sui.button_to "Delete", destroy_demo_chart_path(1), method: :delete, variant: :destructive, size: :sm %> <% end %> Delete this chart <% end %> <%= sui.tooltip do |t| %> <%= t.trigger { sui.button variant: :ghost, size: :icon, icon: :ellipsis } %> Settings <% end %>
Sides
side: takes :top (default),
:right, :bottom, :left.
When the declared side doesn't fit the viewport the panel flips to the
opposite one, and it slides along the free axis to stay in view — scroll
this page with a tooltip open to watch it.
<% %i[top right bottom left].each do |side| %> <%= sui.tooltip side: side do |t| %> <%= t.trigger { sui.button side.to_s.titleize, variant: :outline } %> On the <%= side %> <% end %> <% end %>
Alignment
align: shifts the panel along the free axis —
:start, :center (default),
:end. Handy when the trigger sits at a container edge.
<% %i[start center end].each do |align| %> <%= sui.tooltip side: :bottom, align: align do |t| %> <%= t.trigger { sui.button align.to_s.titleize, variant: :outline } %> Aligned <%= align %> <% end %> <% end %>
Delays
Milliseconds before the panel opens and after the pointer leaves. Both
default to 0 — shadcn's TooltipProvider
default, minus the provider.
<%= sui.tooltip delay: 500, close_delay: 200 do |t| %> <%= t.trigger { sui.button "Patient", variant: :secondary } %> Waited half a second <% end %>
Markup panels
The panel body is ordinary ERB, so anything composes inside it. Style the
panel itself through content: — its own option hash,
the same way button_to takes form:.
<%= sui.tooltip content: { class: "w-44" } do |t| %> <%= t.trigger { sui.button "Save" } %> <span class="flex items-center justify-between gap-2"> Save changes <kbd class="rounded border border-background/30 px-1 font-mono text-[10px]">⌘S</kbd> </span> <% end %> <%= sui.tooltip side: :bottom do |t| %> <%= t.trigger { sui.button "Two lines", variant: :outline } %> <span class="block font-medium">Rate limited</span> <span class="block text-background/70">Try again in about a minute.</span> <% end %>
Long labels
The panel is w-fit max-w-xs, same as upstream: it hugs
a short label and wraps past 20rem.
<%= sui.tooltip do |t| %> <%= t.trigger { sui.button "Long label", variant: :outline } %> Exports the current view as CSV. Large exports are emailed to you when they finish. <% end %>
Disabled trigger
Hover is bound on the wrapper, not the trigger, so a disabled button
still shows its tooltip — no <span> wrapper needed.
<%= sui.tooltip do |t| %> <%= t.trigger { sui.button "Publish", variant: :outline, disabled: true } %> Add a title before publishing <% end %>
Clipping
The panel is a native popover="manual" element shown in
the browser's top layer, so ancestors with overflow: hidden
(or scrolling containers, transforms, stacking contexts) can't clip or
cover it. This trigger sits in a cramped overflow-hidden
box; the tooltip doesn't care.
<div class="h-12 w-40 overflow-hidden rounded-md border border-dashed p-2"> <%= sui.tooltip do |t| %> <%= t.trigger { sui.button "Boxed in", variant: :outline, size: :sm } %> Escaped the box <% end %> </div>
API
| Method | Args | Default | Description |
|---|---|---|---|
sui.tooltip | side:, align:, arrow:, delay:, close_delay:, content:, **opts | :top / :center / true / 0 / 0 | Wrapper; the block body is the panel. Mounts the shared shadcnrb--anchored--component controller and owns the hover/focus wiring. **opts land on the wrapper, content: on the panel. |
t.trigger | &block | — | Marks which part of the block is the trigger; everything else is the panel. Your markup, verbatim — the component renders no button of its own, which is why it has no dependencies. Hoisted to the top of the wrapper, so its position in the block doesn't matter. |
aria-describedby is wired on connect to the hoisted
trigger's first element. Tag your own element
data-slot="tooltip-trigger" to point it somewhere else
— worth doing if the trigger block renders several siblings.
Divergences
No TooltipProvider — delays are per-tooltip. No
TooltipContent part either; the block body is the
panel. Positioning is @floating-ui/dom, vendored via
importmap (no npm), and the panel renders in the browser's top layer
instead of a portal. The
sideOffset / alignOffset props are
baked in at 4px.