unity-overlays
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.
This skill targets the current workspace Unity v3 API. Legacy examples are migration context.
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.
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 for complete examples.
Common Mistakes
CRITICAL Wrap a disabled control in Tooltip
Wrong:
<Tooltip title="Disabled because …">
<Button isDisabled>Submit</Button>
</Tooltip>Correct:
<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: src/core/components/tooltip/Tooltip.tsx
MEDIUM Use Dialog for a non-blocking hint
Wrong:
<DialogTrigger>
<Button>Info</Button>
<Dialog>
<DialogContent>FYI…</DialogContent>
</Dialog>
</DialogTrigger>Correct:
<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: src/core/components/dialog/Dialog.tsx (ModalOverlay)
MEDIUM Use Tooltip for interactive content
Wrong:
<Tooltip
title={
<>
<Button>Delete</Button>
<Button>Edit</Button>
</>
}
>
<IconButton icon={MoreFilled} />
</Tooltip>Correct:
<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: src/core/components/tooltip/Tooltip.tsx (no interactive support); src/core/components/menu/Menu.tsx
MEDIUM Fight the modal focus trap with custom useEffect focus moves
Wrong:
useEffect(() => {
if (open) triggerRef.current?.focus()
}, [open])Correct:
// 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: src/core/components/dialog/Dialog.tsx
MEDIUM PromoDialog without PromoDialogHero
Wrong:
<PromoDialog>
<PromoDialogTitle>Feature</PromoDialogTitle>
<PromoDialogContent>…</PromoDialogContent>
</PromoDialog>Correct:
<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: src/core/components/promo-dialog/PromoDialog.tsx (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 — compose overlay content with Flex and Grid.
- unity-themes — apply spacing and component-owned data-* state styles.
- unity-illustrations — select and load the artwork required by PromoDialogHero.