Using Cue with shadcn
Install Cue with shadcn and let Cue own the shared backdrop.
The @vlkoss/cue registry item installs shadcn's Dialog component and creates a local Cue module for your app. The namespace is @vlkoss; cue is the item name.
Once shadcn/ui PR #11959 is merged, the recommended install will be:
npx shadcn@latest add @vlkoss/cueUntil then, install the same registry item directly:
npx shadcn@latest add https://cue.vlkstudio.com/r/cue.jsonThe registry installs shadcn's dialog item first, then creates components/cue/index.tsx. That file owns the Cue instance, the shared backdrop, and the Dialog component map. You can edit it like any other application file.
Allow Cue to own the backdrop
shadcn's DialogContent normally renders its own DialogOverlay. Cue renders one backdrop for the whole overlay stack, so the local Dialog component needs an opt-out.
Add showBackdrop to the existing DialogContent props, default it to true, and render the existing DialogOverlay conditionally:
function DialogContent({
showBackdrop = true,
...props
}: DialogContentProps & { showBackdrop?: boolean }) {
return (
<DialogPortal>
{showBackdrop && <DialogOverlay />}
{/* Keep the existing Dialog content or popup here. */}
</DialogPortal>
);
}Keep the rest of shadcn's generated component unchanged. With the Radix-based Dialog, the existing content element is DialogPrimitive.Content. With the Base UI Dialog, it is DialogPrimitive.Popup.
The generated Cue module
The generated file adapts the local shadcn components to Cue. wrapper is the Dialog root. content disables the local backdrop. The shared backdrop is a dim Cue portals with the overlay instances, not a second Dialog root. The remaining keys are ordinary shared components that you can use or replace.
// components/cue/index.tsx
"use client";
import type { ComponentProps } from "react";
import { createCue, type CueBackdrop } from "@vlkoss/cue";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
type CueContentProps = Omit<ComponentProps<typeof DialogContent>, "showBackdrop">;
function CueContent(props: CueContentProps) {
return <DialogContent {...props} showBackdrop={false} />;
}
const CueBackdrop: CueBackdrop = ({ open, close }) => (
<div
aria-hidden="true"
data-open={open ? "" : undefined}
className="fixed inset-0 isolate z-50 bg-black/10 opacity-0 transition-opacity duration-100 supports-backdrop-filter:backdrop-blur-xs starting:opacity-0 data-open:opacity-100"
style={{ pointerEvents: open ? "auto" : "none" }}
onClick={() => close({ strategy: "last" })}
/>
);
export const cue = createCue({
backdrop: CueBackdrop,
components: {
wrapper: Dialog,
content: CueContent,
footer: DialogFooter,
header: DialogHeader,
title: DialogTitle,
description: DialogDescription,
close: DialogClose,
trigger: DialogTrigger,
},
});Do not copy DialogOverlay's animate-in / animate-out classes onto this div. Cue keeps the backdrop mounted until close delay ends, and those keyframes use fill-mode none, so the dim fades out then pops back in until unmount. An opacity transition stays faded. Cue passes open for that delay, and this adapter closes the top open overlay instance on click.
Use the configured components
Every overlay created from this Cue gets the exact component map through ctx.components:
import { cue } from "@/components/cue";
export const confirmDialog = cue.createOverlay<{ message: string }, boolean>((props, ctx) => {
const components = ctx.components;
return (
<components.wrapper open={ctx.open} onOpenChange={ctx.onOpenChange}>
<components.content>
<components.header>
<components.title>Confirm</components.title>
<components.description>{props.message}</components.description>
</components.header>
<components.footer>
<button type="button" onClick={() => ctx.close({ result: false })}>
Cancel
</button>
<button type="button" onClick={() => ctx.close({ result: true })}>
Confirm
</button>
</components.footer>
</components.content>
</components.wrapper>
);
});Mount the same instance near the app root:
<cue.OverlayProvider>{children}</cue.OverlayProvider>You can change the generated component map whenever the application needs different shared pieces. Cue does not render those components automatically.