diff --git a/.changeset/little-llamas-swim.md b/.changeset/little-llamas-swim.md
new file mode 100644
index 0000000000..c2a34bdd1f
--- /dev/null
+++ b/.changeset/little-llamas-swim.md
@@ -0,0 +1,6 @@
+---
+"@twilio-paste/core": patch
+"@twilio-paste/non-modal-dialog-primitive": patch
+---
+
+[Non-modal Dialog Primitive] Fix to always use a portal when rendering, even when using the state hook.
diff --git a/packages/paste-core/primitives/non-modal-dialog/__tests__/NonModalDialogPrimitive.tsx b/packages/paste-core/primitives/non-modal-dialog/__tests__/NonModalDialogPrimitive.tsx
new file mode 100644
index 0000000000..7ff1625a93
--- /dev/null
+++ b/packages/paste-core/primitives/non-modal-dialog/__tests__/NonModalDialogPrimitive.tsx
@@ -0,0 +1,49 @@
+import { act, fireEvent, render, screen } from "@testing-library/react";
+import * as React from "react";
+
+import {
+ NonModalDialogArrowPrimitive,
+ NonModalDialogDisclosurePrimitive,
+ NonModalDialogPrimitive,
+ type NonModalDialogPrimitivePopoverInitialState,
+ useNonModalDialogPrimitiveState,
+} from "../src";
+
+const Example: React.FC<{ options?: NonModalDialogPrimitivePopoverInitialState }> = ({
+ options = { placement: "bottom-end" },
+}) => {
+ const popover = useNonModalDialogPrimitiveState(options);
+ return (
+
+ Open Popover
+
+
+ Welcome to Reakit!
+
+
+ );
+};
+Example.displayName = "Example";
+
+describe("NonModalDialogPrimitive", () => {
+ describe("portal behavior", () => {
+ test("renders in a portal under when nothing is provided into hook", async () => {
+ render();
+
+ const wrapper = screen.getByTestId("wrapper");
+ const button = screen.getByText("Open Popover");
+ const popover = screen.getByText("Welcome to Reakit!");
+ expect(wrapper).toBeInTheDocument();
+ expect(popover).not.toBeVisible();
+
+ await act(async () => {
+ fireEvent.click(button);
+ });
+
+ expect(popover).toBeVisible();
+
+ // Check if popover is in portal
+ expect(wrapper).not.toContainElement(popover);
+ });
+ });
+});
diff --git a/packages/paste-core/primitives/non-modal-dialog/src/index.tsx b/packages/paste-core/primitives/non-modal-dialog/src/index.tsx
index 694cda4497..0c51b99d46 100644
--- a/packages/paste-core/primitives/non-modal-dialog/src/index.tsx
+++ b/packages/paste-core/primitives/non-modal-dialog/src/index.tsx
@@ -1,14 +1,28 @@
+import {
+ type PopoverInitialState as NonModalDialogPrimitivePopoverInitialState,
+ type PopoverStateReturn as NonModalDialogPrimitiveStateReturn,
+ usePopoverState as _usePopoverState,
+} from "@twilio-paste/reakit-library";
+
+/*
+ * Fixes issue where the popover would not open in a portal
+ * when using the state hook directly
+ */
+export const useNonModalDialogPrimitiveState = (
+ options: NonModalDialogPrimitivePopoverInitialState,
+): NonModalDialogPrimitiveStateReturn => {
+ return _usePopoverState({ modal: true, ...options });
+};
+
export {
- usePopoverState as useNonModalDialogPrimitiveState,
Popover as NonModalDialogPrimitive,
PopoverDisclosure as NonModalDialogDisclosurePrimitive,
PopoverArrow as NonModalDialogArrowPrimitive,
} from "@twilio-paste/reakit-library";
+export type { NonModalDialogPrimitivePopoverInitialState, NonModalDialogPrimitiveStateReturn };
export type {
PopoverState as NonModalDialogPrimitiveState,
- PopoverStateReturn as NonModalDialogPrimitiveStateReturn,
- PopoverInitialState as NonModalDialogPrimitivePopoverInitialState,
PopoverProps as NonModalDialogPrimitiveProps,
PopoverDisclosureProps as NonModalDialogDisclosurePrimitiveProps,
PopoverArrowProps as NonModalDialogArrowPrimitiveProps,