swapmodal
v2.0.0
Published
Handle shadcn dialog, sheet and drawer with ease
Maintainers
Readme

SwapModal
Handle shadcn dialog, sheet and drawer with ease. Forked from pushmodal with improvements and bug fixes.
Installation
bun i swapmodalWe take for granted that you already have
@radix-ui/react-dialoginstalled. If not ➡️bun i @radix-ui/react-dialog
Usage
1. Create a modal
When creating a dialog/sheet/drawer you need to wrap your component with the <(Dialog|Sheet|Drawer)Content> component. But skip the Root since we do that for you.
// file: src/modals/modal-example.tsx
import { DialogContent } from '@/ui/dialog' // shadcn dialog
// or any of the below
// import { SheetContent } from '@/ui/sheet' // shadcn sheet
// import { DrawerContent } from '@/ui/drawer' // shadcn drawer
export default function ModalExample({ foo }: { foo: string }) {
return (
<DialogContent>
Your modal
</DialogContent>
)
}2. Initialize your modals
// file: src/modals/index.tsx (alias '@/modals')
import ModalExample from './modal-example'
import SheetExample from './sheet-example'
import DrawerExample from './drawer-examle'
import { createPushModal } from 'swapmodal'
import { Drawer } from '@/ui/drawer' // shadcn drawer
export const {
pushModal,
popModal,
popAllModals,
replaceWithModal,
useOnPushModal,
onPushModal,
ModalProvider
} = createPushModal({
modals: {
// Short hand
ModalExample,
SheetExample,
// Longer definition where you can choose what wrapper you want
// Only needed if you don't want `Dialog.Root` from '@radix-ui/react-dialog'
// shadcn drawer needs a custom Wrapper
DrawerExample: {
Wrapper: Drawer,
Component: DrawerExample
}
},
})How we usually structure things
src
├── ...
├── modals
│ ├── modal-example.tsx
│ ├── sheet-example.tsx
│ ├── drawer-examle.tsx
│ ├── ... more modals here ...
│ └── index.tsx
├── ...
└── ...3. Add the <ModalProvider /> to your root file.
import { ModalProvider } from '@/modals'
export default function App({ children }: { children: React.ReactNode }) {
return (
<>
{/* Notice! You should not wrap your children */}
<ModalProvider />
{children}
</>
)
}4. Use pushModal
pushModal can have 1-2 arguments
name- name of your modalprops(might be optional) - props for your modal, types are infered from your component!
import { pushModal } from '@/modals'
export default function RandomComponent() {
return (
<div>
<button onClick={() => pushModal('ModalExample', { foo: 'string' })}>
Open modal
</button>
<button onClick={() => pushModal('SheetExample')}>
Open Sheet
</button>
<button onClick={() => pushModal('DrawerExample')}>
Open Drawer
</button>
</div>
)
}4. Closing modals
You can close a modal in three different ways:
popModal()- will pop the last added modalpopModal('Modal1')- will pop the last added modal with nameModal1popAllModals()- will close all your modals
5. Replacing current modal
Replace the last pushed modal. Same interface as pushModal.
replaceWithModal('SheetExample', { /* Props if any */ })6. Using events
You can listen to events with useOnPushModal (inside react component) or onPushModal (or globally).
The event receive the state of the modal (open/closed), the modals name and props. You can listen to all modal changes with * or provide a name of the modal you want to listen on.
Inside a component
import { useCallback } from 'react'
import { useOnPushModal } from '@/modals'
// file: a-react-component.tsx
export default function ReactComponent() {
// listen to any modal open/close
useOnPushModal('*',
useCallback((open, props, name) => {
console.log('is open?', open);
console.log('props from component', props);
console.log('name', name);
}, [])
)
// listen to `ModalExample` open/close
useOnPushModal('ModalExample',
useCallback((open, props) => {
console.log('is `ModalExample` open?', open);
console.log('props for ModalExample', props);
}, [])
)
}Globally
import { onPushModal } from '@/modals'
const unsub = onPushModal('*', (open, props, name) => {
// do stuff
})7. Listening to modal close events
You can listen specifically to when modals close using useOnCloseModal (inside react component) or onCloseModal (globally). This is useful for cleanup, analytics, or triggering actions after a modal closes.
The callback receives the modal's props and name. You can optionally specify a delay (in milliseconds) before the callback executes.
Inside a component
import { useCallback } from 'react'
import { useOnCloseModal } from '@/modals'
// file: a-react-component.tsx
export default function ReactComponent() {
// Listen to any modal close
useOnCloseModal('*',
useCallback((props, name) => {
console.log('Modal closed:', name);
console.log('Final props:', props);
}, [])
)
// Listen to specific modal close with delay
useOnCloseModal(
'ModalExample',
useCallback((props) => {
console.log('ModalExample closed after 300ms');
// Perform cleanup or analytics
}, []),
{ delay: 300 } // Optional delay in milliseconds
)
}Globally
import { onCloseModal } from '@/modals'
// Immediate execution on close
const unsub = onCloseModal('SheetExample', (props, name) => {
console.log('Sheet closed!', props)
})
// With delay
const unsub2 = onCloseModal(
'DrawerExample',
(props, name) => {
console.log('Drawer closed after delay!', props)
},
{ delay: 500 }
)
// Don't forget to unsubscribe when needed
unsub()
unsub2()Common use cases:
- Analytics tracking when users close modals
- Cleanup operations after modal dismissal
- Triggering animations or state updates
- Form data persistence or validation
Responsive rendering (mobile/desktop)
In some cases you want to show a drawer on mobile and a dialog on desktop. This is possible and we have created a helper function to get you going faster. createResponsiveWrapper 💪
// path: src/modals/dynamic.tsx
import { createResponsiveWrapper } from 'swapmodal'
import { Dialog, DialogContent } from '@/ui/dialog'; // shadcn dialog
import { Drawer, DrawerContent } from '@/ui/drawer'; // shadcn drawer
export default createResponsiveWrapper({
desktop: {
Wrapper: Dialog,
Content: DialogContent,
},
mobile: {
Wrapper: Drawer,
Content: DrawerContent,
},
breakpoint: 640,
});
// path: src/modals/your-modal.tsx
import * as Dynamic from './dynamic'
export default function YourModal() {
return (
<Dynamic.Content>
Drawer in mobile and dialog on desktop 🤘
</Dynamic.Content>
)
}
// path: src/modals/index.ts
import * as Dynamic from './dynamic'
import YourModal from './your-modal'
import { createPushModal } from 'swapmodal'
export const {
pushModal,
popModal,
popAllModals,
replaceWithModal,
useOnPushModal,
onPushModal,
ModalProvider
} = createPushModal({
modals: {
YourModal: {
Wrapper: Dynamic.Wrapper,
Component: YourModal
}
},
})Issues / Limitations
Issues or limitations will be listed here.
Credits
SwapModal is forked from pushmodal by @lindesvard.
Original Contributors
License
MIT
