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.
gx add popoverThe 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.
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>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>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>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>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>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.")},
}package popover
import "github.com/alternayte/gx"
var PopoverDescriptionFixtures = gx.Fixtures[PopoverDescriptionProps]{"Default": {Children: gx.Text("Set the dimensions for the layer.")}}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.")}),
)},
}package popover
import "github.com/alternayte/gx"
var PopoverTitleFixtures = gx.Fixtures[PopoverTitleProps]{"Default": {Children: gx.Text("Dimensions")}}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")},
}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. |