Published on

How to Retain Close Button Functionality in Shadcn Dialogs ?

Authors
  • Name
    Ripal & Zalak
    Twitter

When working with Shadcn or Radix dialogs in React, especially in Next.js, using the open prop for programmatic control can sometimes conflict with the built-in close button functionality. This guide explains how to ensure the dialog’s close button continues to function properly while maintaining programmatic control.

The Problem

When you use the open prop to control a Shadcn dialog, you might find that the close button (the X in the top-right corner) stops working. This happens because the dialog's open state is now controlled entirely by the parent component, and the close button no longer triggers a state change.

Example

Here’s a typical example of a dialog using the open prop:

const [isOpen, setIsOpen] = useState(false)

return (
  <Dialog open={isOpen} onOpenChange={setIsOpen}>
    <DialogTrigger>Open Dialog</DialogTrigger>
    <DialogContent>
      <DialogHeader>
        <DialogTitle>Dialog Title</DialogTitle>
        <DialogDescription>Content goes here</DialogDescription>
      </DialogHeader>
    </DialogContent>
  </Dialog>
)

In this setup, the dialog's open state is tied to the isOpen state. Clicking the close button will not update this state unless explicitly handled.

Solution: Use onOpenChange

To fix this, you need to use the onOpenChange prop to handle state changes triggered by the close button or other user interactions (e.g., clicking outside the dialog). Here’s the updated code:

const [isOpen, setIsOpen] = useState(false)

return (
  <Dialog open={isOpen} onOpenChange={setIsOpen}>
    <DialogTrigger>Open Dialog</DialogTrigger>
    <DialogContent>
      <DialogHeader>
        <DialogTitle>Dialog Title</DialogTitle>
        <DialogDescription>Content goes here</DialogDescription>
      </DialogHeader>
      <p>Some more content here.</p>
    </DialogContent>
  </Dialog>
)

Why This Works

The onOpenChange prop ensures that state changes triggered by the close button (or other interactive elements) are communicated back to your component. The setIsOpen function is called automatically with the new state, keeping your component in sync with the dialog.

Additional Considerations

  1. Custom Close Behavior: If you want to add custom behavior when the close button is clicked, you can extend the onOpenChange handler:

    const handleDialogClose = (open: boolean) => {
      if (!open) {
        // Perform additional actions, e.g., cleanup or analytics
        console.log('Dialog closed')
      }
      setIsOpen(open)
    }
    
    return (
      <Dialog open={isOpen} onOpenChange={handleDialogClose}>
        <DialogTrigger>Open Dialog</DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Dialog Title</DialogTitle>
            <DialogDescription>Content goes here</DialogDescription>
          </DialogHeader>
        </DialogContent>
      </Dialog>
    )
    
  2. Prevent Closing on Outside Click: If you want to prevent the dialog from closing when clicking outside, you can customize the dialog behavior:

    <Dialog open={isOpen} onOpenChange={(open) => setIsOpen(open)}>
      <DialogContent onInteractOutside={(e) => e.preventDefault()}>
        <DialogHeader>
          <DialogTitle>Dialog Title</DialogTitle>
        </DialogHeader>
      </DialogContent>
    </Dialog>
    
  3. Keyboard Accessibility: Radix dialogs are keyboard accessible by default, meaning users can press Esc to close the dialog. Ensure your state management supports this behavior.

Conclusion

By combining the open prop with the onOpenChange handler, you can maintain programmatic control over the dialog while ensuring that the close button and other interactive elements work as expected. This approach provides a seamless user experience and avoids potential issues with state synchronization.