Cal.comdesign-system

Modality

When Cal.com uses dialogs, drawers, sheets, alert dialogs, and popovers — and how to compose them.

Modal surfaces interrupt the current task to collect input, confirm a decision, or show focused detail. They sit above the page and block interaction with content underneath.

Use them sparingly. In Cal.com product UI, modality comes from @coss/ui — not legacy @calcom/ui dialogs.

Primitive APIs live on coss.com/ui. This page covers which component to pick and compositions we ship.

Choosing a surface

Naming debt: many files are named *Sheet* but import Drawer (e.g. EditMemberSheet, BlocklistEntryDetailsSheet). Check imports, not filenames.

Dialog

Centered overlays for short, focused tasks: create flows, invites, booking actions, SSO setup, previews.

Dialog · short create flow
apps/web — create / invite dialog (pattern)

Dialog conventions

  • showCloseButton={false} on DialogPopup — use DialogClose render={<Button />} in the footer (or a custom header action) instead of the default X.
  • Form className="contents" — form participates in dialog layout without breaking header / panel / footer stacking. See Forms.
  • onOpenChangeComplete — reset form state after the close animation, not while the dialog is still visible.
  • className="max-w-*" on DialogPopup — width sizing (max-w-xl is common for forms).

Real call sites: CreateEventTypeDialog.tsx, MemberInvitationModal.tsx, smtp-configuration-dialog.tsx, ReassignDialog.tsx, booking-action-dialogs.tsx.

AlertDialog

For destructive or irreversible actions: delete, disconnect, cancel event, charge card, bulk remove.

AlertDialog · delete confirmation
apps/web — DeleteWebhookDialog (pattern)

AlertDialog conventions

  • No DialogPanel — body fields use manual padding (px-6 pb-6) when needed (cancel-event-dialog.tsx).
  • Primary action is destructive — variant="destructive" for delete/remove; AlertDialogClose stays non-destructive (ghost Cancel).
  • Optional field in AlertDialog is still AlertDialog when the flow is fundamentally “confirm destruction” — e.g. optional cancellation reason in cancel-event-dialog.tsx.
  • Prefer controlled open / onOpenChange from the parent; AlertDialogTrigger is fine for inline delete buttons.

Use Dialog (not AlertDialog) for informational confirmations that are not destructive — e.g. billing period switch.

Real call sites: DeleteWebhookDialog.tsx, DeleteDialog.tsx (event types), cancel-event-dialog.tsx, disconnect-account-dialog.tsx, DeleteBulkUsers.tsx, team-danger-zone.tsx.

Drawer

Slide-over panels when the user opened something from a list or on mobile and should still feel anchored to the page.

Drawer · edit from list (position=right)
apps/web — role-sheet (pattern)

Drawer variants

Same Form className="contents" / DrawerClose render={<Button />} rules as Dialog.

Real call sites: booking-details-drawer.tsx, role-sheet.tsx, BlocklistEntryDetailsSheet.tsx, EventTypeLayout.tsx, ActiveUserBreakdown.tsx.

Sheet

@coss/ui/components/sheet is legacy. New side panels, including history and audit panels, use Drawer. Sheet is modal by default (blocks the page); existing Sheet panels move to Drawer over time.

Sheet · history / audit panel
apps/web — event-type-history-sheet (legacy, still on Sheet)

Drawer vs Sheet in practice: interactive edit flows often land in Drawer even when the file says Sheet. The history panels still on Sheet (event-type-history-sheet.tsx, routing-form-history-sheet.tsx) are known debt to move to Drawer.

Real call sites: event-type-history-sheet.tsx, routing-form-history-sheet.tsx, assign-feature-sheet.tsx, agent-configuration-sheet.tsx.

Popover

Non-modal, anchored overlays — filters, date pickers, hover help. Never use for full forms or destructive confirmations.

Popover · anchored helper (non-modal)
Organization slug
apps/web — HelpLink (pattern)

Filter chips wrap Popover inside FilterChipShell — see Tables and Filter chip shell.

Real call sites: help-link.tsx, date-range-filter.tsx, text-filter.tsx, TravelScheduleModal.tsx (popovers inside a dialog form).

Dirty form dismissal

Modals with forms should reset on close via onOpenChangeComplete. Cal.com does not yet ship a shared “discard unsaved changes” hook across all dialogs.

What we do today:

  • Reset after close animation — onOpenChangeComplete + form.reset() (SMTP dialog, cancel-event dialog clears reason on close).
  • Page-level guard — Cal Events editor warns on full-page navigation with unsaved edits (event-editor-web-wrapper.tsx).
  • Explicit save-before-navigate — payment app install prompts when OAuth would navigate away (payment-app-select.tsx).

When adding dirty-close confirmation, use a nested AlertDialog (“Discard changes?” / “Keep editing”) and intercept onOpenChange — same flow described in Coss dialog docs and .agents/skills/coss-migration/references/rules/dialogs-overlays.md. Do not block route changes from inside the dialog alone.

Do / don't

Do

  • Match header and primary button text to the entry point (“Delete webhook”, not “OK”).
  • Use AlertDialog for delete/disconnect/cancel flows.
  • Use Drawer for every edge panel, including audit and history panels.
  • Keep showCloseButton={false} on DialogPopup with explicit Cancel in the footer; on DrawerPopup pass showCloseButton so long-scrolling panels can be dismissed from the header.
  • Use Popover for anchored pickers; Tooltip for single-line hints.

Don't

  • Use Dialog for simple delete confirm — use AlertDialog.
  • Use Popover for multi-field submit forms.
  • Assume *Sheet* in the filename means @coss/ui/components/sheet.
  • Reset form state in onOpenChange(true) — use onOpenChangeComplete(false).

Reference call sites