<Dialog /> / <Portal /> custom root element #666
Replies: 27 comments 28 replies
-
Any update on this? This is a feature we would like as well 😄 |
Beta Was this translation helpful? Give feedback.
-
Running into this same issue. Custom root element would be great. |
Beta Was this translation helpful? Give feedback.
-
Same here. I think I'm running into a similar thing... |
Beta Was this translation helpful? Give feedback.
-
Same issue here. Workaround: must use a custom modal implementation :/ |
Beta Was this translation helpful? Give feedback.
-
Would be great to have this feature. My custom Modal implementation isn't as good as this one 😢. Using styled components and my CSS is scoped to the top of my shadow too. A custom element for where the portal should be rendered would be a great implementation! |
Beta Was this translation helpful? Give feedback.
-
I also have the use case where I don't want to use a portal because the dialog needs to be positioned based on the element close to which the React component is rendered. The dialog takes in most of the screen space, the background gets blurred while the dialog is open and I want to trap the focus in the dialog, so using a Popover wouldn't be the right choice here. I need to use a different library for this although the functionality is basically the same as the Headless UI dialog. Would be awesome if Headless UI could support not using a portal at all! ❤️ |
Beta Was this translation helpful? Give feedback.
-
I have an unbelievably hacky solution, but it works (sort of). Paste the following code as early as possible (index.ts or something) const elementById = Document.prototype.getElementById;
Document.prototype.getElementById = function (elementId: string) {
if (elementId === "headlessui-portal-root") {
return root.element; // This is the root element which exists inside the Shadow DOM
}
return elementById.call(this, elementId);
}; The issue now is that clicking anywhere closes the Modal |
Beta Was this translation helpful? Give feedback.
-
I've written a more complete solution here - #874 (comment) |
Beta Was this translation helpful? Give feedback.
-
Dialog should allow for customizing what's/where's the portal root node. This will solve problems like: Until then, devs have to choose a different Dialog implementation, or go around with hacks (like replacing the @RobinMalfait tell me that you'd be open to this, and I can raise a PR. |
Beta Was this translation helpful? Give feedback.
-
For some reason non of the solutions here helped me, so listening to changes in the DOM and then moving the portal root to wherever I wanted worked for me: // Just create the container and shadow root
let container = document.getElementById('some-container');
if (!container) {
container = document.createElement('div');
container.id = 'some-container';
document.body.appendChild(container);
}
const shadow = container.shadowRoot ?? container.attachShadow({ mode: 'open' });
/**
* Every time a new element `headlessui-portal-root` is added to the DOM,
* it will place it into the shadow body inside `some-container`
*/
new MutationObserver((records: MutationRecord[]) => {
const record: MutationRecord | undefined = records
.filter((record) => (record.target as HTMLElement).id === 'headlessui-portal-root')
.pop();
if (!record) return;
shadow.append(record.target);
}).observe(document, { childList: true, subtree: true }); |
Beta Was this translation helpful? Give feedback.
-
Forcing a single portal that is unmodifiable is a pretty big architectural flaw imo. We should be able to use our own like Radix UI does https://www.radix-ui.com/docs/primitives/components/dialog#custom-portal-container |
Beta Was this translation helpful? Give feedback.
-
I really need this for my use case. |
Beta Was this translation helpful? Give feedback.
-
I have the same issue here. As a work around i am doing the following:
|
Beta Was this translation helpful? Give feedback.
-
any updates on this? |
Beta Was this translation helpful? Give feedback.
-
For those who happen to them in Next.js and the new Font component , this was my solution:
|
Beta Was this translation helpful? Give feedback.
-
Just got trapped into that same issue, by trying to introduce dynamic CSS variables. In Vue, they can't be bound to <script setup>
// Root.vue
const primaryColor = computed(() => ({
50: '#eff9ff',
100: '#def2ff',
200: '#b6e7ff',
300: '#75d7ff',
400: '#2cc3ff',
500: '#00A0E6',
600: '#0089d4',
700: '#006dab',
800: '#005c8d',
900: '#064d74',
950: '#04304d',
})); // Computed, because values can change at runtime (changing app theme)
</script>
<style lang="scss">
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
--color-primary-50: v-bind(primaryColor[50]);
--color-primary-100: v-bind(primaryColor[100]);
--color-primary-200: v-bind(primaryColor[200]);
--color-primary-300: v-bind(primaryColor[300]);
--color-primary-400: v-bind(primaryColor[400]);
--color-primary-500: v-bind(primaryColor[500]);
--color-primary-600: v-bind(primaryColor[600]);
--color-primary-700: v-bind(primaryColor[700]);
--color-primary-800: v-bind(primaryColor[800]);
--color-primary-900: v-bind(primaryColor[900]);
--color-primary-950: v-bind(primaryColor[950]);
}
}
</style>
<template>
<RouterView />
</template> // tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: 'var(--color-primary-50)',
100: 'var(--color-primary-100)',
200: 'var(--color-primary-200)',
300: 'var(--color-primary-300)',
400: 'var(--color-primary-400)',
500: 'var(--color-primary-500)',
600: 'var(--color-primary-600)',
700: 'var(--color-primary-700)',
800: 'var(--color-primary-800)',
900: 'var(--color-primary-900)',
950: 'var(--color-primary-950)',
}
},
},
},
}; <script setup>
// PrimaryButton.vue
</script>
<template>
<button class="bg-primary-700 text-white">
<slot />
</button>
</template> Rendering
Thanks a lot @okpedro for saving my day! |
Beta Was this translation helpful? Give feedback.
-
Just wanted to mention that function DisableInert() {
useEffect(() => {
const rootNode = document.getElementById("__next");
if (rootNode) {
rootNode.inert = false;
}
}, []);
return null;
} |
Beta Was this translation helpful? Give feedback.
-
Why is this issue not fixed after 3 years of initial raise? any particular reason? This flaw significantly affects the main concept of headless UI - fully customizable, the modal component is completely useless for my usecase because of this ;*( |
Beta Was this translation helpful? Give feedback.
-
Was there any solution found for this problem? :-( |
Beta Was this translation helpful? Give feedback.
-
I facing the Dialog display issue when rendering Dialog in iframe (using react-frame-component), the Dialog will always attach to the outer window instead of the iframe window. |
Beta Was this translation helpful? Give feedback.
-
The way I solve it was to set a Modal on the my template root (the first element that will render and use context to open and close it. TL;DR I think I create my own modal but still use Transitions from headlessUI to animate it |
Beta Was this translation helpful? Give feedback.
-
Wanted to chime in with my use case where I stumbled on this issue: I am using Storybook + React and was testing out a Dialog. I have a wrapper around my Stories at the preview level which applies my custom font. This works fine for the baseline rendering of a Story, but when a Dialog Portal is created it is created outside of that wrapper and thus does not retain the font. I can fix this for an initial Dialog opening by defining a div with id = 'headlessui-portal-root' within my Stories rendering function, but this gets wiped out after you close and reopen the Dialog a second time. Given this issue is just in a testing environment I will probably just overlook it for now, but it would be nice to have a way to define a permanent portal root if necessary. |
Beta Was this translation helpful? Give feedback.
-
this makes it impossible to have more than one root (i.e. separate apps on the same page) It also makes it soe that you can't guarantee certain dialogs are at a higher stacking context than others |
Beta Was this translation helpful? Give feedback.
-
Chiming in with another workaround for usage in Storybook using the removeInert hook from @sturmenta and the storybook tips from @wade-wojcak-vib: export const Default: Story = {
args: {
open: false
},
render: function Render(args) {
const [open, setOpen] = useState(args.open)
// This can be extracted into a hook, but for the sake of example it's not
useEffect(() => {
if (open) {
setTimeout(() => {
const rootNode = document.getElementById('storybook-root')
if (rootNode) rootNode.inert = false
}, 100)
}
}, [open])
return (
<>
<div id="headlessui-portal-root" />
<MeshProviderMock mocks={globalQueryMocks}>
<SaturateCacheMock>
<ButtonPrimary label="Toggle" onClick={() => setOpen(!open)} />
{/* This EditorDialog component is a thin wrapper for the headlessui dialog component and is not rendered until the button is clicked */}
{open && <EditorDialog open={open} />}
</SaturateCacheMock>
</MeshProviderMock>
</>
)
}
} Note that the headlessui portal root component needs to exist in the dom before the headlessui dialog component has been rendered, otherwise it won't detect that a root already exists in the ownerDocument |
Beta Was this translation helpful? Give feedback.
-
Not ideal, but in React I was trying to have notifications appear over all other elements. This issue caused me problems. I just did this: https://react.dev/reference/react-dom/createPortal#rendering-a-modal-dialog-with-a-portal It creates another portal which renders as expected. |
Beta Was this translation helpful? Give feedback.
-
This is so silly that we can't predefine where the portal goes to using a prop. <div id="headlessui-portal-root" /> You can try putting this in your DOM but it will only hook into it one time. Once you close the Dialog, it will remove this element and then reinject it in the default spot. So silly. Took me twenty minutes reading through this to find my solve: {/* It needs at least one child, so that HeadlessUI doesn't remove this portal root workaround
( https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-react/src/components/portal/portal.tsx#L84 ) */} |
Beta Was this translation helpful? Give feedback.
-
What package within Headless UI are you using?
@headlessui/react
What version of that package are you using?
1.0.0
What browser are you using?
chrome
Reproduction repository
I'm rendering my react app under shadow root and all my css are scoped inside the shadow root.
I want the
<Dialog />
or the<Portal />
insert inside the shadow root instead of the document.body. It would be nice we can pass custom root element for<Dialog />
/<Portal />
.Beta Was this translation helpful? Give feedback.
All reactions