Lattice reference

Layer

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

@lattice-ui/layer Stable direction depends on core , 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/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.

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 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.

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;
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.

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 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.

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.