Render either a Sheet, or a Dialog in a streamlined way with Shadcn/ui?

Hi there, i'm dipping my toes into shadcn lately, and i'm really digging it. But a common issue i've been running into, is that often times i'd like to use a Sheet component on mobile, while on desktop i'd like to use a Dialog component. Would there be a way to have a re-usable component that will either render a Dialog or a Sheet, similar to how the Sheet and Dialog function independently? Or do you think I am thinking of this in the wrong way? For context what i'm roughly thinking of:
<DialogOrSheet> // These components would either render a Dialog, or a Sheet, depending on conditions I set it, or alternatively as a prop.
<DialogOrSheetTrigger>Trigger example</DialogOrSheetTrigger>
<DialogOrSheetHeader>
<DialogOrSheetTitle>A cool title</DialogOrSheetTitle>
</DialogOrSheetHeader>
<DialogOrSheetContent>...</DialogOrSheetContent>
</DialogOrSheet>
<DialogOrSheet> // These components would either render a Dialog, or a Sheet, depending on conditions I set it, or alternatively as a prop.
<DialogOrSheetTrigger>Trigger example</DialogOrSheetTrigger>
<DialogOrSheetHeader>
<DialogOrSheetTitle>A cool title</DialogOrSheetTitle>
</DialogOrSheetHeader>
<DialogOrSheetContent>...</DialogOrSheetContent>
</DialogOrSheet>
11 Replies
ott
ott11mo ago
I would have both on the page and only show one depending on the device width with tailwind you can use className="hidden md:flex" and className="flex md:hidden" for the different containers
Xaohs
Xaohs11mo ago
Yeah, of course that would work, but that's not scalable for long-term in my opinion. You'd have so much repeated code for in essence, the exact same thing just a slightly different type of popup I've did some brainstorming and came up with something like this, which I think is pretty okay(ish):
function useMode() {
const { windowSize } = useWindowSize();
return windowSize.lg ? 'dialog' : 'sheet';
}

type DialogOrSheetProps = DialogAndSheetPrimitive.DialogProps;
const DialogOrSheet: FC<DialogOrSheetProps> = ({ ...props }) => {
const mode = useMode();
if (mode === 'dialog') {
return <Dialog.Dialog {...props} />;
} else {
return <Sheet.Sheet {...props} />;
}
};

type DialogOrSheetTriggerProps = DialogAndSheetPrimitive.DialogTriggerProps;
const DialogOrSheetTrigger: FC<DialogOrSheetTriggerProps> = ({ ...props }) => {
const mode = useMode();
if (mode === 'dialog') {
return <Dialog.DialogTrigger {...props} />;
} else {
return <Sheet.SheetTrigger {...props} />;
}
};

.......
function useMode() {
const { windowSize } = useWindowSize();
return windowSize.lg ? 'dialog' : 'sheet';
}

type DialogOrSheetProps = DialogAndSheetPrimitive.DialogProps;
const DialogOrSheet: FC<DialogOrSheetProps> = ({ ...props }) => {
const mode = useMode();
if (mode === 'dialog') {
return <Dialog.Dialog {...props} />;
} else {
return <Sheet.Sheet {...props} />;
}
};

type DialogOrSheetTriggerProps = DialogAndSheetPrimitive.DialogTriggerProps;
const DialogOrSheetTrigger: FC<DialogOrSheetTriggerProps> = ({ ...props }) => {
const mode = useMode();
if (mode === 'dialog') {
return <Dialog.DialogTrigger {...props} />;
} else {
return <Sheet.SheetTrigger {...props} />;
}
};

.......
ott
ott11mo ago
i would make a single component that renders both the dialogue and sheet like
export function DialogueOrSheet({trigger, title, description}) {

return (
<>
<Sheet className="block lg:hidden">
<SheetTrigger>{trigger}</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
<Dialog className="hidden lg:block">
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
)
}
export function DialogueOrSheet({trigger, title, description}) {

return (
<>
<Sheet className="block lg:hidden">
<SheetTrigger>{trigger}</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
<Dialog className="hidden lg:block">
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
)
}
Xaohs
Xaohs11mo ago
Yeah that could also work, but that kind of disables all kind of props that I'd like to pass to the components from time to time
ott
ott11mo ago
ya, there probably a way to get the intersecting properties and unique properties so you can do something like <Sheet {...intersecting} {...sheetProps}> and <Dialogue {...intersecting} {...dialogueProps}>
Xaohs
Xaohs11mo ago
Hmm yeah. I appreciate your opinion on this! Not sure if this is kind of an anti-pattern I'm developing but we'll see haha
ott
ott11mo ago
A type for getting properties common in all objects from a union
Using typescript type conditions and Exclude to get keys commmon in parts of a union and an object with only common keys from that union
ott
ott11mo ago
something like K in keyof T if K not keyof U and K in keyof T if K keyof U, im not a major typescript expert but chatgpt might be i don't think its an antipattern, i've mostly just used the mobile dialogue because its works well enough why do you want to render a sheet anyway?
Xaohs
Xaohs11mo ago
Well, I think sheets are just a way better experience on mobile than dialogs, dialogs feel very intrusive, and don't look as nice on mobile as sheets do.
ott
ott11mo ago
fair enough
max14
max1411mo ago
I think if you look at the Skateshop website (by shadcn) his way seems fine, I know what you said earlier but his implementation is pretty solid he renders a sheet for mobile and something else for normal (can’t remember)
Want results from more Discord servers?
Add your server