unity-migrate-from-midnight
Load when replacing @payfit/midnight UI with Unity. Use it to choose Unity equivalents, avoid obsolete Midnight patterns, and route unclear component mappings to unity-find-component.
This skill targets the current workspace Unity v3 API. Legacy examples are migration context.
Convert a legacy @payfit/midnight screen to Unity. The table below covers the highest-traffic mappings; the exhaustive ~85-row map lives in references/midnight-component-map.md. When a Midnight component is not in either, fall through to unity-find-component.
Equivalency Map (high-traffic)
| Midnight | Unity | Notes |
|---|---|---|
| Button | Button | Prop renames apply (see below) |
| Box | Flex (or Grid for 2D) | No 1:1 layout box; use Flex with direction/gap/align |
| Modal | Dialog + DialogContent + DialogActions | Use PromoDialog for marketing dialogs (requires PromoDialogHero) |
| Link | RawLink (base) or Link (tanstack-router) | Pick the router-aware one when navigating inside the app |
| Heading | Text with variant="heading*" | No dedicated Heading; typography is a Text variant |
| Badge | Badge | color → variant; Unity Badge is the chip with intent colors |
| Pill | Pill | Unity Pill is numeric-only with fewer colors; same indicator intent |
| transition('smooth') | uy:transition-* / uy:duration-* classes | No JS helper; use TailwindCSS utilities |
For everything else, see references/midnight-component-map.md. When a target is unclear, switch to unity-find-component rather than guessing. Do not invent Unity component names from Midnight ones.
Prop renames
Confirmed renames from Button.tsx and the React Aria base classes Unity extends:
| Midnight | Unity | Mechanism |
|---|---|---|
| visual="danger" | color="danger" | Color/intent prop renamed on Button, Pill, Alert |
| onClick={fn} | onPress={fn} | React Aria press events (touch/keyboard parity) |
| disabled | isDisabled | React Aria boolean convention |
Do not auto-extend this table. If a prop is not listed, look at the Unity component source — many props are unchanged and adding speculative renames produces silent default behavior.
Accessibility deltas
Unity enforces several WCAG rules that Midnight allowed:
- Tooltip on disabled control — rejected at the library level. Tooltip ignores or removes the trigger when the wrapped control is disabled, because disabled controls are not focusable and therefore the tooltip is unreachable by keyboard or screen reader. See the ## Common Mistakes entry below for the replacement.
- Dialog focus management. Unity Dialog traps focus, restores it on close, and requires an accessible title. Midnight Modal was looser about both.
- Autocomplete keyboard navigation. Unity Autocomplete follows the ARIA combobox pattern (Arrow keys, Home/End, Escape to dismiss). If Midnight code relied on click-only interactions, those still work, but do not strip the keyboard handlers when porting.
Forms
Midnight-era screens commonly use React Hook Form. When porting, replace the form with useTanstackUnityForm and the Tanstack-bound *Field components (TextField, SelectField, NumberField, etc.). The legacy RHF useUnityForm + RHF *Field wrappers were removed in Unity v3. Import useTanstackUnityForm from @payfit/unity-components/forms. Do not pause on the RHF intermediate step. See unity-tanstack-form.
Common Mistakes
CRITICAL Wrap disabled button in Tooltip (Midnight-era pattern)
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>
// Or, if you must explain inline, gate the tooltip on enabled state:
{isDisabled
? <Button isDisabled>Submit</Button>
: <Tooltip title="…"><Button>Submit</Button></Tooltip>}Midnight allowed Tooltip on disabled buttons; Unity enforces the WCAG rule that disabled controls must not have tooltips because they are not keyboard-focusable, so the tooltip is unreachable — Unity's Tooltip component rejects this pairing at the library level.
Source: libs/shared/unity/components/src/core/components/tooltip/Tooltip.tsx
HIGH Assume Midnight props map 1:1 to Unity
Wrong:
<Button visual="danger" onClick={handleDelete}>
Delete
</Button>Correct:
<Button color="danger" onPress={handleDelete}>
Delete
</Button>visual is the Midnight intent prop and color is the Unity one; onClick still type-checks on the underlying DOM node but bypasses React Aria's touch and keyboard press handling, so keyboard activations silently no-op.
Source: libs/shared/unity/components/src/core/components/button/Button.tsx
MEDIUM Leave Midnight + Unity side-by-side in the same screen
Wrong:
import { Button } from '@payfit/midnight'
import { Dialog } from '@payfit/unity-components'Correct:
import { Button, Dialog } from '@payfit/unity-components'Midnight and Unity ship divergent theme tokens — colors, spacing, and typography differ — so a mixed screen renders with mismatched scales and the visual regression lands silently because both stylesheets are valid.
Source: AGENTS.md migration awareness
MEDIUM Port Midnight transition utility instead of using uy: classes
Wrong:
import { transition } from '@payfit/midnight'
<Flex transition={transition('smooth')}>Correct:
<Flex className="uy:transition-all uy:duration-200 uy:ease-out">Unity has no JS transition helper; the migration target is TailwindCSS 4 transition utilities under the uy: prefix, and porting the Midnight function leaves a dead import that the new theme tokens never honor.
Source: no Unity equivalent for Midnight transition helpers
See also
- references/midnight-component-map.md — exhaustive Midnight → Unity map (v1 + v2 surfaces, ~85 rows, grouped by category)
- unity-find-component — decision tree for any Midnight component still ambiguous after consulting the full map
- unity-overlays — full a11y constraints for Dialog, Tooltip, Popover, Menu
- unity-tanstack-form — replacement path for RHF forms in the migrated screen
- unity-icons — select and type replacements for Midnight icons
- unity-illustrations — select, load, and describe replacement artwork