---
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.
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

### Dialog with DialogTrigger + DialogActions

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

export function ArchiveDialog({ onArchive }: { onArchive: () => void }) {
  return (
    <DialogTrigger>
      <Button>Archive</Button>
      <Dialog size="md">
        <DialogTitle>Archive this employee?</DialogTitle>
        <DialogContent>
          They will no longer appear in active lists. You can restore them
          later.
        </DialogContent>
        <DialogActions>
          <DialogButton variant="close">Cancel</DialogButton>
          <DialogButton variant="confirm" onPress={onArchive}>
            Archive
          </DialogButton>
        </DialogActions>
      </Dialog>
    </DialogTrigger>
  )
}
```

### SidePanel for detail views

```tsx
import { useState } from 'react'

import {
  Button,
  SidePanel,
  SidePanelContent,
  SidePanelFooter,
  SidePanelHeader,
} from '@payfit/unity-components'

export function EmployeeDetailPanel({ employee }: { employee: Employee }) {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <>
      <Button onPress={() => setIsOpen(true)}>View details</Button>
      <SidePanel isOpen={isOpen} onOpenChange={setIsOpen}>
        <SidePanelHeader>{employee.name}</SidePanelHeader>
        <SidePanelContent>
          <p>{employee.position}</p>
          <p>{employee.team}</p>
        </SidePanelContent>
        <SidePanelFooter>
          <Button variant="secondary" onPress={() => setIsOpen(false)}>
            Close
          </Button>
        </SidePanelFooter>
      </SidePanel>
    </>
  )
}
```

### Popover for interactive informational content

`title` is required (use `isTitleSrOnly` if you don't want it visible).
Children may be focusable; the Popover is non-modal so the page stays
interactive behind it.

```tsx
import { Button, Popover, PopoverTrigger } from '@payfit/unity-components'

export function HelpPopover() {
  return (
    <PopoverTrigger>
      <Button variant="tertiary">What is a working agreement?</Button>
      <Popover title="Working agreement">
        <p>
          A working agreement defines when, where and how an employee works. See
          the <a href="/handbook/working-agreement">handbook</a> for details.
        </p>
      </Popover>
    </PopoverTrigger>
  )
}
```

### Menu for action lists

`Menu` expects exactly two children: a trigger and a content block. Use
`MenuTrigger` / `MenuContent` / `RawMenuItem` for the standard composition.

```tsx
import {
  IconButton,
  Menu,
  MenuContent,
  MenuTrigger,
  RawMenuItem,
} from '@payfit/unity-components'

export function RowActions({
  onEdit,
  onArchive,
}: {
  onEdit: () => void
  onArchive: () => void
}) {
  return (
    <Menu>
      <MenuTrigger>
        <IconButton icon="MoreFilled" title="Actions" />
      </MenuTrigger>
      <MenuContent>
        <RawMenuItem onAction={onEdit}>Edit</RawMenuItem>
        <RawMenuItem onAction={onArchive}>Archive</RawMenuItem>
      </MenuContent>
    </Menu>
  )
}
```

## 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; maintainer interview (enforced by Unity)

### 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]:…`).
