---
name: unity-find-component
description: >
  Load before choosing or creating Unity UI. Use it to map a UI need to an
  existing @payfit/unity-components export, then fall back to React Aria plus
  uy: classes only when Unity has no fit.
type: core
library: '@payfit/unity-components'
library_version: '2.x'
sources:
  - 'PayFit/hr-apps:libs/shared/unity/components/src/index.ts'
  - 'PayFit/hr-apps:libs/shared/unity/icons/src/components/icon/parts/IconSprite.tsx'
  - 'PayFit/hr-apps:libs/shared/unity/icons/src/generated/index.ts'
  - 'PayFit/hr-apps:libs/shared/unity/components/OVERVIEW.md'
  - 'PayFit/hr-apps:AGENTS.md'
---

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

## Quick Reference

All exports come from a single entry: `@payfit/unity-components`. There are
no sub-paths for runtime components; the only sub-paths are
`@payfit/unity-components/integrations/tanstack-router` (router-aware
navigation) and `@payfit/unity-components/i18n/<locale>.json` (message
bundles). Names below are grouped by purpose.

### Layout

`Flex`, `FlexItem`, `Grid`, `GridItem`, `Card`, `CardTitle`,
`CardContent`, `SelectableCard...`, `NavigationCard`, `NavigationCardGroup`,
`Page`, `PageHeader`, `PageHeading`, `AppLayout`, `AppMenu`,
`FunnelLayout`, `FunnelPage`, `FunnelBody`, `FunnelSidebar`, `FunnelTopBar`,
`FunnelPageHeader`, `FunnelPageContent`, `FunnelPageFooter`,
`FunnelPageActions`, `FunnelBackButton`.

### Navigation

Router-agnostic (base entry): `RawLink`, `RawNavItem`, `RawBreadcrumbLink`,
`RawPaginationLink`, `RawPaginationPrevious`, `RawPaginationNext`, `RawTab`,
`Nav`, `NavGroup`, `Breadcrumbs`, `Breadcrumb`, `Pagination`,
`PaginationContent`, `PaginationItem`, `PaginationEllipsis`, `Tabs`,
`TabList`, `TabPanel`, `SkipLinks`, `TaskMenu`, `RawTask`, `RawSubTask`,
`TaskGroup`, `ListView`, `RawListViewItem`, `ListViewSection`,
`ListViewItemLabel`, `ListViewItemText`.

Router-aware (from
`@payfit/unity-components/integrations/tanstack-router`): `Link`,
`NavItem`, `BreadcrumbLink`, `PaginationLink`, `Tab`.

### Form fields (Tanstack Form — current)

Bound via `form.AppField` then `field.<Name>`: `TextField`, `SelectField`,
`NumberField`, `CheckboxField`, `CheckboxGroupField`, `DatePickerField`,
`DateRangePickerField`, `MultiSelectField`, `RadioButtonGroupField`,
`SelectableButtonGroupField`, `SelectableCardCheckboxGroupField`,
`SelectableCardRadioGroupField`, `ToggleSwitchField`,
`ToggleSwitchGroupField`, `PasswordField`.

### Form primitives (no form state)

`Input`, `NumberInput`, `Select`, `SelectButton`, `SelectOption`,
`SelectOptionGroup`, `SelectOptionHelper`, `MultiSelect`,
`MultiSelectOption`, `MultiSelectOptGroup`, `Checkbox`, `CheckboxStandalone`,
`CheckboxGroup`, `RadioButtonGroup`, `RadioButton`, `RadioButtonHelper`,
`TextArea`, `ToggleSwitch`, `ToggleSwitchGroup`, `SegmentedButtonGroup`,
`ToggleButton`, `SelectableButtonGroup`, `SelectableButton`, `Search`,
`PhoneNumberInput`, `DatePicker`, `DateCalendar`, `DateRangePicker`,
`DateRangeCalendar`, `Autocomplete`, `AutocompleteItem`,
`AutocompleteItemGroup`, `Fieldset`, `FieldGroup`, `Label`, `FormField`,
`FormControl`, `FormLabel`, `FormHelperText`, `FormFeedbackText`.

### Buttons and actions

`Button`, `IconButton`, `CircularIconButton`, `RawLinkButton`, `Actionable`,
`ActionBar`, `ActionBarRoot`, `ActionBarButton`, `ActionBarIconButton`,
`FloatingActionBar`, `Anchor`.

### Overlays

Modal: `Dialog`, `DialogContent`, `DialogTitle`, `DialogActions`,
`DialogButton`, `PromoDialog`, `PromoDialogHero`, `PromoDialogTitle`,
`PromoDialogSubtitle`, `PromoDialogContent`, `PromoDialogActions`,
`SidePanel`, `SidePanelHeader`, `SidePanelContent`, `SidePanelFooter`,
`BottomSheet`, `BottomSheetHeader`, `BottomSheetContent`,
`BottomSheetFooter`.

Non-modal: `Tooltip`, `DefinitionTooltip`, `Popover`, `Menu`, `MenuTrigger`,
`MenuContent`, `MenuHeader`, `RawMenuItem`, `MenuSeparator`.

### Content and data

`Text`, `Icon`, `Pill`, `Badge`, `Alert`, `AlertTitle`, `AlertContent`,
`AlertActions`, `Avatar`, `AvatarFallback`, `AvatarIcon`, `AvatarImage`,
`AvatarPair`, `DataTable`, `DataTableRoot`, `DataTableBulkActions`,
`Table`, `TableBody`, `TableHeader`, `TableColumnHeader`, `TableRow`,
`TableCell`, `TableEmptyState`, `TablePagination`, `Filter`,
`FilterToolbar`, `Carousel`, `CarouselHeader`, `CarouselContent`,
`CarouselSlide`, `CarouselNav`, `Collapsible`, `CollapsibleTitle`,
`CollapsibleContent`, `Timeline`, `TimelineStep`, `TimelineStepHeader`,
`TimelineStepDescription`.

### Status and loading

`Spinner`, `ProgressBar`, `FullPageLoader`, `EmptyState`, `EmptyStateIcon`,
`EmptyStateContent`, `EmptyStateActions`, `EmptyStateGetStarted`,
`EmptyStateWaitingForData`, `EmptyStateGoodJob`,
`EmptyStateUpgradeRequired`, `EmptyStateNoSearchResults`,
`EmptyStateUseDesktop`, `ErrorState`, `ToastManager`, `toast`.

### Semantic and brand

`PayFitBrand`, `PayFitPreprod`.

## 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 and scheduled for removal after the rebrand. 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)"; maintainer interview

### 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); maintainer interview

## See also

- `unity-setup-feature-plugin` — required before any Unity import will work
- `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
