Skip to main content

Overlays and Modals

This guide demonstrates how to implement and configure interactive overlays—including Dialogs, Sheets, Dropdown Menus, and Tooltips—using the Radix UI-based components in this project.

Create a Modal Dialog for CRUD Operations

Use the Dialog component to create modal windows for forms or confirmation actions. This is the standard pattern for adding or editing resources in the Admin and Items modules.

import { useState } from "react"
import { Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { LoadingButton } from "@/components/ui/loading-button"

export const AddUser = () => {
const [isOpen, setIsOpen] = useState(false)

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="my-4">
<Plus className="mr-2" />
Add User
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add User</DialogTitle>
<DialogDescription>
Fill in the form below to add a new user to the system.
</DialogDescription>
</DialogHeader>

{/* Form content goes here */}

<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<LoadingButton type="submit">Save</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

Key Components

  • DialogTrigger: Wraps the element that opens the modal. Use asChild to prevent extra DOM nodes.
  • DialogContent: The container for the modal content.
  • DialogClose: A helper to close the modal, typically used for "Cancel" buttons.

Implement a Responsive Side Panel (Sheet)

The Sheet component is used for side-aligned overlays. In this project, it is primarily used to implement the mobile version of the sidebar.

import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"

// Example from frontend/src/components/ui/sidebar.tsx
export const MobileSidebar = ({ openMobile, setOpenMobile, children }) => {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
<SheetContent
side="left"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}

Group Actions in a Dropdown Menu

Use DropdownMenu to group row-level actions in data tables or to provide navigation options.

import { EllipsisVertical } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export const UserActionsMenu = ({ user }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => console.log("Edit", user.id)}>
Edit User
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
Delete User
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

Add Contextual Tooltips

Tooltips provide additional information on hover. The SidebarMenuButton component integrates tooltips to show labels when the sidebar is collapsed.

import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"

export const SidebarItem = ({ icon: Icon, label, isCollapsed }) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<button className="flex items-center gap-2">
<Icon className="size-4" />
{!isCollapsed && <span>{label}</span>}
</button>
</TooltipTrigger>
<TooltipContent side="right" hidden={!isCollapsed}>
{label}
</TooltipContent>
</Tooltip>
)
}

Advanced: Triggering Dialogs from Dropdown Menus

A common pattern in this codebase (e.g., EditUser.tsx) is triggering a Dialog from a DropdownMenuItem. To prevent the dropdown's selection logic from interfering with the dialog's focus management, you must prevent the default selection event.

// frontend/src/components/Admin/EditUser.tsx
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()} // CRITICAL: Prevents menu closing issues
onClick={() => setIsOpen(true)}
>
<Pencil className="mr-2 h-4 w-4" />
Edit User
</DropdownMenuItem>
<DialogContent>
{/* Edit Form */}
</DialogContent>
</Dialog>

Troubleshooting

Dialog Not Opening from Dropdown

If a Dialog fails to open when triggered from a DropdownMenuItem, ensure you have called e.preventDefault() in the onSelect handler of the DropdownMenuItem. Radix UI's dropdown menu closes on selection by default, which can destroy the trigger before the dialog can mount.

Interacting with Background Elements

By default, DropdownMenu and Dialog are modal, meaning they block interaction with the rest of the page. If you need to allow interaction (e.g., for a theme switcher), set the modal prop to false:

// frontend/src/components/Common/Appearance.tsx
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="outline">Toggle Theme</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

Accessibility Labels

When using Sheet or Dialog without visible headers (like the mobile sidebar), use the sr-only class on SheetHeader or DialogHeader to provide necessary context for screen readers while keeping the UI clean.

<SheetHeader className="sr-only">
<SheetTitle>Mobile Navigation</SheetTitle>
</SheetHeader>