A responsive modal component for shadcn/ui.
The easiest way to install Credenza is through the shadcn registry:
pnpm dlx shadcn@latest add https://credenza.rdev.pro/r/credenza.json
You can also use npm:
npx shadcn@latest add https://credenza.rdev.pro/r/credenza.json
- Copy the
dialog
anddrawer
component from shadcn/ui.
npx shadcn@latest add dialog drawer
Alternatively, if you are not using shadcn/ui cli, you can manually copy the components from shadcn/ui or directly copy from dialog.tsx and drawer.tsx.
If you copied the drawer component manually, make sure to install vaul.
npm install vaul
- Copy the
useIsMobile
hook: use-mobile.ts
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}
- Copy the
credenza
component: credenza.tsx
Click to show code
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
interface BaseProps {
children: React.ReactNode
}
interface RootCredenzaProps extends BaseProps {
open?: boolean
onOpenChange?: (open: boolean) => void
}
interface CredenzaProps extends BaseProps {
className?: string
asChild?: true
}
const CredenzaContext = React.createContext<{ isMobile: boolean }>({
isMobile: false,
})
const useCredenzaContext = () => {
const context = React.useContext(CredenzaContext)
if (!context) {
throw new Error(
"Credenza components cannot be rendered outside the Credenza Context"
)
}
return context
}
const Credenza = ({ children, ...props }: RootCredenzaProps) => {
const isMobile = useIsMobile()
const Credenza = isMobile ? Drawer : Dialog
return (
<CredenzaContext.Provider value={{ isMobile }}>
<Credenza {...props} {...(isMobile && { autoFocus: true })}>
{children}
</Credenza>
</CredenzaContext.Provider>
)
}
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
const { isMobile } = useCredenzaContext()
const CredenzaTrigger = isMobile ? DrawerTrigger : DialogTrigger
return (
<CredenzaTrigger className={className} {...props}>
{children}
</CredenzaTrigger>
)
}
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
const { isMobile } = useCredenzaContext()
const CredenzaClose = isMobile ? DrawerClose : DialogClose
return (
<CredenzaClose className={className} {...props}>
{children}
</CredenzaClose>
)
}
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
const { isMobile } = useCredenzaContext()
const CredenzaContent = isMobile ? DrawerContent : DialogContent
return (
<CredenzaContent className={className} {...props}>
{children}
</CredenzaContent>
)
}
const CredenzaDescription = ({
className,
children,
...props
}: CredenzaProps) => {
const { isMobile } = useCredenzaContext()
const CredenzaDescription = isMobile ? DrawerDescription : DialogDescription
return (
<CredenzaDescription className={className} {...props}>
{children}
</CredenzaDescription>
)
}
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
const { isMobile } = useCredenzaContext()
const CredenzaHeader = isMobile ? DrawerHeader : DialogHeader
return (
<CredenzaHeader className={className} {...props}>
{children}
</CredenzaHeader>
)
}
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
const { isMobile } = useCredenzaContext()
const CredenzaTitle = isMobile ? DrawerTitle : DialogTitle
return (
<CredenzaTitle className={className} {...props}>
{children}
</CredenzaTitle>
)
}
const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
return (
<div className={cn("px-4 md:px-0", className)} {...props}>
{children}
</div>
)
}
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
const { isMobile } = useCredenzaContext()
const CredenzaFooter = isMobile ? DrawerFooter : DialogFooter
return (
<CredenzaFooter className={className} {...props}>
{children}
</CredenzaFooter>
)
}
export {
Credenza,
CredenzaTrigger,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaHeader,
CredenzaTitle,
CredenzaBody,
CredenzaFooter,
}
-
Update the import paths based on your project structure.
-
If you want to enable background scaling, wrap your app with the
vaul-drawer-wrapper
.
<div vaul-drawer-wrapper="" className="bg-background">{children}</div>
See my implementation at layout.tsx. Make sure to update the background color to match your project's theme.
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "@/components/ui/credenza"
Basic usage with CredenzaTrigger
<Credenza>
<CredenzaTrigger asChild>
<button>Open modal</button>
</CredenzaTrigger>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Credenza</CredenzaTitle>
<CredenzaDescription>
A responsive modal component for shadcn/ui.
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
This component is built using shadcn/ui's dialog and drawer
component, which is built on top of Vaul.
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<button>Close</button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
Using state to open modal
function StateModal() {
const [open, setOpen] = React.useState(false)
const handleOpen = () => {
setOpen(true)
}
return (
<>
<Button onClick={handleOpen}>Open with State</Button>
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Credenza</CredenzaTitle>
<CredenzaDescription>
A responsive modal component for shadcn/ui.
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>This modal got triggered using state</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button>Close</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
)
}
- shadcn/ui by shadcn
- Vaul by emilkowalski