Displays rich content in a portal, triggered by a button.
Dimensions
Set the dimensions for the layer.
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/input" "github.com/axadrn/shadcn-templ/v2/components/label" "github.com/axadrn/shadcn-templ/v2/components/popover") templ PopoverDemo() { @popover.Popover() { @button.Button(button.Props{ Variant: button.VariantOutline, Attributes: popover.Trigger(ctx), }) { Open popover } @popover.Content(popover.ContentProps{Class: "w-80"}) { <div class="grid gap-4"> <div class="space-y-2"> <h4 class="leading-none font-medium">Dimensions</h4> <p class="text-sm text-muted-foreground">Set the dimensions for the layer.</p> </div> <div class="grid gap-2"> <div class="grid grid-cols-3 items-center gap-4"> @label.Label(label.Props{For: "width"}) { Width } @input.Input(input.Props{ ID: "width", Value: "100%", Class: "col-span-2 h-8", }) </div> <div class="grid grid-cols-3 items-center gap-4"> @label.Label(label.Props{For: "maxWidth"}) { Max. width } @input.Input(input.Props{ ID: "maxWidth", Value: "300px", Class: "col-span-2 h-8", }) </div> <div class="grid grid-cols-3 items-center gap-4"> @label.Label(label.Props{For: "height"}) { Height } @input.Input(input.Props{ ID: "height", Value: "25px", Class: "col-span-2 h-8", }) </div> <div class="grid grid-cols-3 items-center gap-4"> @label.Label(label.Props{For: "maxHeight"}) { Max. height } @input.Input(input.Props{ ID: "maxHeight", Value: "none", Class: "col-span-2 h-8", }) </div> </div> </div> } }}Installation
shadcn-templ add popoverCopy and paste the following code into your project.
package popover 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 Align string const ( AlignStart Align = "start" AlignCenter Align = "center" AlignEnd Align = "end") type ctxKey string const idKey ctxKey = "popoverID" func popoverID(ctx context.Context) string { if id, ok := ctx.Value(idKey).(string); ok { return id } return ""} // Trigger returns the attributes that turn any element (usually a button)// into the popover trigger — the asChild equivalent: no wrapper element.func Trigger(ctx context.Context) templ.Attributes { return templ.Attributes{ "data-tui-popover-trigger": true, "aria-controls": popoverID(ctx), "aria-haspopup": "dialog", "aria-expanded": "false", }} type Props struct { ID string} type ContentProps struct { ID string Class string Attributes templ.Attributes // Side of the trigger to open against. Defaults to bottom. Side Side // Align along the trigger edge. Defaults to center. Align Align // SideOffset is the gap to the trigger, in px. Defaults to 4. SideOffset int // AlignOffset shifts the content along the aligned edge, in px. AlignOffset int} type HeaderProps struct { ID string Class string Attributes templ.Attributes} type TitleProps struct { ID string Class string Attributes templ.Attributes} type DescriptionProps struct { ID string Class string Attributes templ.Attributes} // Root renders no element: it only carries the id that links Trigger and// Content (via ctx).templ Popover(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 = SideBottom }} } if p.Align == "" { {{ p.Align = AlignCenter }} } {{ sideOffset := p.SideOffset if sideOffset == 0 { sideOffset = 4 } id := popoverID(ctx) if p.ID != "" { id = p.ID } }} // Inert until the script portals it to <body> at init, so the SSRd // content never participates in layout or sibling CSS. <template data-tui-popover-portal> <div id={ id } data-tui-popover-content data-tui-popover-side={ string(p.Side) } data-tui-popover-align={ string(p.Align) } data-tui-popover-side-offset={ strconv.Itoa(sideOffset) } if p.AlignOffset != 0 { data-tui-popover-align-offset={ strconv.Itoa(p.AlignOffset) } } data-state="closed" popover="manual" class="pointer-events-none isolate fixed inset-auto z-50 m-0 overflow-visible border-0 bg-transparent p-0" > <div data-slot="popover-content" data-tui-popover-popup data-state="closed" role="dialog" tabindex="-1" class={ utils.CN( // 1:1 base/ui/popover.tsx PopoverContent, the look comes from // cn-popover-content (animations key on our data-state attribute). // origin-(--transform-origin) becomes our JS variable name. "cn-popover-content cn-popover-content-logical z-50 w-72 origin-(--tui-popover-transform-origin) outline-hidden", // JS wiring: the positioner wrapper is pointer-events-none, and the // popup stays mounted after animate-out until hidePopover runs. "pointer-events-auto data-closed:fill-mode-forwards", p.Class, ), } { p.Attributes... } > { children... } </div> </div> </template>} templ Header(props ...HeaderProps) { {{ var p HeaderProps }} if len(props) > 0 { {{ p = props[0] }} } <div if p.ID != "" { id={ p.ID } } data-slot="popover-header" class={ utils.CN("cn-popover-header", p.Class) } { p.Attributes... } > { children... } </div>} templ Title(props ...TitleProps) { {{ var p TitleProps }} if len(props) > 0 { {{ p = props[0] }} } <h2 if p.ID != "" { id={ p.ID } } data-slot="popover-title" class={ utils.CN("cn-popover-title", p.Class) } { p.Attributes... } > { children... } </h2>} templ Description(props ...DescriptionProps) { {{ var p DescriptionProps }} if len(props) > 0 { {{ p = props[0] }} } <p if p.ID != "" { id={ p.ID } } data-slot="popover-description" class={ utils.CN("cn-popover-description", p.Class) } { p.Attributes... } > { children... } </p>}// Uses window.FloatingUIDOM from components/floatingui (loaded in the same bundle).(function () { // Constants from Base UI's popover, shadcn's reference implementation. const EXIT_MS = 120; // exit animation (duration-100) + slack const COLLISION_PADDING = 5; function allContents() { return document.querySelectorAll("[data-tui-popover-content]"); } function triggerFor(content) { return document.querySelector( '[data-tui-popover-trigger][aria-controls="' + content.id + '"]', ); } function contentFor(trigger) { return document.getElementById(trigger.getAttribute("aria-controls")); } function popupFor(content) { return content.querySelector("[data-tui-popover-popup]"); } function setState(content, state) { content.setAttribute("data-state", state); const popup = popupFor(content); if (popup) popup.setAttribute("data-state", state); } function setSide(content, side) { content.setAttribute("data-side", side); const popup = popupFor(content); if (popup) popup.setAttribute("data-side", side); } // Base UI zooms the popup out of the anchor's center point, 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; } // 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-popover-content]").forEach((c) => { if (c !== content && !triggerFor(c)) c.remove(); }); if (content.parentElement !== document.body) { document.body.appendChild(content); } wireAria(content); } // Base UI links Title/Description to the popup via aria-labelledby and // aria-describedby with generated ids. function wireAria(content) { const popup = popupFor(content); if (!popup) return; const title = popup.querySelector("[data-slot=popover-title]"); if (title) { if (!title.id) title.id = content.id + "-title"; popup.setAttribute("aria-labelledby", title.id); } const description = popup.querySelector("[data-slot=popover-description]"); if (description) { if (!description.id) description.id = content.id + "-description"; popup.setAttribute("aria-describedby", description.id); } } function position(content) { const trigger = triggerFor(content); if (!trigger) return Promise.resolve(); const { computePosition, offset, flip, shift } = window.FloatingUIDOM; const side = content.getAttribute("data-tui-popover-side") || "bottom"; const align = content.getAttribute("data-tui-popover-align") || "center"; const sideOffset = parseFloat(content.getAttribute("data-tui-popover-side-offset")) || 0; const alignOffset = parseFloat(content.getAttribute("data-tui-popover-align-offset")) || 0; const placement = align === "center" ? side : side + "-" + align; return computePosition(trigger, content, { placement: placement, strategy: "fixed", middleware: [ offset({ mainAxis: sideOffset, crossAxis: alignOffset }), flip({ padding: COLLISION_PADDING }), shift({ padding: COLLISION_PADDING }), ], }).then((result) => { content.style.left = result.x + "px"; content.style.top = result.y + "px"; setSide(content, result.placement.split("-")[0]); const popup = popupFor(content); if (popup) { popup.style.setProperty( "--tui-popover-transform-origin", anchorOrigin(result, trigger.getBoundingClientRect(), sideOffset), ); } }); } function isOpen(content) { return content.getAttribute("data-state") === "open"; } function open(content) { if (typeof content === "string") content = document.getElementById(content); if (!content || isOpen(content)) return; allContents().forEach((c) => { if (c !== content) close(c); }); 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"; const finish = () => { content.style.visibility = ""; if (!content.matches(":popover-open")) return; setState(content, "open"); const trigger = triggerFor(content); if (trigger) trigger.setAttribute("aria-expanded", "true"); // Base UI moves focus into the popup when it opens. const popup = popupFor(content); if (popup && !content.contains(document.activeElement)) { popup.focus({ preventScroll: true }); } }; position(content).then(finish, finish); } // returnFocus false skips the focus restore, like Base UI on pointer // dismiss: focus follows the outside press instead of the trigger. function close(content, returnFocus) { if (typeof content === "string") content = document.getElementById(content); if (!content || !content.matches(":popover-open")) return; if (returnFocus !== false && content.contains(document.activeElement)) { const focusTrigger = triggerFor(content); if (focusTrigger) focusTrigger.focus({ preventScroll: true }); } content.style.visibility = ""; setState(content, "closed"); const trigger = triggerFor(content); if (trigger) trigger.setAttribute("aria-expanded", "false"); clearTimeout(content._tuiHide); content._tuiHide = setTimeout(() => { if (content.getAttribute("data-state") === "closed" && content.matches(":popover-open")) { content.hidePopover(); } }, EXIT_MS); } function closeAll(returnFocus) { allContents().forEach((content) => close(content, returnFocus)); } function closeNearest(element) { if (!element) return; const content = element.closest?.("[data-tui-popover-content]") || (element.closest?.("[data-tui-popover-trigger]") && contentFor(element.closest("[data-tui-popover-trigger]"))) || element.querySelector?.("[data-tui-popover-content]"); if (content) close(content); } function toggle(content) { if (typeof content === "string") content = document.getElementById(content); if (!content) return; if (isOpen(content)) { close(content); } else { open(content); } } // Pointer interactions toggle and dismiss on PRESS, exactly like Base UI. // Click is never used for open/close, so the stray click the browser fires // on body when the popup ends up under the released pointer is harmless. document.addEventListener("pointerdown", (e) => { if (e.button !== 0 || !(e.target instanceof Element)) return; const trigger = e.target.closest("[data-tui-popover-trigger]"); if (trigger) { if (trigger.disabled) return; const content = contentFor(trigger); if (content) toggle(content); return; } if (!e.target.closest("[data-tui-popover-content]")) closeAll(false); }); document.addEventListener("click", (e) => { if (!(e.target instanceof Element)) return; const trigger = e.target.closest("[data-tui-popover-trigger]"); if (trigger) { // Keyboard activation only (Enter/Space fire a detail-0 click without // a preceding pointerdown); pointer presses are handled on pointerdown. if (e.detail === 0 && !trigger.disabled) { const content = contentFor(trigger); if (content) toggle(content); } } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeAll(); }); // Page scroll or resize: keep open popovers attached to their trigger. window.addEventListener( "scroll", (e) => { if (e.target instanceof Element && e.target.closest("[data-tui-popover-content]")) return; allContents().forEach((content) => { if (isOpen(content)) position(content); }); }, true, ); window.addEventListener("resize", () => { allContents().forEach((content) => { if (isOpen(content)) position(content); }); }); // Portal all contents up front (React portals on mount too): popovers must // not sit inside layout groups where hidden siblings break :last-child // rules. Runs on load and whenever new popovers appear in the DOM. // Lift SSR'd contents out of their inert <template> wrappers into <body>, // replacing a stale portaled copy on re-swaps (e.g. htmx). function liftTemplates() { document.querySelectorAll("template[data-tui-popover-portal]").forEach((tpl) => { const content = tpl.content.querySelector("[data-tui-popover-content]"); if (content) { const stale = document.getElementById(content.id); if (stale) stale.remove(); document.body.appendChild(content); } tpl.remove(); }); } function portalAll() { liftTemplates(); allContents().forEach((content) => { if (triggerFor(content)) portal(content); }); } let portalQueued = false; function queuePortal() { if (portalQueued) return; portalQueued = true; requestAnimationFrame(() => { portalQueued = false; portalAll(); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", portalAll); } else { portalAll(); } new MutationObserver((mutations) => { for (const m of mutations) { if (m.addedNodes.length) { queuePortal(); break; } } }).observe(document.documentElement, { childList: true, subtree: true }); window.tui = window.tui || {}; window.tui.popover = { open, close, closeAll, closeNearest, toggle, isOpen: (c) => { if (typeof c === "string") c = document.getElementById(c); return !!c && isOpen(c); }, };})();// 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/popover"@popover.Popover() { @button.Button(button.Props{ Variant: button.VariantOutline, Attributes: popover.Trigger(ctx), }) { Open Popover } @popover.Content() { @popover.Header() { @popover.Title() { Title } @popover.Description() { Description text here. } } }}Composition
Use the following composition to build a Popover:
popover.Popover├── popover.Trigger└── popover.ContentBasic
A simple popover with a header, title, and description.
Dimensions
Set the dimensions for the layer.
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/popover") templ PopoverBasic() { @popover.Popover() { @button.Button(button.Props{ Variant: button.VariantOutline, Attributes: popover.Trigger(ctx), }) { Open Popover } @popover.Content(popover.ContentProps{Side: popover.SideBottom, Align: popover.AlignStart}) { @popover.Header() { @popover.Title() { Dimensions } @popover.Description() { Set the dimensions for the layer. } } } }}Align
Use the Align prop on popover.Content to control the horizontal alignment.
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/popover") templ PopoverAlignments() { <div class="flex gap-6"> @popover.Popover() { @button.Button(button.Props{ Variant: button.VariantOutline, Size: button.SizeSm, Attributes: popover.Trigger(ctx), }) { Start } @popover.Content(popover.ContentProps{ Side: popover.SideBottom, Align: popover.AlignStart, Class: "w-40", }) { Aligned to start } } @popover.Popover() { @button.Button(button.Props{ Variant: button.VariantOutline, Size: button.SizeSm, Attributes: popover.Trigger(ctx), }) { Center } @popover.Content(popover.ContentProps{ Side: popover.SideBottom, Class: "w-40", }) { Aligned to center } } @popover.Popover() { @button.Button(button.Props{ Variant: button.VariantOutline, Size: button.SizeSm, Attributes: popover.Trigger(ctx), }) { End } @popover.Content(popover.ContentProps{ Side: popover.SideBottom, Align: popover.AlignEnd, Class: "w-40", }) { Aligned to end } } </div>}With Form
A popover with form fields inside.
Dimensions
Set the dimensions for the layer.
package examples import (package examples import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/input" "github.com/axadrn/shadcn-templ/v2/components/popover") templ PopoverForm() { @popover.Popover() { @button.Button(button.Props{ Variant: button.VariantOutline, Attributes: popover.Trigger(ctx), }) { Open Popover } @popover.Content(popover.ContentProps{ Side: popover.SideBottom, Align: popover.AlignStart, Class: "w-64", }) { @popover.Header() { @popover.Title() { Dimensions } @popover.Description() { Set the dimensions for the layer. } } @field.Group(field.GroupProps{Class: "gap-4"}) { @field.Field(field.Props{Orientation: field.OrientationHorizontal}) { @field.Label(field.LabelProps{ For: "popover-form-width", Class: "w-1/2", }) { Width } @input.Input(input.Props{ ID: "popover-form-width", Value: "100%", }) } @field.Field(field.Props{Orientation: field.OrientationHorizontal}) { @field.Label(field.LabelProps{ For: "popover-form-height", Class: "w-1/2", }) { Height } @input.Input(input.Props{ ID: "popover-form-height", Value: "25px", }) } } } }}API Reference
Popover
The popover.Root component renders no element, it carries the id that links trigger and content.
| Prop | Type | Default |
|---|---|---|
ID |
string |
- |
PopoverTrigger
popover.Trigger(ctx) returns the attributes that turn any element into the popover trigger, popover.TriggerFor(id) targets a popover outside the current root.
PopoverContent
The popover.Content component is the floating panel.
| Prop | Type | Default |
|---|---|---|
Side |
SideTop | SideRight | SideBottom | SideLeft |
SideBottom |
Align |
AlignStart | AlignCenter | AlignEnd |
AlignCenter |
SideOffset |
int |
4 |
AlignOffset |
int |
0 |
Class |
string |
- |
PopoverHeader
The popover.Header component wraps the title and description.
| Prop | Type | Default |
|---|---|---|
Class |
string |
- |
PopoverTitle
The popover.Title component renders the accessible popover title.
| Prop | Type | Default |
|---|---|---|
Class |
string |
- |
PopoverDescription
The popover.Description component renders the accessible popover description.
| Prop | Type | Default |
|---|---|---|
Class |
string |
- |