---
name: unity-find-component
description: >
  Load when selecting a Unity component is unclear or before creating a custom
  UI primitive that may already exist in @payfit/unity-components. Use it to
  find the existing component, then fall back to React Aria plus uy: classes
  only when Unity has no fit.
metadata:
  type: core
  library: '@payfit/unity-components'
  library_version: '2.x'
---

Routing skill for selecting a Unity component before writing UI code. Walk
the catalog, then the decision tree, then the commonly-confused pairs.

## Component catalog

Inspect the canonical [component catalog](../../src/agent-references/component-catalog.json)
before searching component source. It contains exact component ids, titles,
descriptions, keywords, sections, tags, related entries, and Storybook preview
metadata. There is no separate canonical Markdown catalog.

For a targeted lookup, search the full JSON and compare all plausible matches. Use whatever reliable local search tool is available (for example `rg`, a JSON-aware CLI, or a short Node script); do not assume `jq` is installed:

```sh
rg -i -C 2 'dialog|confirmation|modal' \
  libs/shared/unity/components/src/agent-references/component-catalog.json
```

## Standard operating procedure

1. Extract the user's intent and expand it into synonyms, interaction patterns, and adjacent component terms.
2. Search the complete catalog; do not stop at the first slice or plausible hit.
3. Rank candidates by intent fit, description, keywords, tags, section, and related entries.
4. Return the best match first. Mention an alternative only when the evidence is genuinely ambiguous.
5. Search `libs/shared/unity/components/src` to confirm implementation details or locate nearby source.
6. Inspect Storybook stories only for props, API details, or usage examples after a likely component has been identified. When the Storybook MCP is available, use its curated component manifest or matching Storybook entry as a complementary source for rendered examples; the canonical JSON catalog remains the source of truth for discovery.
7. Walk the decision tree below before creating a custom primitive.

## Decision Tree

For every UI need, walk these three levels in order. Stop at the first that
fits.

### Level 1 — Use Unity directly

If a named export covers the use case, import it. No abstractions over the
top.

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

export function ConfirmDelete({
  isOpen,
  onClose,
}: {
  isOpen: boolean
  onClose: () => void
}) {
  return (
    <Dialog isOpen={isOpen} onOpenChange={onClose}>
      <DialogContent>
        <Pill color="danger">Destructive</Pill>
      </DialogContent>
      <DialogActions>
        <Button variant="secondary" onPress={onClose}>
          Cancel
        </Button>
        <Button color="danger" onPress={onClose}>
          Delete
        </Button>
      </DialogActions>
    </Dialog>
  )
}
```

### Level 2 — Fall back to React Aria + uy:\* classes

Build your own primitive only when Unity has no equivalent. Compose React
Aria primitives, style with the `uy:` prefix, and merge classes with
`uyMerge` / `uyTv`.

```tsx
import { uyTv } from '@payfit/unity-themes'
import { ToggleButton } from 'react-aria-components'

const toggleStyles = uyTv({
  base: 'uy:inline-flex uy:items-center uy:gap-100 uy:rounded-100 uy:px-200 uy:py-100',
  variants: {
    isSelected: {
      true: 'uy:bg-surface-action uy:text-content-on-action',
      false: 'uy:bg-surface-secondary uy:text-content-neutral',
    },
  },
})

export function CustomPivot({ label }: { label: string }) {
  return (
    <ToggleButton className={({ isSelected }) => toggleStyles({ isSelected })}>
      {label}
    </ToggleButton>
  )
}
```

### Level 3 — Midnight (last resort, deprecated)

Only when (a) Unity has no equivalent, (b) React Aria + `uy:*` cannot
realistically rebuild it, and (c) the feature ships against a deadline. Open
a follow-up to migrate. Per `AGENTS.md`, Midnight is deprecated; never
import it into a new module.

## Commonly Confused Pairs

- `Badge` vs `Pill`: `Badge` is a numeric/dot indicator anchored to another
  element (notification count). `Pill` is a standalone label/status chip
  with text content.
- `Card` vs `SelectableCard...` vs `NavigationCard`: `Card` is the generic
  container. `SelectableCardCheckboxGroup` / `SelectableCardRadioGroup`
  wrap cards as form inputs. `NavigationCard` wraps a card as a router-aware
  link.
- `Button` vs `IconButton` vs `RawLinkButton`: `Button` for actions with
  text. `IconButton` / `CircularIconButton` for icon-only actions
  (requires `aria-label`). `RawLinkButton` renders as an anchor but styled
  like a button — use when the action navigates.
- `Menu` vs `Popover`: `Menu` is a list of actionable items keyed by
  keyboard (Enter/Arrow). `Popover` is a free-form floating panel and
  requires a `title`.
- `Dialog` vs `PromoDialog`: `Dialog` for confirmation / edit flows.
  `PromoDialog` for marketing / onboarding announcements; requires
  `PromoDialogHero`.
- `Table` vs `DataTable`: `Table` is the layout primitive (header, body,
  rows, cells) with no behavior. `DataTable` wires Tanstack Table for
  sorting, filtering, pagination, virtualization, bulk actions.
- `ErrorState` vs `Alert`: `ErrorState` is a full-area empty-replacement
  for "this section failed to load." `Alert` is an inline banner that
  coexists with surrounding content.

## Icon Source Convention

`Icon` takes a typed `src` prop of type `UnityIcon` — a literal union of
PascalCase names with a `Filled` or `Outlined` suffix (~310 values, from
`@payfit/unity-icons`). Strings outside that union are a type error. Do
not cast to `UnityIcon`; the cast bypasses the sprite-id guard and the
icon silently renders empty.

```tsx
import type { UnityIcon } from '@payfit/unity-icons'

import { Icon } from '@payfit/unity-components'

const icon: UnityIcon = 'MagnifyingGlassOutlined'
;<Icon src={icon} size={20} />
```

## Forms Notice

Tanstack Form (`useTanstackUnityForm`) is the only supported form system.
The React Hook Form path — `useUnityForm` plus the legacy `TextField` /
`SelectField` / `NumberField` etc. RHF wrappers exported from the same
index — is deprecated. Do not author new code with `useUnityForm`. See
`unity-tanstack-form`.

## Common Mistakes

### HIGH Hand-roll component that already exists

Wrong:

```tsx
const Tag = ({ children }) => (
  <span className="uy:rounded-full uy:px-200 uy:py-100 uy:bg-surface-primary">
    {children}
  </span>
)
```

Correct:

```tsx
import { Pill } from '@payfit/unity-components'
;<Pill>{children}</Pill>
```

The hand-rolled span re-derives Pill's tokens by guesswork and drifts from
the design-system source-of-truth on every theme update.

Source: libs/shared/unity/components/src/components/pill/Pill.tsx

### HIGH Use React Aria primitive directly when Unity wraps it

Wrong:

```tsx
import { Button as AriaButton } from 'react-aria-components'
;<AriaButton>Click</AriaButton>
```

Correct:

```tsx
import { Button } from '@payfit/unity-components'
;<Button variant="primary">Click</Button>
```

The bare React Aria Button has no Unity theming, intl, or styling
defaults; you ship an unstyled element with no `uy:*` classes.

Source: libs/shared/unity/components/src/components/button/Button.tsx

### HIGH Reach for Midnight when Unity has an equivalent

Wrong:

```tsx
import { Button, Modal } from '@payfit/midnight'
```

Correct:

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

Midnight is deprecated; new screens that import it cannot match the Unity
theme tokens and will require a migration pass later anyway.

Source: AGENTS.md "Do NOT use (deprecated)"

### MEDIUM Combine Input + FormField when \*Field exists

Wrong:

```tsx
<FormField label="Name" error={errors.name}>
  <Input {...register('name')} />
</FormField>
```

Correct:

```tsx
<form.AppField name="name">
  {field => <field.TextField label="Name" />}
</form.AppField>
```

Manual `FormField` + `Input` skips the label-for/aria-describedby wiring,
the `field.state.meta` error plumbing, and the required-state inference
that the `*Field` components handle.

Source: libs/shared/unity/components/src/components/form-field/FormField.tsx; index.ts:205-223

### HIGH Pass an untyped string to Icon src and guess the name

Wrong:

```tsx
<Icon src="search" size={20} />
<Icon src="trash-filled" />
const name: string = 'trash'
<Icon src={name as UnityIcon} />
```

Correct:

```tsx
import { Icon } from '@payfit/unity-components'
import type { UnityIcon } from '@payfit/unity-icons'
<Icon src="MagnifyingGlassOutlined" size={20} />
<Icon src="TrashFilled" />
type Props = { icon: UnityIcon }
```

The `UnityIcon` literal union encodes the exact sprite ids; lowercase or
kebab-case strings have no matching `<symbol id>` in the injected sprite,
so `<use href="#search">` resolves to nothing and the SVG renders empty.

Source: libs/shared/unity/icons/src/components/icon/parts/IconSprite.tsx; generated/index.ts (UnityIcon type)

## See also

- For projects not yet configured for Unity, use the repository's
  `@payfit/nx-tools:setup-unity` generator
- `unity-migrate-from-midnight` — when Level 3 fallback hits a Midnight
  screen, follow this skill to replace it
- `unity-tanstack-form` — the only supported form authoring path
