Lattice components

Popover

Anchored surface primitive that owns open state, popper positioning, layered dismissal, and presence motion while you own the visuals.

@lattice-ui/popover Stable direction import Popover depends on core , focus , layer , motion , popper

Popover is the primitive for a non-blocking surface that floats next to the element that opened it: hover cards, inline editors, detail panels, and small forms. It coordinates open state, positioning, dismissal, and exit motion so your component only has to render the floating frame and its contents.

Reach for Popover when a surface should anchor to a trigger (or a separate anchor), position itself with the popper foundation, and dismiss predictably — by an explicit close, an outside interaction, or a controlled state change. Unlike Dialog, Popover is non-modal by default, so the rest of the UI stays interactive while it is open.

Import

import { Popover } from "@lattice-ui/popover";

Anatomy

Compose the parts you need. Root, Portal, and Content form the minimum useful surface; Trigger, Anchor, and Close are optional depending on how you drive and position the popover.

Popover anatomy
<Popover.Root>
<Popover.Trigger />
<Popover.Anchor />
<Popover.Portal>
<Popover.Content>
<Popover.Close />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
PartRequiredResponsibility
Popover.RootyesOwns open state and shares it with every part through context.
Popover.TriggernoA button that toggles the popover and acts as the default positioning anchor.
Popover.AnchornoAn explicit anchor to position against when the trigger is not the right reference.
Popover.PortalyesRenders the surface into a ScreenGui outside the local tree.
Popover.ContentyesThe positioned, dismissable, motion-driven surface.
Popover.ClosenoA button that closes the popover from inside the content.

Example

A controlled popover anchored to its trigger, positioned to the right, with an app-owned frame inside the content.

ProfileCardPopover.tsx
import { useState } from "@rbxts/react";
import { Popover } from "@lattice-ui/popover";
export function ProfileCardPopover() {
const [open, setOpen] = useState(false);
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<textbutton Text="View profile" Size={UDim2.fromOffset(140, 38)} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content placement="right" sideOffset={8} collisionPadding={12}>
<frame
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
Size={UDim2.fromOffset(260, 140)}
>
<uilistlayout Padding={new UDim(0, 8)} />
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(240, 28)}
Text="Astra"
TextColor3={Color3.fromRGB(240, 244, 250)}
/>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(240, 24)}
Text="Level 42 builder"
TextColor3={Color3.fromRGB(176, 184, 198)}
/>
<Popover.Close asChild>
<textbutton Text="Close" Size={UDim2.fromOffset(100, 34)} />
</Popover.Close>
</frame>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}

How it behaves

Open state

Popover.Root is controllable. Pass open and onOpenChange to control it, or defaultOpen to run uncontrolled (defaults to closed). Popover.Trigger toggles the open state on activation and Popover.Close closes it; both go through the same state, so controlled and uncontrolled usage behave identically.

Positioning

Popover.Content is positioned by the popper foundation. It measures the anchor and the content, then resolves a final placement and flips to the opposite side when the requested side would collide with the screen edge. Tune it with placement ("top" | "bottom" | "left" | "right", default "bottom"), sideOffset (gap from the anchor, default 0), alignOffset (shift along the anchor’s cross axis, default 0), and collisionPadding (minimum distance from the screen edge, default 8).

By default the content anchors to the trigger. Add a Popover.Anchor to position against a different element — for example, anchoring a popover to a row while the trigger sits elsewhere. The anchor takes precedence over the trigger when both are present.

Focus and selection

Popover.Content mounts a focus scope tied to the open state. Because Popover is non-modal by default, the scope is not trapped — gamepad and GuiObject selection can move freely between the popover and the rest of the screen. Focus is restored to the previously selected object when the popover closes, and the trigger is the natural restore target. Setting modal on the Root switches the scope to trapped so selection stays inside the surface while it is open.

Dismissal

Popover.Content participates in dismissable-layer behavior. An outside press dismisses it, and when modal is true, interaction behind the surface is also blocked. Use onPointerDownOutside and onInteractOutside to observe or veto those interactions before the popover closes.

Motion and presence

Popover.Content runs a default popper-aware reveal/exit recipe that animates from the resolved placement, so it animates in and out without extra setup. The default surface uses a canvas-group recipe; with asChild it uses a transform recipe applied to your element. Override either with transition, and pass forceMount to keep the content mounted through its exit animation (useful when you drive motion yourself or need the node to persist).

API reference

Popover.Root

Prop Type Description
open boolean Controlled open state. Pair with onOpenChange.
defaultOpen boolean Initial open state for uncontrolled usage. Defaults to false.
onOpenChange (open: boolean) => void Called whenever the open state changes.
modal boolean When true, blocks interaction behind the popover and traps focus inside it. Defaults to false.
children React.ReactNode The popover parts.

Popover.Trigger

Prop Type Description
asChild boolean Merge behavior onto the single child element instead of rendering the default textbutton.
disabled boolean Prevents the trigger from toggling the popover and removes it from focus tracking.
children React.ReactElement The element to render. Required when asChild is set.

Popover.Anchor

Prop Type Description
asChild boolean Merge anchor tracking onto the single child element instead of rendering the default zero-size frame.
children React.ReactElement The element to anchor against. Required when asChild is set.

Popover.Portal

Prop Type Description
container BasePlayerGui Target PlayerGui to render the surface into. Defaults to the surrounding portal context's container.
displayOrderBase number Base DisplayOrder for the generated ScreenGui, used to order it against other layers. Defaults to the surrounding portal context's value.
children React.ReactNode The content part.

Popover.Content

Prop Type Description
placement "top" | "bottom" | "left" | "right" Requested side to position the content on. Flips on collision. Defaults to "bottom".
sideOffset number Gap in pixels between the anchor and the content. Defaults to 0.
alignOffset number Shift in pixels along the anchor's cross axis. Defaults to 0.
collisionPadding number Minimum distance in pixels to keep from the screen edge. Defaults to 8.
asChild boolean Position and animate the single child element instead of rendering the default canvasgroup wrapper.
forceMount boolean Keeps the content mounted while exit motion runs.
transition PresenceMotionConfig Overrides the default popper-aware reveal/exit recipe.
onPointerDownOutside (event: LayerInteractEvent) => void Called when a pointer press occurs outside the content, before dismissal.
onInteractOutside (event: LayerInteractEvent) => void Called for any other outside interaction, before dismissal.
children React.ReactNode The surface contents.

Popover.Close

Prop Type Description
asChild boolean Merge close behavior onto the single child element instead of rendering the default textbutton.
children React.ReactElement The element to render. Required when asChild is set.