Skip to content

Instantly share code, notes, and snippets.

@nicksheffield
Last active December 11, 2025 19:27
Show Gist options
  • Select an option

  • Save nicksheffield/89e30ea528b938dbdea9ff3c2db7152b to your computer and use it in GitHub Desktop.

Select an option

Save nicksheffield/89e30ea528b938dbdea9ff3c2db7152b to your computer and use it in GitHub Desktop.
Imperative modals for react using shadcn
import { openModal, type ModalProps } from '@/components/Modal'
import {
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
type MyModalProps = {
name: string
message: string
}
// this function that opens the modal. You can use it wherever you want.
export const openMyModal = (props: MyModalProps) => {
openModal({
render: (close) => {
return <MyModal close={close} {...props} />
},
})
}
const MyModal = ({ name, message, close }: ModalProps<MyModalProps>) => {
return (
<DialogContent>
<DialogHeader>
<DialogTitle>My Modal</DialogTitle>
</DialogHeader>
<div>
<h1>Hello {name}</h1>
<p>{message}</p>
</div>
{/* this is optional, clicking the backdrop will close the modal, and the ModalHeader has its own x button that also works */}
<button onClick={close}>Close</button>
</DialogContent>
)
}
'use client'
import { Dialog } from '@/components/ui/dialog'
import { ReactNode, useEffect, useState } from 'react'
type ModalDef = {
id: string
render: ReactNode
onClose?: () => void
}
// since we only have one modal provider per app,
// we can hoist some functions up out of the provider to use globally
const container: {
addModal: (def: ModalDef) => void
removeModal: (id: string) => void
} = {
addModal: () => {},
removeModal: () => {},
}
export type ModalCloseFn = () => void
export type ModalProps<T extends Record<string, unknown>> = T & {
close: ModalCloseFn
}
export const openModal = (x: {
onClose?: () => void
render: (close: ModalCloseFn) => ReactNode
}) => {
const id = crypto.randomUUID()
const close = () => {
container.removeModal(id)
}
container.addModal({
id,
render: x.render(close),
onClose: x.onClose,
})
}
export const ModalProvider = () => {
const [defs, setDefs] = useState<ModalDef[]>([])
const [closing, setClosing] = useState<string[]>([])
useEffect(() => {
container.addModal = (def: ModalDef) => {
setDefs((d) => [...d, def])
}
container.removeModal = (id: string) => {
setClosing((c) => [...c, id])
defs.find((x) => x.id === id)?.onClose?.()
const newDefs = defs.filter((x) => {
return x.id !== id
})
setDefs(newDefs)
setTimeout(() => {
setClosing((c) => c.filter((x) => x !== id))
document.body.style.pointerEvents = 'auto'
}, 500) // set this to the duration of your modal exit animation
}
}, [defs])
return (
<div id="modal-provider">
{defs.map((def) => (
<Dialog
key={def.id}
open={!closing.includes(def.id)}
onOpenChange={(val) => {
if (!val) container.removeModal(def.id)
}}
>
{def.render}
</Dialog>
))}
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment