A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
Add to library
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/tooltip") templ TooltipDemo() { @tooltip.Tooltip() { @button.Button(button.Props{ Variant: button.VariantOutline, Attributes: tooltip.Trigger(ctx), }) { Hover } @tooltip.Content() { <p>Add to library</p> } }}Installation
shadcn-templ add tooltipCopy and paste the following code into your project.
package tooltip import ( "context" "strconv" "github.com/axadrn/shadcn-templ/v2/utils") type Side string const ( SideTop Side = "top" SideRight Side = "right" SideBottom Side = "bottom" SideLeft Side = "left") type ctxKey string const idKey ctxKey = "tooltipID" func tooltipID(ctx context.Context) string { if id, ok := ctx.Value(idKey).(string); ok { return id } return ""} // Trigger returns the attributes that turn any element into the tooltip// trigger — the asChild equivalent: no wrapper element. Spread them onto your// own element://// @button.Button(button.Props{Attributes: tooltip.Trigger(ctx)}) { ... }func Trigger(ctx context.Context) templ.Attributes { return templ.Attributes{ "data-tui-tooltip-trigger": true, "aria-describedby": tooltipID(ctx), }} type Props struct { ID string} type ContentProps struct { Class string Attributes templ.Attributes // Side of the trigger the tooltip shows on. Defaults to top; flips // automatically when there is no room. Side Side // SideOffset is the gap to the trigger in px. Defaults to 4 (Base UI). SideOffset int} // Tooltip renders no element: it only generates the id that links Trigger and// Content (via ctx), so the trigger stays a direct child of its layout.templ Tooltip(props ...Props) { {{ var p Props }} if len(props) > 0 { {{ p = props[0] }} } if p.ID == "" { {{ p.ID = utils.RandomID() }} } {{ ctx = context.WithValue(ctx, idKey, p.ID) }} { children... }} templ Content(props ...ContentProps) { {{ var p ContentProps }} if len(props) > 0 { {{ p = props[0] }} } if p.Side == "" { {{ p.Side = SideTop }} } if p.SideOffset == 0 { {{ p.SideOffset = 4 }} } // Inert until the script portals it to <body>: inline it would count as a // layout sibling (e.g. button group's last-child rounding). <template data-tui-tooltip-portal> <div id={ tooltipID(ctx) } data-slot="tooltip-content" data-tui-tooltip-content data-tui-tooltip-side={ string(p.Side) } data-tui-tooltip-side-offset={ strconv.Itoa(p.SideOffset) } data-state="closed" popover="manual" role="tooltip" class={ utils.CN( // 1:1 base/ui/tooltip.tsx TooltipContent, the look comes from // cn-tooltip-content (animations key on our data-state attribute). // origin-(--transform-origin) becomes our JS variable name. "cn-tooltip-content cn-tooltip-content-logical z-50 w-fit max-w-xs origin-(--tui-tooltip-transform-origin) bg-foreground text-background", // Native [popover] resets plus JS wiring: the popup stays mounted // after animate-out until hidePopover runs. "pointer-events-none fixed inset-auto left-0 top-0 m-0 overflow-visible data-closed:fill-mode-forwards", p.Class, ), } { p.Attributes... } > { children... } <span data-tui-tooltip-arrow class="cn-tooltip-arrow cn-tooltip-arrow-logical absolute z-50 bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" ></span> </div> </template>}// Uses window.FloatingUIDOM from components/floatingui (loaded in the same bundle).(function () { // Exit animations run at the tw-animate default (150ms); hide after. const EXIT_MS = 170; function allContents() { return document.querySelectorAll("[data-tui-tooltip-content]"); } function contentFor(trigger) { return document.getElementById(trigger.getAttribute("aria-describedby")); } function triggerFor(content) { return document.querySelector( '[data-tui-tooltip-trigger][aria-describedby="' + content.id + '"]', ); } // Base UI zooms the popup out of the anchor's center point (e.g. // "96px -4px"), not out of a placement corner. function anchorOrigin(result, anchorRect, sideOffset) { const side = result.placement.split("-")[0]; const centerX = anchorRect.left + anchorRect.width / 2 - result.x + "px"; const centerY = anchorRect.top + anchorRect.height / 2 - result.y + "px"; if (side === "bottom") return centerX + " " + -sideOffset + "px"; if (side === "top") return centerX + " calc(100% + " + sideOffset + "px)"; if (side === "right") return -sideOffset + "px " + centerY; return "calc(100% + " + sideOffset + "px) " + centerY; } // The arrow styles itself per side (data-side classes, like Base UI's // Arrow); the script only feeds it the side and the centered coordinate. function placeArrow(content, side, arrowData) { const arrowEl = content.querySelector("[data-tui-tooltip-arrow]"); if (!arrowEl) return; arrowEl.setAttribute("data-side", side); arrowEl.style.left = arrowData && arrowData.x != null ? arrowData.x + "px" : ""; arrowEl.style.top = arrowData && arrowData.y != null ? arrowData.y + "px" : ""; } // Moves the content to <body> (shadcn portals it the same way). Also removes // contents whose trigger is gone (leftovers from swapped-out pages). function portal(content) { document.querySelectorAll("body > [data-tui-tooltip-content]").forEach((c) => { if (c !== content && !triggerFor(c)) c.remove(); }); if (content.parentElement !== document.body) { document.body.appendChild(content); } } function positionContent(content, trigger) { const { computePosition, offset, flip, shift, arrow } = window.FloatingUIDOM; const side = content.getAttribute("data-tui-tooltip-side") || "top"; const sideOffset = parseInt(content.getAttribute("data-tui-tooltip-side-offset"), 10) || 4; const arrowEl = content.querySelector("[data-tui-tooltip-arrow]"); return computePosition(trigger, content, { placement: side, strategy: "fixed", middleware: [ offset(sideOffset), flip(), shift({ padding: 8 }), arrowEl ? arrow({ element: arrowEl, padding: 6 }) : undefined, ].filter(Boolean), }).then((result) => { content.style.transition = "none"; content.style.left = result.x + "px"; content.style.top = result.y + "px"; content.style.setProperty( "--tui-tooltip-transform-origin", anchorOrigin(result, trigger.getBoundingClientRect(), sideOffset), ); const finalSide = result.placement.split("-")[0]; content.setAttribute("data-side", finalSide); placeArrow(content, finalSide, result.middlewareData.arrow); content.offsetHeight; // flush styles before re-enabling transitions content.style.transition = ""; }); } function open(trigger) { // Consumers can suppress a tooltip situationally (e.g. the sidebar only // shows menu tooltips while collapsed to icons). if (trigger.hasAttribute("data-tui-tooltip-disabled")) return; const content = contentFor(trigger); if (!content) return; clearTimeout(content._tuiHide); portal(content); if (!content.matches(":popover-open")) { content.showPopover(); // native top layer } // Position it invisibly first, then play the enter animation in place. content.style.visibility = "hidden"; positionContent(content, trigger).then(() => { if (!content.matches(":popover-open")) return; // closed meanwhile content.style.visibility = ""; content.setAttribute("data-state", "open"); }); } function close(content) { if (!content.matches(":popover-open")) return; content.setAttribute("data-state", "closed"); clearTimeout(content._tuiHide); content._tuiHide = setTimeout(() => { if (content.getAttribute("data-state") === "closed" && content.matches(":popover-open")) { content.hidePopover(); } }, EXIT_MS); } function closeAll() { allContents().forEach(close); } // ----- events ------------------------------------------------------------- document.addEventListener("mouseover", (e) => { const trigger = e.target.closest("[data-tui-tooltip-trigger]"); if (trigger) open(trigger); }); document.addEventListener("mouseout", (e) => { const trigger = e.target.closest("[data-tui-tooltip-trigger]"); if (!trigger) return; if (e.relatedTarget && trigger.contains(e.relatedTarget)) return; // still inside const content = contentFor(trigger); if (content) close(content); }); // Keyboard: show on focus, hide on blur. Like Base UI, only visible // focus opens the tooltip, so programmatic focus (e.g. a dialog's // autofocus) does not pop it. document.addEventListener("focusin", (e) => { const trigger = e.target.closest("[data-tui-tooltip-trigger]"); if (trigger && trigger.matches(":focus-visible")) open(trigger); }); document.addEventListener("focusout", (e) => { const trigger = e.target.closest("[data-tui-tooltip-trigger]"); if (!trigger) return; const content = contentFor(trigger); if (content) close(content); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeAll(); }); // Lift every content out of its inert <template> into <body>, shadcn's // portal renders it there from the start. function portalAll() { document .querySelectorAll("template[data-tui-tooltip-portal]") .forEach((tpl) => { const content = tpl.content.querySelector("[data-tui-tooltip-content]"); if (content) { const stale = document.getElementById(content.id); if (stale) stale.remove(); // htmx re-swap of the same id portal(content); } tpl.remove(); }); allContents().forEach(portal); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", portalAll); } else { portalAll(); } new MutationObserver(portalAll).observe(document.documentElement, { childList: true, subtree: true, }); // Keep open tooltips anchored while scrolling or resizing. function repositionOpen() { allContents().forEach((content) => { if (content.getAttribute("data-state") !== "open") return; const trigger = triggerFor(content); if (trigger) positionContent(content, trigger); }); } window.addEventListener("scroll", repositionOpen, true); window.addEventListener("resize", repositionOpen);})();// https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]!(function (t, e) { "object" == typeof exports && "undefined" != typeof module ? e(exports) : "function" == typeof define && define.amd ? define(["exports"], e) : e( ((t = "undefined" != typeof globalThis ? globalThis : t || self).FloatingUICore = {}) );})(this, function (t) { "use strict"; const e = ["top", "right", "bottom", "left"], n = ["start", "end"], i = e.reduce((t, e) => t.concat(e, e + "-" + n[0], e + "-" + n[1]), []), o = Math.min, r = Math.max, a = { left: "right", right: "left", bottom: "top", top: "bottom" }, l = { start: "end", end: "start" }; function s(t, e, n) { return r(t, o(e, n)); } function f(t, e) { return "function" == typeof t ? t(e) : t; } function c(t) { return t.split("-")[0]; } function u(t) { return t.split("-")[1]; } function m(t) { return "x" === t ? "y" : "x"; } function d(t) { return "y" === t ? "height" : "width"; } function g(t) { return ["top", "bottom"].includes(c(t)) ? "y" : "x"; } function p(t) { return m(g(t)); } function h(t, e, n) { void 0 === n && (n = !1); const i = u(t), o = p(t), r = d(o); let a = "x" === o ? i === (n ? "end" : "start") ? "right" : "left" : "start" === i ? "bottom" : "top"; return e.reference[r] > e.floating[r] && (a = w(a)), [a, w(a)]; } function y(t) { return t.replace(/start|end/g, (t) => l[t]); } function w(t) { return t.replace(/left|right|bottom|top/g, (t) => a[t]); } function x(t) { return "number" != typeof t ? (function (t) { return { top: 0, right: 0, bottom: 0, left: 0, ...t }; })(t) : { top: t, right: t, bottom: t, left: t }; } function v(t) { const { x: e, y: n, width: i, height: o } = t; return { width: i, height: o, top: n, left: e, right: e + i, bottom: n + o, x: e, y: n, }; } function b(t, e, n) { let { reference: i, floating: o } = t; const r = g(e), a = p(e), l = d(a), s = c(e), f = "y" === r, m = i.x + i.width / 2 - o.width / 2, h = i.y + i.height / 2 - o.height / 2, y = i[l] / 2 - o[l] / 2; let w; switch (s) { case "top": w = { x: m, y: i.y - o.height }; break; case "bottom": w = { x: m, y: i.y + i.height }; break; case "right": w = { x: i.x + i.width, y: h }; break; case "left": w = { x: i.x - o.width, y: h }; break; default: w = { x: i.x, y: i.y }; } switch (u(e)) { case "start": w[a] -= y * (n && f ? -1 : 1); break; case "end": w[a] += y * (n && f ? -1 : 1); } return w; } async function A(t, e) { var n; void 0 === e && (e = {}); const { x: i, y: o, platform: r, rects: a, elements: l, strategy: s } = t, { boundary: c = "clippingAncestors", rootBoundary: u = "viewport", elementContext: m = "floating", altBoundary: d = !1, padding: g = 0, } = f(e, t), p = x(g), h = l[d ? ("floating" === m ? "reference" : "floating") : m], y = v( await r.getClippingRect({ element: null == (n = await (null == r.isElement ? void 0 : r.isElement(h))) || n ? h : h.contextElement || (await (null == r.getDocumentElement ? void 0 : r.getDocumentElement(l.floating))), boundary: c, rootBoundary: u, strategy: s, }) ), w = "floating" === m ? { x: i, y: o, width: a.floating.width, height: a.floating.height } : a.reference, b = await (null == r.getOffsetParent ? void 0 : r.getOffsetParent(l.floating)), A = ((await (null == r.isElement ? void 0 : r.isElement(b))) && (await (null == r.getScale ? void 0 : r.getScale(b)))) || { x: 1, y: 1, }, R = v( r.convertOffsetParentRelativeRectToViewportRelativeRect ? await r.convertOffsetParentRelativeRectToViewportRelativeRect({ elements: l, rect: w, offsetParent: b, strategy: s, }) : w ); return { top: (y.top - R.top + p.top) / A.y, bottom: (R.bottom - y.bottom + p.bottom) / A.y, left: (y.left - R.left + p.left) / A.x, right: (R.right - y.right + p.right) / A.x, }; } function R(t, e) { return { top: t.top - e.height, right: t.right - e.width, bottom: t.bottom - e.height, left: t.left - e.width, }; } function P(t) { return e.some((e) => t[e] >= 0); } function D(t) { const e = o(...t.map((t) => t.left)), n = o(...t.map((t) => t.top)); return { x: e, y: n, width: r(...t.map((t) => t.right)) - e, height: r(...t.map((t) => t.bottom)) - n, }; } (t.arrow = (t) => ({ name: "arrow", options: t, async fn(e) { const { x: n, y: i, placement: r, rects: a, platform: l, elements: c, middlewareData: m, } = e, { element: g, padding: h = 0 } = f(t, e) || {}; if (null == g) return {}; const y = x(h), w = { x: n, y: i }, v = p(r), b = d(v), A = await l.getDimensions(g), R = "y" === v, P = R ? "top" : "left", D = R ? "bottom" : "right", T = R ? "clientHeight" : "clientWidth", O = a.reference[b] + a.reference[v] - w[v] - a.floating[b], E = w[v] - a.reference[v], L = await (null == l.getOffsetParent ? void 0 : l.getOffsetParent(g)); let k = L ? L[T] : 0; (k && (await (null == l.isElement ? void 0 : l.isElement(L)))) || (k = c.floating[T] || a.floating[b]); const C = O / 2 - E / 2, B = k / 2 - A[b] / 2 - 1, H = o(y[P], B), S = o(y[D], B), F = H, j = k - A[b] - S, z = k / 2 - A[b] / 2 + C, M = s(F, z, j), V = !m.arrow && null != u(r) && z !== M && a.reference[b] / 2 - (z < F ? H : S) - A[b] / 2 < 0, W = V ? (z < F ? z - F : z - j) : 0; return { [v]: w[v] + W, data: { [v]: M, centerOffset: z - M - W, ...(V && { alignmentOffset: W }), }, reset: V, }; }, })), (t.autoPlacement = function (t) { return ( void 0 === t && (t = {}), { name: "autoPlacement", options: t, async fn(e) { var n, o, r; const { rects: a, middlewareData: l, placement: s, platform: m, elements: d, } = e, { crossAxis: g = !1, alignment: p, allowedPlacements: w = i, autoAlignment: x = !0, ...v } = f(t, e), b = void 0 !== p || w === i ? (function (t, e, n) { return ( t ? [ ...n.filter((e) => u(e) === t), ...n.filter((e) => u(e) !== t), ] : n.filter((t) => c(t) === t) ).filter((n) => !t || u(n) === t || (!!e && y(n) !== n)); })(p || null, x, w) : w, R = await A(e, v), P = (null == (n = l.autoPlacement) ? void 0 : n.index) || 0, D = b[P]; if (null == D) return {}; const T = h( D, a, await (null == m.isRTL ? void 0 : m.isRTL(d.floating)) ); if (s !== D) return { reset: { placement: b[0] } }; const O = [R[c(D)], R[T[0]], R[T[1]]], E = [ ...((null == (o = l.autoPlacement) ? void 0 : o.overflows) || []), { placement: D, overflows: O }, ], L = b[P + 1]; if (L) return { data: { index: P + 1, overflows: E }, reset: { placement: L }, }; const k = E.map((t) => { const e = u(t.placement); return [ t.placement, e && g ? t.overflows.slice(0, 2).reduce((t, e) => t + e, 0) : t.overflows[0], t.overflows, ]; }).sort((t, e) => t[1] - e[1]), C = (null == (r = k.filter((t) => t[2].slice(0, u(t[0]) ? 2 : 3).every((t) => t <= 0) )[0]) ? void 0 : r[0]) || k[0][0]; return C !== s ? { data: { index: P + 1, overflows: E }, reset: { placement: C }, } : {}; }, } ); }), (t.computePosition = async (t, e, n) => { const { placement: i = "bottom", strategy: o = "absolute", middleware: r = [], platform: a, } = n, l = r.filter(Boolean), s = await (null == a.isRTL ? void 0 : a.isRTL(e)); let f = await a.getElementRects({ reference: t, floating: e, strategy: o, }), { x: c, y: u } = b(f, i, s), m = i, d = {}, g = 0; for (let n = 0; n < l.length; n++) { const { name: r, fn: p } = l[n], { x: h, y: y, data: w, reset: x, } = await p({ x: c, y: u, initialPlacement: i, placement: m, strategy: o, middlewareData: d, rects: f, platform: a, elements: { reference: t, floating: e }, }); (c = null != h ? h : c), (u = null != y ? y : u), (d = { ...d, [r]: { ...d[r], ...w } }), x && g <= 50 && (g++, "object" == typeof x && (x.placement && (m = x.placement), x.rects && (f = !0 === x.rects ? await a.getElementRects({ reference: t, floating: e, strategy: o, }) : x.rects), ({ x: c, y: u } = b(f, m, s))), (n = -1)); } return { x: c, y: u, placement: m, strategy: o, middlewareData: d }; }), (t.detectOverflow = A), (t.flip = function (t) { return ( void 0 === t && (t = {}), { name: "flip", options: t, async fn(e) { var n, i; const { placement: o, middlewareData: r, rects: a, initialPlacement: l, platform: s, elements: m, } = e, { mainAxis: d = !0, crossAxis: p = !0, fallbackPlacements: x, fallbackStrategy: v = "bestFit", fallbackAxisSideDirection: b = "none", flipAlignment: R = !0, ...P } = f(t, e); if (null != (n = r.arrow) && n.alignmentOffset) return {}; const D = c(o), T = g(l), O = c(l) === l, E = await (null == s.isRTL ? void 0 : s.isRTL(m.floating)), L = x || (O || !R ? [w(l)] : (function (t) { const e = w(t); return [y(t), e, y(e)]; })(l)), k = "none" !== b; !x && k && L.push( ...(function (t, e, n, i) { const o = u(t); let r = (function (t, e, n) { const i = ["left", "right"], o = ["right", "left"], r = ["top", "bottom"], a = ["bottom", "top"]; switch (t) { case "top": case "bottom": return n ? (e ? o : i) : e ? i : o; case "left": case "right": return e ? r : a; default: return []; } })(c(t), "start" === n, i); return ( o && ((r = r.map((t) => t + "-" + o)), e && (r = r.concat(r.map(y)))), r ); })(l, R, b, E) ); const C = [l, ...L], B = await A(e, P), H = []; let S = (null == (i = r.flip) ? void 0 : i.overflows) || []; if ((d && H.push(B[D]), p)) { const t = h(o, a, E); H.push(B[t[0]], B[t[1]]); } if ( ((S = [...S, { placement: o, overflows: H }]), !H.every((t) => t <= 0)) ) { var F, j; const t = ((null == (F = r.flip) ? void 0 : F.index) || 0) + 1, e = C[t]; if (e) { var z; const n = "alignment" === p && T !== g(e), i = (null == (z = S[0]) ? void 0 : z.overflows[0]) > 0; if (!n || i) return { data: { index: t, overflows: S }, reset: { placement: e }, }; } let n = null == (j = S.filter((t) => t.overflows[0] <= 0).sort( (t, e) => t.overflows[1] - e.overflows[1] )[0]) ? void 0 : j.placement; if (!n) switch (v) { case "bestFit": { var M; const t = null == (M = S.filter((t) => { if (k) { const e = g(t.placement); return e === T || "y" === e; } return !0; }) .map((t) => [ t.placement, t.overflows .filter((t) => t > 0) .reduce((t, e) => t + e, 0), ]) .sort((t, e) => t[1] - e[1])[0]) ? void 0 : M[0]; t && (n = t); break; } case "initialPlacement": n = l; } if (o !== n) return { reset: { placement: n } }; } return {}; }, } ); }), (t.hide = function (t) { return ( void 0 === t && (t = {}), { name: "hide", options: t, async fn(e) { const { rects: n } = e, { strategy: i = "referenceHidden", ...o } = f(t, e); switch (i) { case "referenceHidden": { const t = R( await A(e, { ...o, elementContext: "reference" }), n.reference ); return { data: { referenceHiddenOffsets: t, referenceHidden: P(t) }, }; } case "escaped": { const t = R(await A(e, { ...o, altBoundary: !0 }), n.floating); return { data: { escapedOffsets: t, escaped: P(t) } }; } default: return {}; } }, } ); }), (t.inline = function (t) { return ( void 0 === t && (t = {}), { name: "inline", options: t, async fn(e) { const { placement: n, elements: i, rects: a, platform: l, strategy: s, } = e, { padding: u = 2, x: m, y: d } = f(t, e), p = Array.from( (await (null == l.getClientRects ? void 0 : l.getClientRects(i.reference))) || [] ), h = (function (t) { const e = t.slice().sort((t, e) => t.y - e.y), n = []; let i = null; for (let t = 0; t < e.length; t++) { const o = e[t]; !i || o.y - i.y > i.height / 2 ? n.push([o]) : n[n.length - 1].push(o), (i = o); } return n.map((t) => v(D(t))); })(p), y = v(D(p)), w = x(u); const b = await l.getElementRects({ reference: { getBoundingClientRect: function () { if ( 2 === h.length && h[0].left > h[1].right && null != m && null != d ) return ( h.find( (t) => m > t.left - w.left && m < t.right + w.right && d > t.top - w.top && d < t.bottom + w.bottom ) || y ); if (h.length >= 2) { if ("y" === g(n)) { const t = h[0], e = h[h.length - 1], i = "top" === c(n), o = t.top, r = e.bottom, a = i ? t.left : e.left, l = i ? t.right : e.right; return { top: o, bottom: r, left: a, right: l, width: l - a, height: r - o, x: a, y: o, }; } const t = "left" === c(n), e = r(...h.map((t) => t.right)), i = o(...h.map((t) => t.left)), a = h.filter((n) => (t ? n.left === i : n.right === e)), l = a[0].top, s = a[a.length - 1].bottom; return { top: l, bottom: s, left: i, right: e, width: e - i, height: s - l, x: i, y: l, }; } return y; }, }, floating: i.floating, strategy: s, }); return a.reference.x !== b.reference.x || a.reference.y !== b.reference.y || a.reference.width !== b.reference.width || a.reference.height !== b.reference.height ? { reset: { rects: b } } : {}; }, } ); }), (t.limitShift = function (t) { return ( void 0 === t && (t = {}), { options: t, fn(e) { const { x: n, y: i, placement: o, rects: r, middlewareData: a } = e, { offset: l = 0, mainAxis: s = !0, crossAxis: u = !0 } = f(t, e), d = { x: n, y: i }, p = g(o), h = m(p); let y = d[h], w = d[p]; const x = f(l, e), v = "number" == typeof x ? { mainAxis: x, crossAxis: 0 } : { mainAxis: 0, crossAxis: 0, ...x }; if (s) { const t = "y" === h ? "height" : "width", e = r.reference[h] - r.floating[t] + v.mainAxis, n = r.reference[h] + r.reference[t] - v.mainAxis; y < e ? (y = e) : y > n && (y = n); } if (u) { var b, A; const t = "y" === h ? "width" : "height", e = ["top", "left"].includes(c(o)), n = r.reference[p] - r.floating[t] + ((e && (null == (b = a.offset) ? void 0 : b[p])) || 0) + (e ? 0 : v.crossAxis), i = r.reference[p] + r.reference[t] + (e ? 0 : (null == (A = a.offset) ? void 0 : A[p]) || 0) - (e ? v.crossAxis : 0); w < n ? (w = n) : w > i && (w = i); } return { [h]: y, [p]: w }; }, } ); }), (t.offset = function (t) { return ( void 0 === t && (t = 0), { name: "offset", options: t, async fn(e) { var n, i; const { x: o, y: r, placement: a, middlewareData: l } = e, s = await (async function (t, e) { const { placement: n, platform: i, elements: o } = t, r = await (null == i.isRTL ? void 0 : i.isRTL(o.floating)), a = c(n), l = u(n), s = "y" === g(n), m = ["left", "top"].includes(a) ? -1 : 1, d = r && s ? -1 : 1, p = f(e, t); let { mainAxis: h, crossAxis: y, alignmentAxis: w, } = "number" == typeof p ? { mainAxis: p, crossAxis: 0, alignmentAxis: null } : { mainAxis: p.mainAxis || 0, crossAxis: p.crossAxis || 0, alignmentAxis: p.alignmentAxis, }; return ( l && "number" == typeof w && (y = "end" === l ? -1 * w : w), s ? { x: y * d, y: h * m } : { x: h * m, y: y * d } ); })(e, t); return a === (null == (n = l.offset) ? void 0 : n.placement) && null != (i = l.arrow) && i.alignmentOffset ? {} : { x: o + s.x, y: r + s.y, data: { ...s, placement: a } }; }, } ); }), (t.rectToClientRect = v), (t.shift = function (t) { return ( void 0 === t && (t = {}), { name: "shift", options: t, async fn(e) { const { x: n, y: i, placement: o } = e, { mainAxis: r = !0, crossAxis: a = !1, limiter: l = { fn: (t) => { let { x: e, y: n } = t; return { x: e, y: n }; }, }, ...u } = f(t, e), d = { x: n, y: i }, p = await A(e, u), h = g(c(o)), y = m(h); let w = d[y], x = d[h]; if (r) { const t = "y" === y ? "bottom" : "right"; w = s(w + p["y" === y ? "top" : "left"], w, w - p[t]); } if (a) { const t = "y" === h ? "bottom" : "right"; x = s(x + p["y" === h ? "top" : "left"], x, x - p[t]); } const v = l.fn({ ...e, [y]: w, [h]: x }); return { ...v, data: { x: v.x - n, y: v.y - i, enabled: { [y]: r, [h]: a } }, }; }, } ); }), (t.size = function (t) { return ( void 0 === t && (t = {}), { name: "size", options: t, async fn(e) { var n, i; const { placement: a, rects: l, platform: s, elements: m } = e, { apply: d = () => {}, ...p } = f(t, e), h = await A(e, p), y = c(a), w = u(a), x = "y" === g(a), { width: v, height: b } = l.floating; let R, P; "top" === y || "bottom" === y ? ((R = y), (P = w === ((await (null == s.isRTL ? void 0 : s.isRTL(m.floating))) ? "start" : "end") ? "left" : "right")) : ((P = y), (R = "end" === w ? "top" : "bottom")); const D = b - h.top - h.bottom, T = v - h.left - h.right, O = o(b - h[R], D), E = o(v - h[P], T), L = !e.middlewareData.shift; let k = O, C = E; if ( (null != (n = e.middlewareData.shift) && n.enabled.x && (C = T), null != (i = e.middlewareData.shift) && i.enabled.y && (k = D), L && !w) ) { const t = r(h.left, 0), e = r(h.right, 0), n = r(h.top, 0), i = r(h.bottom, 0); x ? (C = v - 2 * (0 !== t || 0 !== e ? t + e : r(h.left, h.right))) : (k = b - 2 * (0 !== n || 0 !== i ? n + i : r(h.top, h.bottom))); } await d({ ...e, availableWidth: C, availableHeight: k }); const B = await s.getDimensions(m.floating); return v !== B.width || b !== B.height ? { reset: { rects: !0 } } : {}; }, } ); });});// https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]!(function (t, e) { "object" == typeof exports && "undefined" != typeof module ? e(exports, require("./floating_ui_core")) : "function" == typeof define && define.amd ? define(["exports", "./floatingUICore"], e) : e( ((t = "undefined" != typeof globalThis ? globalThis : t || self).FloatingUIDOM = {}), t.FloatingUICore );})(this, function (t, e) { "use strict"; const n = Math.min, o = Math.max, i = Math.round, r = Math.floor, c = (t) => ({ x: t, y: t }); function l() { return "undefined" != typeof window; } function s(t) { return a(t) ? (t.nodeName || "").toLowerCase() : "#document"; } function f(t) { var e; return ( (null == t || null == (e = t.ownerDocument) ? void 0 : e.defaultView) || window ); } function u(t) { var e; return null == (e = (a(t) ? t.ownerDocument : t.document) || window.document) ? void 0 : e.documentElement; } function a(t) { return !!l() && (t instanceof Node || t instanceof f(t).Node); } function d(t) { return !!l() && (t instanceof Element || t instanceof f(t).Element); } function h(t) { return !!l() && (t instanceof HTMLElement || t instanceof f(t).HTMLElement); } function p(t) { return ( !(!l() || "undefined" == typeof ShadowRoot) && (t instanceof ShadowRoot || t instanceof f(t).ShadowRoot) ); } function g(t) { const { overflow: e, overflowX: n, overflowY: o, display: i } = b(t); return ( /auto|scroll|overlay|hidden|clip/.test(e + o + n) && !["inline", "contents"].includes(i) ); } function m(t) { return ["table", "td", "th"].includes(s(t)); } function y(t) { return [":popover-open", ":modal"].some((e) => { try { return t.matches(e); } catch (t) { return !1; } }); } function w(t) { const e = x(), n = d(t) ? b(t) : t; return ( ["transform", "translate", "scale", "rotate", "perspective"].some( (t) => !!n[t] && "none" !== n[t] ) || (!!n.containerType && "normal" !== n.containerType) || (!e && !!n.backdropFilter && "none" !== n.backdropFilter) || (!e && !!n.filter && "none" !== n.filter) || [ "transform", "translate", "scale", "rotate", "perspective", "filter", ].some((t) => (n.willChange || "").includes(t)) || ["paint", "layout", "strict", "content"].some((t) => (n.contain || "").includes(t) ) ); } function x() { return ( !("undefined" == typeof CSS || !CSS.supports) && CSS.supports("-webkit-backdrop-filter", "none") ); } function v(t) { return ["html", "body", "#document"].includes(s(t)); } function b(t) { return f(t).getComputedStyle(t); } function T(t) { return d(t) ? { scrollLeft: t.scrollLeft, scrollTop: t.scrollTop } : { scrollLeft: t.scrollX, scrollTop: t.scrollY }; } function L(t) { if ("html" === s(t)) return t; const e = t.assignedSlot || t.parentNode || (p(t) && t.host) || u(t); return p(e) ? e.host : e; } function R(t) { const e = L(t); return v(e) ? t.ownerDocument ? t.ownerDocument.body : t.body : h(e) && g(e) ? e : R(e); } function C(t, e, n) { var o; void 0 === e && (e = []), void 0 === n && (n = !0); const i = R(t), r = i === (null == (o = t.ownerDocument) ? void 0 : o.body), c = f(i); if (r) { const t = E(c); return e.concat( c, c.visualViewport || [], g(i) ? i : [], t && n ? C(t) : [] ); } return e.concat(i, C(i, [], n)); } function E(t) { return t.parent && Object.getPrototypeOf(t.parent) ? t.frameElement : null; } function S(t) { const e = b(t); let n = parseFloat(e.width) || 0, o = parseFloat(e.height) || 0; const r = h(t), c = r ? t.offsetWidth : n, l = r ? t.offsetHeight : o, s = i(n) !== c || i(o) !== l; return s && ((n = c), (o = l)), { width: n, height: o, $: s }; } function F(t) { return d(t) ? t : t.contextElement; } function O(t) { const e = F(t); if (!h(e)) return c(1); const n = e.getBoundingClientRect(), { width: o, height: r, $: l } = S(e); let s = (l ? i(n.width) : n.width) / o, f = (l ? i(n.height) : n.height) / r; return ( (s && Number.isFinite(s)) || (s = 1), (f && Number.isFinite(f)) || (f = 1), { x: s, y: f } ); } const D = c(0); function H(t) { const e = f(t); return x() && e.visualViewport ? { x: e.visualViewport.offsetLeft, y: e.visualViewport.offsetTop } : D; } function P(t, n, o, i) { void 0 === n && (n = !1), void 0 === o && (o = !1); const r = t.getBoundingClientRect(), l = F(t); let s = c(1); n && (i ? d(i) && (s = O(i)) : (s = O(t))); const u = (function (t, e, n) { return void 0 === e && (e = !1), !(!n || (e && n !== f(t))) && e; })(l, o, i) ? H(l) : c(0); let a = (r.left + u.x) / s.x, h = (r.top + u.y) / s.y, p = r.width / s.x, g = r.height / s.y; if (l) { const t = f(l), e = i && d(i) ? f(i) : i; let n = t, o = E(n); for (; o && i && e !== n; ) { const t = O(o), e = o.getBoundingClientRect(), i = b(o), r = e.left + (o.clientLeft + parseFloat(i.paddingLeft)) * t.x, c = e.top + (o.clientTop + parseFloat(i.paddingTop)) * t.y; (a *= t.x), (h *= t.y), (p *= t.x), (g *= t.y), (a += r), (h += c), (n = f(o)), (o = E(n)); } } return e.rectToClientRect({ width: p, height: g, x: a, y: h }); } function W(t, e) { const n = T(t).scrollLeft; return e ? e.left + n : P(u(t)).left + n; } function M(t, e, n) { void 0 === n && (n = !1); const o = t.getBoundingClientRect(); return { x: o.left + e.scrollLeft - (n ? 0 : W(t, o)), y: o.top + e.scrollTop, }; } function z(t, n, i) { let r; if ("viewport" === n) r = (function (t, e) { const n = f(t), o = u(t), i = n.visualViewport; let r = o.clientWidth, c = o.clientHeight, l = 0, s = 0; if (i) { (r = i.width), (c = i.height); const t = x(); (!t || (t && "fixed" === e)) && ((l = i.offsetLeft), (s = i.offsetTop)); } return { width: r, height: c, x: l, y: s }; })(t, i); else if ("document" === n) r = (function (t) { const e = u(t), n = T(t), i = t.ownerDocument.body, r = o(e.scrollWidth, e.clientWidth, i.scrollWidth, i.clientWidth), c = o(e.scrollHeight, e.clientHeight, i.scrollHeight, i.clientHeight); let l = -n.scrollLeft + W(t); const s = -n.scrollTop; return ( "rtl" === b(i).direction && (l += o(e.clientWidth, i.clientWidth) - r), { width: r, height: c, x: l, y: s } ); })(u(t)); else if (d(n)) r = (function (t, e) { const n = P(t, !0, "fixed" === e), o = n.top + t.clientTop, i = n.left + t.clientLeft, r = h(t) ? O(t) : c(1); return { width: t.clientWidth * r.x, height: t.clientHeight * r.y, x: i * r.x, y: o * r.y, }; })(n, i); else { const e = H(t); r = { x: n.x - e.x, y: n.y - e.y, width: n.width, height: n.height }; } return e.rectToClientRect(r); } function A(t, e) { const n = L(t); return ( !(n === e || !d(n) || v(n)) && ("fixed" === b(n).position || A(n, e)) ); } function B(t, e, n) { const o = h(e), i = u(e), r = "fixed" === n, l = P(t, !0, r, e); let f = { scrollLeft: 0, scrollTop: 0 }; const a = c(0); function d() { a.x = W(i); } if (o || (!o && !r)) if ((("body" !== s(e) || g(i)) && (f = T(e)), o)) { const t = P(e, !0, r, e); (a.x = t.x + e.clientLeft), (a.y = t.y + e.clientTop); } else i && d(); r && !o && i && d(); const p = !i || o || r ? c(0) : M(i, f); return { x: l.left + f.scrollLeft - a.x - p.x, y: l.top + f.scrollTop - a.y - p.y, width: l.width, height: l.height, }; } function V(t) { return "static" === b(t).position; } function N(t, e) { if (!h(t) || "fixed" === b(t).position) return null; if (e) return e(t); let n = t.offsetParent; return u(t) === n && (n = n.ownerDocument.body), n; } function I(t, e) { const n = f(t); if (y(t)) return n; if (!h(t)) { let e = L(t); for (; e && !v(e); ) { if (d(e) && !V(e)) return e; e = L(e); } return n; } let o = N(t, e); for (; o && m(o) && V(o); ) o = N(o, e); return o && v(o) && V(o) && !w(o) ? n : o || (function (t) { let e = L(t); for (; h(e) && !v(e); ) { if (w(e)) return e; if (y(e)) return null; e = L(e); } return null; })(t) || n; } const k = { convertOffsetParentRelativeRectToViewportRelativeRect: function (t) { let { elements: e, rect: n, offsetParent: o, strategy: i } = t; const r = "fixed" === i, l = u(o), f = !!e && y(e.floating); if (o === l || (f && r)) return n; let a = { scrollLeft: 0, scrollTop: 0 }, d = c(1); const p = c(0), m = h(o); if ( (m || (!m && !r)) && (("body" !== s(o) || g(l)) && (a = T(o)), h(o)) ) { const t = P(o); (d = O(o)), (p.x = t.x + o.clientLeft), (p.y = t.y + o.clientTop); } const w = !l || m || r ? c(0) : M(l, a, !0); return { width: n.width * d.x, height: n.height * d.y, x: n.x * d.x - a.scrollLeft * d.x + p.x + w.x, y: n.y * d.y - a.scrollTop * d.y + p.y + w.y, }; }, getDocumentElement: u, getClippingRect: function (t) { let { element: e, boundary: i, rootBoundary: r, strategy: c } = t; const l = [ ...("clippingAncestors" === i ? y(e) ? [] : (function (t, e) { const n = e.get(t); if (n) return n; let o = C(t, [], !1).filter((t) => d(t) && "body" !== s(t)), i = null; const r = "fixed" === b(t).position; let c = r ? L(t) : t; for (; d(c) && !v(c); ) { const e = b(c), n = w(c); n || "fixed" !== e.position || (i = null), ( r ? !n && !i : (!n && "static" === e.position && i && ["absolute", "fixed"].includes(i.position)) || (g(c) && !n && A(t, c)) ) ? (o = o.filter((t) => t !== c)) : (i = e), (c = L(c)); } return e.set(t, o), o; })(e, this._c) : [].concat(i)), r, ], f = l[0], u = l.reduce((t, i) => { const r = z(e, i, c); return ( (t.top = o(r.top, t.top)), (t.right = n(r.right, t.right)), (t.bottom = n(r.bottom, t.bottom)), (t.left = o(r.left, t.left)), t ); }, z(e, f, c)); return { width: u.right - u.left, height: u.bottom - u.top, x: u.left, y: u.top, }; }, getOffsetParent: I, getElementRects: async function (t) { const e = this.getOffsetParent || I, n = this.getDimensions, o = await n(t.floating); return { reference: B(t.reference, await e(t.floating), t.strategy), floating: { x: 0, y: 0, width: o.width, height: o.height }, }; }, getClientRects: function (t) { return Array.from(t.getClientRects()); }, getDimensions: function (t) { const { width: e, height: n } = S(t); return { width: e, height: n }; }, getScale: O, isElement: d, isRTL: function (t) { return "rtl" === b(t).direction; }, }; function q(t, e) { return ( t.x === e.x && t.y === e.y && t.width === e.width && t.height === e.height ); } const U = e.detectOverflow, j = e.offset, X = e.autoPlacement, Y = e.shift, $ = e.flip, _ = e.size, G = e.hide, J = e.arrow, K = e.inline, Q = e.limitShift; (t.arrow = J), (t.autoPlacement = X), (t.autoUpdate = function (t, e, i, c) { void 0 === c && (c = {}); const { ancestorScroll: l = !0, ancestorResize: s = !0, elementResize: f = "function" == typeof ResizeObserver, layoutShift: a = "function" == typeof IntersectionObserver, animationFrame: d = !1, } = c, h = F(t), p = l || s ? [...(h ? C(h) : []), ...C(e)] : []; p.forEach((t) => { l && t.addEventListener("scroll", i, { passive: !0 }), s && t.addEventListener("resize", i); }); const g = h && a ? (function (t, e) { let i, c = null; const l = u(t); function s() { var t; clearTimeout(i), null == (t = c) || t.disconnect(), (c = null); } return ( (function f(u, a) { void 0 === u && (u = !1), void 0 === a && (a = 1), s(); const d = t.getBoundingClientRect(), { left: h, top: p, width: g, height: m } = d; if ((u || e(), !g || !m)) return; const y = { rootMargin: -r(p) + "px " + -r(l.clientWidth - (h + g)) + "px " + -r(l.clientHeight - (p + m)) + "px " + -r(h) + "px", threshold: o(0, n(1, a)) || 1, }; let w = !0; function x(e) { const n = e[0].intersectionRatio; if (n !== a) { if (!w) return f(); n ? f(!1, n) : (i = setTimeout(() => { f(!1, 1e-7); }, 1e3)); } 1 !== n || q(d, t.getBoundingClientRect()) || f(), (w = !1); } try { c = new IntersectionObserver(x, { ...y, root: l.ownerDocument, }); } catch (t) { c = new IntersectionObserver(x, y); } c.observe(t); })(!0), s ); })(h, i) : null; let m, y = -1, w = null; f && ((w = new ResizeObserver((t) => { let [n] = t; n && n.target === h && w && (w.unobserve(e), cancelAnimationFrame(y), (y = requestAnimationFrame(() => { var t; null == (t = w) || t.observe(e); }))), i(); })), h && !d && w.observe(h), w.observe(e)); let x = d ? P(t) : null; return ( d && (function e() { const n = P(t); x && !q(x, n) && i(); (x = n), (m = requestAnimationFrame(e)); })(), i(), () => { var t; p.forEach((t) => { l && t.removeEventListener("scroll", i), s && t.removeEventListener("resize", i); }), null == g || g(), null == (t = w) || t.disconnect(), (w = null), d && cancelAnimationFrame(m); } ); }), (t.computePosition = (t, n, o) => { const i = new Map(), r = { platform: k, ...o }, c = { ...r.platform, _c: i }; return e.computePosition(t, n, { ...r, platform: c }); }), (t.detectOverflow = U), (t.flip = $), (t.getOverflowAncestors = C), (t.hide = G), (t.inline = K), (t.limitShift = Q), (t.offset = j), (t.platform = k), (t.shift = Y), (t.size = _); // We put this manually here because we need to make sure it's available // before the popover component is initialized. window.FloatingUIDOM = t; return t;});Component scripts are loaded through the shared script bundle, see JavaScript.
Update the import paths to match your project setup.
Usage
import "github.com/axadrn/shadcn-templ/v2/components/tooltip"@tooltip.Tooltip() { <button { tooltip.Trigger(ctx)... }>Hover</button> @tooltip.Content() { <p>Add to library</p> }}Composition
Use the following composition to build a Tooltip:
tooltip.Tooltip├── tooltip.Trigger└── tooltip.ContentSide
Use the Side prop to change the position of the tooltip.
Add to library
Add to library
Add to library
Add to library
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/tooltip") var tooltipSides = []tooltip.Side{tooltip.SideLeft, tooltip.SideTop, tooltip.SideBottom, tooltip.SideRight} templ TooltipSides() { <div class="flex flex-wrap gap-2"> for _, side := range tooltipSides { @tooltip.Tooltip() { @button.Button(button.Props{ Variant: button.VariantOutline, Class: "w-fit capitalize", Attributes: tooltip.Trigger(ctx), }) { { string(side) } } @tooltip.Content(tooltip.ContentProps{Side: side}) { <p>Add to library</p> } } } </div>}With Keyboard Shortcut
Save ChangesS
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/kbd" "github.com/axadrn/shadcn-templ/v2/components/tooltip") templ TooltipKeyboard() { @tooltip.Tooltip() { @button.Button(button.Props{ Variant: button.VariantOutline, Size: button.SizeIconSm, Attributes: tooltip.Trigger(ctx), }) { @icon.Save() } @tooltip.Content() { Save Changes @kbd.Kbd() { S } } }}Disabled Button
Show a tooltip on a disabled button by wrapping it with a span.
This feature is currently unavailable
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/tooltip") templ TooltipDisabled() { @tooltip.Tooltip() { <span class="inline-block w-fit" { tooltip.Trigger(ctx)... }> @button.Button(button.Props{Variant: button.VariantOutline, Disabled: true}) { Disabled } </span> @tooltip.Content() { This feature is currently unavailable } }}API Reference
Tooltip
The tooltip.Tooltip component is the root, it generates the id that links trigger and content.
TooltipTrigger
tooltip.Trigger(ctx) returns the attributes that turn any element into the tooltip trigger.
TooltipContent
The tooltip.Content component is the popup.
| Prop | Type | Default |
|---|---|---|
Side |
SideTop | SideRight | SideBottom | SideLeft |
SideTop |
SideOffset |
int |
4 |
Class |
string |
- |