@lattice-ui/react-layerStable directiondepends on runtime, focusLayer provides the three behaviours every overlay surface needs. It renders outside the local
tree into a ScreenGui, dismisses on outside interaction while optionally blocking input
behind the surface, and stays mounted through an exit animation. Dialog, Popover, Menu, Select,
Tooltip and Toast are all built on it.
DismissableLayer renders through Portal and wraps its children in a FocusLayerProvider, so
stacked layers trap and order correctly.
Import
import { Portal, PortalProvider, usePortalContext, DismissableLayer, Presence,} from "@lattice-ui/react-layer";API reference
PortalProvider
function PortalProvider(props: { container: BasePlayerGui; displayOrderBase?: number; children?: React.ReactNode;}): React.Element;<PortalProvider container={playerGui}> <App /></PortalProvider>Establishes the portal context: the BasePlayerGui that portalled surfaces render into, and the
base DisplayOrder used to stack layers (displayOrderBase, default 1000). Mount it once near
your app root. It is a strict context provider — readers throw if no provider is above them.
| Prop | Type | Description |
|---|---|---|
| container * | BasePlayerGui | The PlayerGui (or comparable BasePlayerGui) that portalled surfaces render into. |
| displayOrderBase | number | Base DisplayOrder for generated ScreenGuis. Layers stack above it by mount order. Defaults to 1000. |
| children | React.ReactNode | The subtree that can portal into this container. |
usePortalContext
function usePortalContext(): { container: BasePlayerGui; displayOrderBase: number;};Reads the resolved container and displayOrderBase, throwing outside a PortalProvider. Portal
and DismissableLayer both use it.
Portal
function Portal(props: { children?: React.ReactNode; container?: Instance;}): React.Element;<Portal> <screengui> <frame Size={UDim2.fromScale(1, 1)} /> </screengui></Portal>Renders its children elsewhere in the Roblox instance tree through ReactRoblox.createPortal, while
keeping them in the React tree so context and state still flow. It targets the nearest
PortalProvider’s container unless you pass one. Render your own screengui inside if you need a
LayerCollector — Portal only relocates the subtree.
| Prop | Type | Description |
|---|---|---|
| children | React.ReactNode | Content to render at the target instead of in place. |
| container | Instance | Override the render target. Defaults to the PortalProvider container. |
DismissableLayer
function DismissableLayer(props: { children?: React.ReactNode; enabled?: boolean; contentBoundaryRef?: React.MutableRefObject<GuiObject | undefined>; insideRefs?: Array<React.MutableRefObject<GuiObject | undefined>>; modal?: boolean; disableOutsidePointerEvents?: boolean; onPointerDownOutside?: (event: LayerInteractEvent) => void; onInteractOutside?: (event: LayerInteractEvent) => void; onDismiss?: () => void;}): React.Element;const contentRef = React.useRef<Frame>();
<DismissableLayer modal contentBoundaryRef={contentRef} onDismiss={() => setOpen(false)}> <frame ref={contentRef} Size={UDim2.fromOffset(280, 160)} /></DismissableLayer>;The core overlay primitive. It portals a ScreenGui whose DisplayOrder is the provider base plus
this layer’s mount order. It registers on the shared layer stack and watches for pointer
interactions outside its content boundary. A press outside fires onPointerDownOutside, any other
outside interaction fires onInteractOutside, and unless vetoed with preventDefault, onDismiss
follows.
The boundary defaults to the layer’s internal content wrapper. contentBoundaryRef names a specific
GuiObject as “inside”, and insideRefs marks additional instances such as a separate trigger.
With modal or disableOutsidePointerEvents, the layer renders a full-screen blocker behind the
content.
| Prop | Type | Description |
|---|---|---|
| children | React.ReactNode | The layer contents, rendered inside the portalled ScreenGui. |
| enabled | boolean | Whether the layer participates in dismissal and input blocking. Defaults to true. |
| contentBoundaryRef | React.MutableRefObject<GuiObject | undefined> | GuiObject treated as the inside boundary. Defaults to the internal content wrapper. |
| insideRefs | Array<React.MutableRefObject<GuiObject | undefined>> | Extra instances treated as inside, so interacting with them does not dismiss. |
| modal | boolean | Blocks input behind the surface and enables outside-press dismissal. |
| disableOutsidePointerEvents | boolean | Blocks pointer input behind the surface without implying full modality. |
| onPointerDownOutside | (event: LayerInteractEvent) => void | Called on a pointer press outside the boundary, before dismissal. |
| onInteractOutside | (event: LayerInteractEvent) => void | Called on any other outside interaction, before dismissal. |
| onDismiss | () => void | Called to request dismissal when an outside interaction is not vetoed. |
LayerInteractEvent
type LayerInteractEvent = { originalEvent: InputObject; defaultPrevented: boolean; preventDefault: () => void;};The event handed to onPointerDownOutside and onInteractOutside. originalEvent is the Roblox
InputObject. preventDefault() keeps the layer from dismissing, and defaultPrevented reflects
whether it has been vetoed.
Presence
function Presence(props: { present: boolean; exitFallbackMs?: number; onExitComplete?: () => void; children?: PresenceRender; render?: PresenceRender;}): React.Element | undefined;
type PresenceRender = (state: { isPresent: boolean; onExitComplete: () => void;}) => React.ReactElement | undefined;<Presence present={open}> {({ isPresent, onExitComplete }) => ( <AnimatedSurface visible={isPresent} onHidden={onExitComplete} /> )}</Presence>A mount controller for exit animations: Presence keeps its content mounted after present flips
to false, then unmounts once the animation signals completion. It calls its render function with
isPresent, which you drive your animation off, and an onExitComplete to call when the exit
finishes. If that is never called, the content is force-unmounted after exitFallbackMs (default
140).
| Prop | Type | Description |
|---|---|---|
| present * | boolean | Whether the content should be present. Flipping to false starts the exit. |
| exitFallbackMs | number | Fallback timeout that force-unmounts if exit is never reported. Defaults to 140. |
| onExitComplete | () => void | Called once the exit completes, via report or fallback. |
| children | PresenceRender | Render function receiving { isPresent, onExitComplete }. |
| render | PresenceRender | Alternative to children; same signature. Used when children is not provided. |
DismissableLayer portals into a generated ScreenGui parented to the provider’s BasePlayerGui, not into your component tree. Its DisplayOrder is displayOrderBase + mountOrder, so later layers stack on top. Set displayOrderBase to order Lattice surfaces relative to your own ScreenGuis.
With modal or disableOutsidePointerEvents, the blocker is a full-screen transparent textbutton with Modal = true behind the content. That is what stops clicks and gameplay input reaching instances underneath.