---
name: unity-overlays
description: >
  Load when adding Unity dialogs, side panels, bottom sheets, tooltips,
  popovers, or menus. Use it to choose modal versus non-modal overlays and keep
  disabled-control tooltip behavior accessible.
metadata:
  type: core
  library: '@payfit/unity-components'
  library_version: '2.x'
sources:
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/dialog/Dialog.tsx'
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/side-panel/SidePanel.tsx'
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/tooltip/Tooltip.tsx'
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/popover/Popover.tsx'
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/menu/Menu.tsx'
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/promo-dialog/PromoDialog.tsx'
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/bottom-sheet/BottomSheet.tsx'
---

## Setup

Overlays do not require a global provider. Each overlay owns its open state
through its own trigger (`DialogTrigger`, `PopoverTrigger`, `MenuTrigger`,
`TooltipTrigger`) or via the controlled `isOpen` / `onOpenChange` pair. Mount
the trigger and the overlay side-by-side as siblings — `DialogTrigger` reads
exactly two children: the trigger element and the overlay.

```tsx
import {
  Button,
  Dialog,
  DialogActions,
  DialogButton,
  DialogContent,
  DialogTitle,
  DialogTrigger,
} from '@payfit/unity-components'

export function ConfirmDelete({ onConfirm }: { onConfirm: () => void }) {
  return (
    <DialogTrigger>
      <Button variant="danger">Delete</Button>
      <Dialog>
        <DialogTitle>Delete employee?</DialogTitle>
        <DialogContent>This cannot be undone.</DialogContent>
        <DialogActions>
          <DialogButton variant="close">Cancel</DialogButton>
          <DialogButton variant="danger" onPress={onConfirm}>
            Delete
          </DialogButton>
        </DialogActions>
      </Dialog>
    </DialogTrigger>
  )
}
```

## Modal vs non-modal — picking the right one

| Need                                              | Component                                            | Reason                                                       |
| ------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
| Confirmation, blocking form, decision required    | `Dialog`                                             | modal: focus trap, scroll lock, backdrop, Escape closes      |
| Detail view that should not interrupt page flow   | `SidePanel`                                          | modal but side-anchored; same focus trap and Escape behavior |
| Mobile-first sheet, edit on small screens         | `BottomSheet`                                        | modal anchored to viewport bottom                            |
| Feature announcement with hero illustration       | `PromoDialog`                                        | modal with required `PromoDialogHero` slot                   |
| Hover/focus hint on an enabled control            | `Tooltip`                                            | non-interactive, hover/focus activated, single string        |
| Interactive informational content (links, fields) | `Popover`                                            | non-modal, focusable content, requires `title`               |
| Dropdown list of actions                          | `Menu` (+ `MenuTrigger`/`MenuContent`/`RawMenuItem`) | non-modal, keyboard-navigable list                           |
| Inline term definition                            | `DefinitionTooltip`                                  | non-modal, inline anchor                                     |

Modal overlays all build on `react-aria-components`' `ModalOverlay` — they
manage focus, restoration, scroll lock, and Escape automatically.

## Core Patterns

- Compose blocking decisions with `DialogTrigger`, `Dialog`, and
  `DialogActions`.
- Use `SidePanel` for side-anchored detail or editing flows.
- Use `Popover` for focusable informational content and `Menu` for action
  lists.

Read [references/patterns.md](references/patterns.md) for complete examples.

## Common Mistakes

### CRITICAL Wrap a disabled control in Tooltip

Wrong:

```tsx
<Tooltip title="Disabled because …">
  <Button isDisabled>Submit</Button>
</Tooltip>
```

Correct:

```tsx
<Button isDisabled>Submit</Button>
<Text variant="bodySmall" color="content.neutral.low">Disabled because …</Text>
```

Unity enforces the WCAG rule that disabled controls must not carry tooltips (the disabled element is not keyboard-focusable, so the tooltip is unreachable). This is enforced at the library level, not just policy.

Source: components/tooltip/Tooltip.tsx

### MEDIUM Use Dialog for a non-blocking hint

Wrong:

```tsx
<DialogTrigger>
  <Button>Info</Button>
  <Dialog>
    <DialogContent>FYI…</DialogContent>
  </Dialog>
</DialogTrigger>
```

Correct:

```tsx
<PopoverTrigger>
  <Button>Info</Button>
  <Popover title="Info">FYI…</Popover>
</PopoverTrigger>
// or, for hover-only:
<Tooltip title="FYI…"><Button>Info</Button></Tooltip>
```

Dialog is modal (focus trap, scroll lock, backdrop) — heavy for a simple informational popover.

Source: components/dialog/Dialog.tsx:162-196 (ModalOverlay)

### MEDIUM Use Tooltip for interactive content

Wrong:

```tsx
<Tooltip
  title={
    <>
      <Button>Delete</Button>
      <Button>Edit</Button>
    </>
  }
>
  <IconButton icon={MoreFilled} />
</Tooltip>
```

Correct:

```tsx
<Menu>
  <MenuTrigger>
    <IconButton icon={MoreFilled} />
  </MenuTrigger>
  <MenuContent>
    <RawMenuItem>Delete</RawMenuItem>
    <RawMenuItem>Edit</RawMenuItem>
  </MenuContent>
</Menu>
```

Tooltip is display-only; placing interactive elements inside breaks focus and event handling.

Source: components/tooltip/Tooltip.tsx (no interactive support); components/menu/Menu.tsx

### MEDIUM Fight the modal focus trap with custom useEffect focus moves

Wrong:

```tsx
useEffect(() => {
  if (open) triggerRef.current?.focus()
}, [open])
```

Correct:

```tsx
// Let Dialog manage focus; use autoFocus on a specific element inside if needed:
<Dialog isOpen={open} onOpenChange={setOpen}>
  <input autoFocus />
</Dialog>
```

Dialog manages focus and restoration via React Aria. Calling triggerRef.focus() inside a useEffect produces unpredictable jumps.

Source: components/dialog/Dialog.tsx:162-196

### MEDIUM PromoDialog without PromoDialogHero

Wrong:

```tsx
<PromoDialog>
  <PromoDialogTitle>Feature</PromoDialogTitle>
  <PromoDialogContent>…</PromoDialogContent>
</PromoDialog>
```

Correct:

```tsx
<PromoDialog>
  <PromoDialogHero>
    <Illustration src={MyIllustration} />
  </PromoDialogHero>
  <PromoDialogTitle>Feature</PromoDialogTitle>
  <PromoDialogContent>…</PromoDialogContent>
</PromoDialog>
```

PromoDialog validates at render and console.errors if the hero is missing; dialog still renders but the layout breaks.

Source: components/promo-dialog/PromoDialog.tsx:230-236 (console.error guard)

## See also

- `unity-migrate-from-midnight` — the disabled+tooltip trap is the most
  common Midnight-era pattern that Unity blocks; the migration skill covers
  the rewrite.
- `unity-layout-and-styling` — overlay content uses the same `uy:` utility
  classes (Flex/Grid, spacing, `uy:data-[hovered=true]:…`).
