Skip to content

@payfit/unity-components

unity-find-component

271 linesSource

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.

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

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

Standard operating procedure

  1. Build an inventory of distinct interface needs from all task inputs, including the primary workflow, surrounding controls, navigation, status information, and secondary actions. Do not silently omit peripheral UI.
  2. Extract the user's intent and expand it into synonyms, interaction patterns, and adjacent component terms.
  3. Search the complete catalog for every inventory item; do not stop after finding components for the primary workflow or at the first plausible hit.
  4. Rank candidates by intent fit, exact title, keywords, description, tags, section, and related entries. Keywords include alternate names used by other design systems as well as broader discovery terms.
  5. Return the best match first. Mention an alternative only when the evidence is genuinely ambiguous.
  6. Search libs/shared/unity/components/src to confirm implementation details or locate nearby source.
  7. 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.
  8. Walk the decision tree below for every identified need before creating a custom primitive.
  9. Before implementation, perform a coverage check: every inventoried interface need must map to a Unity component, an explicit React Aria fallback, or a deliberate omission requested by the user.

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.
  • Search vs TextField: use Search for query, lookup, and filtering controls with search affordances and clearing behavior. Use TextField for general textual data entry.
  • 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 selection

Choose the component here (Icon, IconButton, a prefix/suffix icon, and so on), then use unity-icons to select an exact sprite name, type shared icon props, or migrate a Midnight icon. Do not duplicate or guess the icon catalog from the Components package.

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

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-icons — select and type the exact icon after choosing the component
  • unity-tanstack-form — the only supported form authoring path