Skip to content
Gx
GitHub

Popover

A small panel anchored to a trigger.

<popover.Popover id="demo-popover">
  <popover.PopoverHeader>
    <popover.PopoverTitle>Dimensions</popover.PopoverTitle>
    <popover.PopoverDescription>Set the dimensions for the layer.</popover.PopoverDescription>
  </popover.PopoverHeader>
</popover.Popover>
<popover.PopoverTrigger id="demo-popover">Open popover</popover.PopoverTrigger>

Installation

Run the command in the app module.

Terminal
gx add popover

The command also installs button.

The command writes these files.

File Path in the app
Popover.gx ui/popover/Popover.gx
PopoverDescription.gx ui/popover/PopoverDescription.gx
PopoverHeader.gx ui/popover/PopoverHeader.gx
PopoverTitle.gx ui/popover/PopoverTitle.gx
PopoverTrigger.gx ui/popover/PopoverTrigger.gx
Popover.fixtures.go ui/popover/Popover.fixtures.go
PopoverDescription.fixtures.go ui/popover/PopoverDescription.fixtures.go
PopoverHeader.fixtures.go ui/popover/PopoverHeader.fixtures.go
PopoverTitle.fixtures.go ui/popover/PopoverTitle.fixtures.go
PopoverTrigger.fixtures.go ui/popover/PopoverTrigger.fixtures.go
styles.go ui/popover/styles.go

Install button first.

Copy each file to its path in the app. Change each import of a registry package to the path of that package in the app.

ui/popover/Popover.gx
package popover

props {
  // Id is the id of the root element. The trigger opens the popover by this id.
  Id       string
  // Align sets the edge of the trigger that the popover lines up with: Center, Start or End.
  Align    Align = Center
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the content of the popover.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<div id={p.Id} popover="auto" data-gx-dismiss data-gx-place={p.place()} style={p.style()} class={gx.Cx("z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden", motionClass, alignClass[p.align()], p.Class)} {...p.Attrs}>{p.Children}</div>
ui/popover/PopoverDescription.gx
package popover

props {
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the text of the description.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<p class={gx.Cx("text-muted-foreground", p.Class)} {...p.Attrs}>{p.Children}</p>
ui/popover/PopoverHeader.gx
package popover

props {
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the content of the header, usually a title and a description.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<div class={gx.Cx("flex flex-col gap-1 text-sm", p.Class)} {...p.Attrs}>{p.Children}</div>
ui/popover/PopoverTitle.gx
package popover

props {
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the text of the title.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<div class={gx.Cx("font-medium", p.Class)} {...p.Attrs}>{p.Children}</div>
ui/popover/PopoverTrigger.gx
package popover

import "github.com/alternayte/gx/registry/button"

props {
  // Id is the id of the popover that the trigger opens.
  Id       string
  // Variant sets the visual style of the button: button.Default, button.Secondary,
  // button.Destructive, button.Outline, button.Ghost or button.Link.
  Variant  button.Variant = button.Outline
  // Size sets the height and padding of the button: button.Xs, button.Sm, button.Md, button.Lg,
  // or an Icon size for a square button.
  Size     button.Size = button.Md
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the button label.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<button.Button variant={p.variant()} size={p.Size} class={p.Class} attrs={p.attrs()}>{p.Children}</button.Button>
ui/popover/Popover.fixtures.go
package popover

import "github.com/alternayte/gx"

var PopoverFixtures = gx.Fixtures[PopoverProps]{
	"Content": {Id: "demo-popover", Children: PopoverHeader(PopoverHeaderProps{Children: gx.Frag(
		PopoverTitle(PopoverTitleProps{Children: gx.Text("Dimensions")}),
		PopoverDescription(PopoverDescriptionProps{Children: gx.Text("Set the dimensions for the layer.")}),
	)})},
	"Start": {Id: "demo-popover-start", Align: Start, Children: gx.Text("Place content for the popover here.")},
	"End":   {Id: "demo-popover-end", Align: End, Children: gx.Text("Place content for the popover here.")},
}
ui/popover/PopoverDescription.fixtures.go
package popover

import "github.com/alternayte/gx"

var PopoverDescriptionFixtures = gx.Fixtures[PopoverDescriptionProps]{"Default": {Children: gx.Text("Set the dimensions for the layer.")}}
ui/popover/PopoverHeader.fixtures.go
package popover

import "github.com/alternayte/gx"

var PopoverHeaderFixtures = gx.Fixtures[PopoverHeaderProps]{
	"Default": {Children: gx.Frag(
		PopoverTitle(PopoverTitleProps{Children: gx.Text("Dimensions")}),
		PopoverDescription(PopoverDescriptionProps{Children: gx.Text("Set the dimensions for the layer.")}),
	)},
}
ui/popover/PopoverTitle.fixtures.go
package popover

import "github.com/alternayte/gx"

var PopoverTitleFixtures = gx.Fixtures[PopoverTitleProps]{"Default": {Children: gx.Text("Dimensions")}}
ui/popover/PopoverTrigger.fixtures.go
package popover

import (
	"github.com/alternayte/gx"
	"github.com/alternayte/gx/registry/button"
)

var PopoverTriggerFixtures = gx.Fixtures[PopoverTriggerProps]{
	"Default": {Id: "demo-popover", Children: gx.Text("Open popover")},
	"Start":   {Id: "demo-popover-start", Children: gx.Text("Open at the start")},
	"End":     {Id: "demo-popover-end", Variant: button.Secondary, Class: "ml-64", Children: gx.Text("Open at the end")},
}
ui/popover/styles.go
package popover

import (
	"github.com/alternayte/gx"
	"github.com/alternayte/gx/registry/button"
)

// Align is the edge of the trigger that the popover lines up with.
type Align string

// The alignments of popover.Popover.
const (
	Center Align = "center"
	Start  Align = "start"
	End    Align = "end"
)

// The popover zooms from the edge that touches the trigger. The overlay
// module writes data-side: a popover that flips opens above the trigger.
var alignClass = gx.Enum[Align]{
	Center: "origin-top data-[side=top]:origin-bottom",
	Start:  "origin-top-left data-[side=top]:origin-bottom-left",
	End:    "origin-top-right data-[side=top]:origin-bottom-right",
}

var alignStyle = gx.Enum[Align]{
	Center: "justify-self: anchor-center",
	Start:  "left: anchor(left)",
	End:    "right: anchor(right)",
}

// align returns the alignment of one popover; a zero value is Center.
func (p PopoverProps) align() Align {
	if p.Align == "" {
		return Center
	}
	return p.Align
}

// style anchors the popover below its trigger for a page whose scripts did
// not run. The overlay module then writes the measured place.
func (p PopoverProps) style() gx.Style {
	return gx.Style("position-anchor: --gx-pop-" + p.Id + "; inset: auto; margin: 0.25rem 0 0; top: anchor(bottom); " + alignStyle[p.align()])
}

// place tells the overlay module where the popover goes: below the trigger,
// 4px away, and inside the viewport.
func (p PopoverProps) place() string {
	return "bottom " + string(p.align()) + " 4"
}

// variant returns the button variant of one trigger; a zero value is
// button.Outline.
func (p PopoverTriggerProps) variant() button.Variant {
	if p.Variant == "" {
		return button.Outline
	}
	return p.Variant
}

// attrs wires the trigger to its popover and names it as the anchor.
func (p PopoverTriggerProps) attrs() gx.Attrs {
	return gx.JoinAttrs(gx.Attrs{
		{Key: "popovertarget", Value: p.Id},
		{Key: "popovertargetaction", Value: "toggle"},
		{Key: "style", Value: "anchor-name: --gx-pop-" + p.Id, Kind: gx.AttrStyle},
	}, p.Attrs)
}

// motionClass fades and zooms the popover from 95% and slides it 2 units from
// the trigger, from below when the popover took the top side. Safari 26.0 never ends a display transition on an element
// that CSS anchor positioning places, which leaves a closed popover rendered.
// The @supports test matches every engine but WebKit, so Safari closes the
// popover at once and still animates the enter.
const motionClass = "opacity-0 scale-95 transition-[opacity,scale,translate,overlay,display] not-supports-[font:-apple-system-body]:transition-discrete duration-150 open:opacity-100 open:scale-100 starting:open:opacity-0 starting:open:scale-95 starting:open:-translate-y-2 data-[side=top]:starting:open:translate-y-2 motion-reduce:transition-none"

The theme must define these tokens: --border, --popover, --popover-foreground, --muted-foreground.

Usage

<popover.PopoverTrigger id="dimensions">Open</popover.PopoverTrigger>
<popover.Popover id="dimensions">
  <popover.PopoverHeader>
    <popover.PopoverTitle>Dimensions</popover.PopoverTitle>
    <popover.PopoverDescription>Set the dimensions for the layer.</popover.PopoverDescription>
  </popover.PopoverHeader>
</popover.Popover>

The element uses the native Popover API. The trigger is a button.Button; Variant and Size select its style, and the default is button.Outline.

Align lines the popover up with the trigger: popover.Center (default), popover.Start or popover.End. The popover opens below the trigger. It flips above the trigger when the space below is too small, and it shifts along the trigger to stay 8px inside the viewport. data-side on the popover names the side it took.

The popover fades and zooms in and out. Safari shows the enter transition only. A user who asks for reduced motion gets no transition.

Examples

Popover: Content

<popover.Popover id="demo-popover">
  <popover.PopoverHeader>
    <popover.PopoverTitle>Dimensions</popover.PopoverTitle>
    <popover.PopoverDescription>Set the dimensions for the layer.</popover.PopoverDescription>
  </popover.PopoverHeader>
</popover.Popover>
<popover.PopoverTrigger id="demo-popover">Open popover</popover.PopoverTrigger>

Popover: Start

<popover.Popover id="demo-popover-start" align={popover.Start}>Place content for the popover here.</popover.Popover>
<popover.PopoverTrigger id="demo-popover-start">Open at the start</popover.PopoverTrigger>

Popover: End

<popover.Popover id="demo-popover-end" align={popover.End}>Place content for the popover here.</popover.Popover>
<popover.PopoverTrigger id="demo-popover-end" variant={button.Secondary} class="ml-64">
  Open at the end
</popover.PopoverTrigger>

PopoverDescription: Default

<popover.PopoverDescription>Set the dimensions for the layer.</popover.PopoverDescription>

PopoverHeader: Default

<popover.PopoverHeader>
  <popover.PopoverTitle>Dimensions</popover.PopoverTitle>
  <popover.PopoverDescription>Set the dimensions for the layer.</popover.PopoverDescription>
</popover.PopoverHeader>

PopoverTitle: Default

<popover.PopoverTitle>Dimensions</popover.PopoverTitle>

API reference

A tag sets a prop by its name with a lower-case first letter: Class is class.

popover.Popover

Prop Type Default Description
Id string Required Id is the id of the root element. The trigger opens the popover by this id.
Align Align Center Align sets the edge of the trigger that the popover lines up with: Center, Start or End.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the content of the popover.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

popover.PopoverDescription

Prop Type Default Description
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the text of the description.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

popover.PopoverHeader

Prop Type Default Description
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the content of the header, usually a title and a description.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

popover.PopoverTitle

Prop Type Default Description
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the text of the title.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

popover.PopoverTrigger

Prop Type Default Description
Id string Required Id is the id of the popover that the trigger opens.
Variant button.Variant button.Outline Variant sets the visual style of the button: button.Default, button.Secondary, button.Destructive, button.Outline, button.Ghost or button.Link.
Size button.Size button.Md Size sets the height and padding of the button: button.Xs, button.Sm, button.Md, button.Lg, or an Icon size for a square button.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the button label.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

Do and do not

Do

  • Give the popover and the trigger the same Id.
  • Put the popover directly after its trigger in the markup.
  • Keep the content short.

Don't

  • Do not put a form with many fields in a popover.
  • Do not open a popover from another popover.

Keyboard

Key Action
Enter, Space Toggles the popover.
Tab Moves focus into the popover.
Escape Closes the popover.