diff --git a/apps/web/content/docs/api/shadcn.mdx b/apps/web/content/docs/api/shadcn.mdx
index 367a3a8f..db47b40b 100644
--- a/apps/web/content/docs/api/shadcn.mdx
+++ b/apps/web/content/docs/api/shadcn.mdx
@@ -25,7 +25,7 @@ Your app must have Tailwind CSS configured.
@json-render/shadcn |
-shadcnComponents |
+shadcnComponents, PortalContainerProvider |
React implementations |
@@ -73,6 +73,21 @@ const { registry } = defineRegistry(catalog, {
State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled by `ActionProvider` automatically. You don't need to declare them in your catalog.
+## Portal containers and scoped themes
+
+Dialog, DropdownMenu, Popover, Select and Tooltip normally portal to `document.body`. When the renderer lives inside a scoped theme, supply a stable host element with that theme through `PortalContainerProvider`:
+
+```tsx
+import { PortalContainerProvider } from "@json-render/shadcn";
+import { Renderer } from "@json-render/react";
+
+
+
+
+```
+
+The host owns the element and its lifetime. Place it outside clipped or transformed ancestors when overlays must cover the viewport. Nested providers select their nearest container. Without a provider, or with `container={undefined}`, the standard Radix body portal remains unchanged. The provider does not change component schemas, state, actions or overlay interactions.
+
## Extending with Custom Components
Add custom components alongside standard ones:
diff --git a/packages/shadcn/README.md b/packages/shadcn/README.md
index 8938aac8..209cd2d7 100644
--- a/packages/shadcn/README.md
+++ b/packages/shadcn/README.md
@@ -63,6 +63,21 @@ function App({ spec }) {
}
```
+## Portal containers and scoped themes
+
+Dialog, DropdownMenu, Popover, Select and Tooltip normally portal to `document.body`. When the renderer lives inside a scoped theme, supply a stable host element with that theme through `PortalContainerProvider`:
+
+```tsx
+import { PortalContainerProvider } from "@json-render/shadcn";
+import { Renderer } from "@json-render/react";
+
+
+
+
+```
+
+The host owns the element and its lifetime. Place it outside clipped or transformed ancestors when overlays must cover the viewport. Nested providers select their nearest container. Without a provider, or with `container={undefined}`, the standard Radix body portal remains unchanged. The provider does not change component schemas, state, actions or overlay interactions.
+
## Extending with Custom Components
Pick standard components as a base and add your own alongside them:
@@ -203,7 +218,7 @@ All form components support the `validateOn` prop to control when validation run
| Entry Point | Exports |
|-------------|---------|
-| `@json-render/shadcn` | `shadcnComponents` |
+| `@json-render/shadcn` | `shadcnComponents`, `PortalContainerProvider` |
| `@json-render/shadcn/catalog` | `shadcnComponentDefinitions` |
The `/catalog` entry point contains only Zod schemas (no React dependency), so it can be used in server-side code for prompt generation.
diff --git a/packages/shadcn/src/index.ts b/packages/shadcn/src/index.ts
index fc7ef14e..a3243564 100644
--- a/packages/shadcn/src/index.ts
+++ b/packages/shadcn/src/index.ts
@@ -7,3 +7,5 @@ export {
type ComponentDefinition,
type ShadcnProps,
} from "./catalog";
+
+export { PortalContainerProvider } from "./portal-container";
diff --git a/packages/shadcn/src/portal-container.tsx b/packages/shadcn/src/portal-container.tsx
new file mode 100644
index 00000000..90f82765
--- /dev/null
+++ b/packages/shadcn/src/portal-container.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { createContext, useContext, type ReactNode } from "react";
+
+const PortalContainerContext = createContext(undefined);
+
+/** Keep shadcn portals inside the host's theme and stylesheet boundary. */
+export function PortalContainerProvider({
+ container,
+ children,
+}: {
+ container: HTMLElement | undefined;
+ children: ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePortalContainer() {
+ return useContext(PortalContainerContext);
+}
diff --git a/packages/shadcn/src/ui/dialog.tsx b/packages/shadcn/src/ui/dialog.tsx
index aa245b90..6aa0be1c 100644
--- a/packages/shadcn/src/ui/dialog.tsx
+++ b/packages/shadcn/src/ui/dialog.tsx
@@ -5,6 +5,7 @@ import { XIcon } from "lucide-react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { cn } from "../lib/utils";
+import { usePortalContainer } from "../portal-container";
import { Button } from "./button";
function Dialog({
@@ -22,7 +23,8 @@ function DialogTrigger({
function DialogPortal({
...props
}: React.ComponentProps) {
- return ;
+ const container = usePortalContainer();
+ return ;
}
function DialogClose({
diff --git a/packages/shadcn/src/ui/dropdown-menu.tsx b/packages/shadcn/src/ui/dropdown-menu.tsx
index 0ca63b15..60b1ebd0 100644
--- a/packages/shadcn/src/ui/dropdown-menu.tsx
+++ b/packages/shadcn/src/ui/dropdown-menu.tsx
@@ -5,6 +5,7 @@ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import { cn } from "../lib/utils";
+import { usePortalContainer } from "../portal-container";
function DropdownMenu({
...props
@@ -15,8 +16,9 @@ function DropdownMenu({
function DropdownMenuPortal({
...props
}: React.ComponentProps) {
+ const container = usePortalContainer();
return (
-
+
);
}
@@ -36,8 +38,9 @@ function DropdownMenuContent({
sideOffset = 4,
...props
}: React.ComponentProps) {
+ const container = usePortalContainer();
return (
-
+
) {
+ const container = usePortalContainer();
return (
-
+
) {
+ const container = usePortalContainer();
return (
-
+
) {
+ const container = usePortalContainer();
return (
-
+