Latticereference

Layer

Portals into ScreenGuis, dismissable outside-press layers, and presence mounting for overlays and anchored surfaces in Roblox.

@lattice-ui/react-layerStable directiondepends on runtime, focus

Layer provides the three behaviors every overlay surface needs in Roblox: rendering outside the local tree into a ScreenGui, dismissing on outside interaction while optionally blocking input behind the surface, and staying mounted through an exit animation. Dialog, Popover, Menu, Select, Tooltip, and Toast are all built on these primitives.

It composes with the other foundations directly — DismissableLayer renders through Portal and wraps its children in a FocusLayerProvider from the Focus package, 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;
Setting the portal target near the app root
<PortalProvider container={playerGui}>
<App />
</PortalProvider>

Establishes the portal context for everything below it: the BasePlayerGui that portalled surfaces render into, and the base DisplayOrder used to stack layers. displayOrderBase defaults to 1000. Mount this once near your app root so every overlay resolves a consistent target and stacking baseline. It is a strict context provider — components that read the context throw if no provider is above them.

PropTypeDescription
container *BasePlayerGuiThe PlayerGui (or comparable BasePlayerGui) that portalled surfaces render into.
displayOrderBasenumberBase DisplayOrder for generated ScreenGuis. Layers stack above it by mount order. Defaults to 1000.
childrenReact.ReactNodeThe subtree that can portal into this container.

usePortalContext

function usePortalContext(): {
container: BasePlayerGui;
displayOrderBase: number;
};

Reads the current portal context — the resolved container and displayOrderBase. Throws if called outside a PortalProvider. Portal and DismissableLayer both use it to find their render target and stacking base.

Portal

function Portal(props: {
children?: React.ReactNode;
container?: Instance;
}): React.Element;
Rendering content into the PlayerGui
<Portal>
<screengui>
<frame Size={UDim2.fromScale(1, 1)} />
</screengui>
</Portal>

Renders its children into a different part of the Roblox instance tree using ReactRoblox.createPortal, while keeping them in the React tree (context and state still flow through). By default it targets the container from the nearest PortalProvider; pass an explicit container to override the target for a single portal. Render your own screengui inside if you need a LayerCollectorPortal itself only relocates the subtree.

PropTypeDescription
childrenReact.ReactNodeContent to render at the target instead of in place.
containerInstanceOverride 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;
A dismissable surface
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, registers itself on the shared layer stack, and watches for pointer interactions outside its content boundary. A press outside the boundary fires onPointerDownOutside, any other outside interaction fires onInteractOutside, and unless an event is vetoed with preventDefault, onDismiss is called.

The content boundary defaults to the layer’s internal content wrapper; pass contentBoundaryRef to treat a specific GuiObject as the “inside” region, and insideRefs to mark additional instances (such as a separate trigger) as inside so pressing them does not dismiss. When modal or disableOutsidePointerEvents is set, the layer renders a full-screen modal blocker behind the content so input cannot reach anything underneath.

PropTypeDescription
childrenReact.ReactNodeThe layer contents, rendered inside the portalled ScreenGui.
enabledbooleanWhether the layer participates in dismissal and input blocking. Defaults to true.
contentBoundaryRefReact.MutableRefObject<GuiObject | undefined>GuiObject treated as the inside boundary. Defaults to the internal content wrapper.
insideRefsArray<React.MutableRefObject<GuiObject | undefined>>Extra instances treated as inside, so interacting with them does not dismiss.
modalbooleanBlocks input behind the surface and enables outside-press dismissal.
disableOutsidePointerEventsbooleanBlocks pointer input behind the surface without implying full modality.
onPointerDownOutside(event: LayerInteractEvent) => voidCalled on a pointer press outside the boundary, before dismissal.
onInteractOutside(event: LayerInteractEvent) => voidCalled on any other outside interaction, before dismissal.
onDismiss() => voidCalled 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 that triggered the interaction. Call preventDefault() to keep the layer from dismissing for that interaction; 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;
Keeping content mounted through its exit
<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 so an exit animation can play, then unmounts once the animation signals completion. It calls its render function (passed as children or render) with isPresent — drive your animation off this — and an onExitComplete callback to call when the exit finishes. As a safety net, if onExitComplete is never called, the content is force-unmounted after exitFallbackMs (default 140). onExitComplete on the props fires whenever the exit completes through either path.

PropTypeDescription
present *booleanWhether the content should be present. Flipping to false starts the exit.
exitFallbackMsnumberFallback timeout that force-unmounts if exit is never reported. Defaults to 140.
onExitComplete() => voidCalled once the exit completes, via report or fallback.
childrenPresenceRenderRender function receiving { isPresent, onExitComplete }.
renderPresenceRenderAlternative to children; same signature. Used when children is not provided.