Skip to content

@payfit/unity-components

unity-overlays

233 linesSource

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.

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>
  )
}
NeedComponentReason
Confirmation, blocking form, decision requiredDialogmodal: focus trap, scroll lock, backdrop, Escape closes
Detail view that should not interrupt page flowSidePanelmodal but side-anchored; same focus trap and Escape behavior
Mobile-first sheet, edit on small screensBottomSheetmodal anchored to viewport bottom
Feature announcement with hero illustrationPromoDialogmodal with required PromoDialogHero slot
Hover/focus hint on an enabled controlTooltipnon-interactive, hover/focus activated, single string
Interactive informational content (links, fields)Popovernon-modal, focusable content, requires title
Dropdown list of actionsMenu (+ MenuTrigger/MenuContent/RawMenuItem)non-modal, keyboard-navigable list
Inline term definitionDefinitionTooltipnon-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:

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: src/core/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: src/core/components/dialog/Dialog.tsx (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: 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:

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: src/core/components/dialog/Dialog.tsx

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