BuildWithMatija
  1. Home
  2. Blog
  3. Payload
  4. Payload CMS ui Field Type: Build Live Previews (Guide)

Payload CMS ui Field Type: Build Live Previews (Guide)

How to mount client/server React components with Payload's ui field to create live admin previews, debounced…

2nd September 2026·Updated on:14th September 2026··
Payload
Payload CMS ui Field Type: Build Live Previews (Guide)

Evaluating Payload CMS Implementation Costs?

Scope design, content structure, and migration hours to estimate a realistic production timeline and hosting setup.

Try the Cost EstimatorGet a Second Opinion

📚 Comprehensive Payload CMS Guides

Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Contents

  • The use case: a live "who does this affect" preview
  • Why `ui` and not something else
  • Building it: the server/client pair
  • Wiring the field into the collection config
  • The server half
  • The client half: where `@payloadcms/ui` does the real work
  • The endpoint it calls
  • Full reference: every `ui` field option
  • Passing configuration through `custom`, not hardcoded imports
  • FAQ
  • Wrapping up
On this page:
  • The use case: a live "who does this affect" preview
  • Why `ui` and not something else
  • Building it: the server/client pair
  • Full reference: every `ui` field option
  • FAQ
Build with Matija logo

Build with Matija

Senior-led B2B websites, applications, content systems, and digital infrastructure. Business-first, full-stack, AI-assisted, no handoffs.

Services

  • B2B Website Development
  • CMS Architecture Review & Platform Blueprint
  • Next.js + Payload Advisory
  • AI Integration & Implementation

Resources

  • CMS Hub
  • B2B Website Strategy
  • E-commerce Hub
  • Blog
  • Case Studies

Payload CMS

  • Payload CMS Developer
  • Payload CMS Migration
  • Payload CMS Demos
  • All Payload CMS Resources

Discuss your project

Planning a rebuild, migration, application, workflow change, or platform decision? Start with the business problem and the system behind it.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

If you need to show something in a Payload CMS admin form that isn't really "data" — a live computed preview, a warning banner, an action panel, a read-only summary pulled from another collection — the ui field type is the tool for it. Unlike every other field type, type: "ui" stores nothing in the database. It exists purely as a mount point in the form schema where you drop in your own React component, and from there you have the full run of Payload's client-side form state and its native @payloadcms/ui component library to build with.

I recently needed exactly this: an admin editing a permission rule needed to see, live, which actual users that rule would apply to — before saving, while still adjusting the department/role/user selectors. That's not a field with a value. It's a computed view over other fields plus a database query. This guide walks through that real implementation, then gives you the complete reference of every ui field option so you can build your own.

The use case: a live "who does this affect" preview

I was working on a governance system inside a multi-tenant Payload 3 app — a collection-access-rules collection where each document defines an access rule: who it targets (departments, sub-departments, roles, named users) and what it grants or denies (read, create, update, delete, and so on). The targeting logic already existed at runtime: a function called governanceRuleMatches decides, for a given user, whether a given rule applies to them.

The problem was that an admin building a new rule had no way to see who it actually affected until after saving it and testing it against a real login. Selecting "Department: Marketing, Role: Editor" is abstract. Seeing "this currently matches 6 people: alice@…, bob@…" is concrete, and it catches mistakes — an empty selector that "matches broadly" instead of narrowly, a role that doesn't have anyone assigned to it yet, a department typo — before the rule is ever saved.

So the requirement was: as the admin changes the department/role/sub-department/user fields in the form, query the real users collection, run the exact same matching function the runtime uses, and render the result — live, without saving, without a page reload.

That's not something an array field, a relationship field, or a group field can do. None of them execute arbitrary logic against other collections and render a custom result. That's precisely what the ui field type is for.

Why ui and not something else

Before reaching for ui, it's worth being clear on what makes it different from Payload's other customization points, because there's real overlap and it's easy to reach for the wrong one.

ApproachWhat it's forStores data?When to use it instead
type: "ui" fieldMount an arbitrary custom component anywhere in the Edit form, with access to full form stateNoYou need a computed view, a live preview, an action panel, or any UI that depends on other fields but has no value of its own
Custom Field component on a real fieldReplace how an existing field's value is rendered and editedYes (the field's own value)You want a better input experience for data that's actually being saved — see my guide to building custom Field components for the segmented-toggle-matrix example
Custom admin view (admin.components.views)Replace or add an entire route in the admin panel (a list view, a whole edit view, a dashboard page)N/A — page-level, not field-levelYou need something bigger than one field's worth of space, or something that isn't attached to a single document at all
virtual fieldCompute and store a value derived from other fields or relationships, evaluated server-side, saved as real field dataYes (derived, but persisted)You want the computed value to exist in the database and API response — e.g. for filtering, sorting, or exposing it over REST
Field-level afterRead hookTransform a field's value when a document is readNo new storage, transforms existing dataYou need to reshape a stored value for API consumers, not for a live in-form preview

The deciding question is simple: does this need to be a value in the document, or is it just something the admin needs to see while editing? If it's the latter, you want ui.

Building it: the server/client pair

Every custom Field component in Payload — ui fields included — follows a two-file convention once you're doing anything non-trivial: a Server Component that reads the field config and any request-scoped data, and a Client Component that owns the interactive part. This isn't strictly required (you can write a single client component and skip the server half), but splitting them keeps your client bundle small and gives you a clean place to do server-only work — reading payload.config, hitting the database directly, checking req.user — without shipping any of that to the browser.

Wiring the field into the collection config

ts
// File: src/payload/collections/governance/access/CollectionAccessRules/fields.ts
{
  name: "matchingUsersPreview",
  type: "ui",
  admin: {
    components: {
      Field:
        "/src/payload/admin-components/matching-users-preview/MatchingUsersPreviewField#MatchingUsersPreviewField",
    },
  },
},

That's the entire field definition. No label, no required, no defaultValue — none of that applies, because there's no value. The only thing that matters is admin.components.Field, a string in the form "<path-from-repo-root>#<exported-function-name>" that tells Payload which component to render at this position in the form. I placed this field directly after the department/sub-department/role/user selector fields in the tab, so it renders immediately below them — field order in the array is layout order in the form, exactly like any other field.

The server half

tsx
// File: src/payload/admin-components/matching-users-preview/MatchingUsersPreviewField.tsx
import type { UIFieldServerProps } from "payload";

import { MatchingUsersPreviewFieldClient } from "./MatchingUsersPreviewFieldClient";

export async function MatchingUsersPreviewField({
  path,
}: UIFieldServerProps) {
  return <MatchingUsersPreviewFieldClient path={path} />;
}

There's barely anything here, and that's the point — this preview's data doesn't exist server-side at render time. The selector the admin is building (which departments, which roles) only lives in unsaved client form state, so there's nothing to precompute on the server for this particular field. All I need from the server is path, the dot-notation location of this field within the form (e.g. matchingUsersPreview), which I forward to the client component so it can register itself correctly with Payload's form machinery.

Contrast this with a ui field that does need server data — say, a field that shows "eligible field names for this collection" computed by inspecting the sanitized Payload config. That server component would do real work: call into payload.config, filter a schema, and hand the client a plain, serializable prop. UIFieldServerProps gives you field, payload, req, user, and more for exactly that case — the point is you only pay for what you use, and here I didn't need any of it beyond path.

The client half: where @payloadcms/ui does the real work

This is where the ui field type stops being empty and becomes powerful. Because the field has no value of its own, it isn't really "a field" from the client component's point of view — it's just a React component rendered inside Payload's FormProvider. That means every hook and every component in @payloadcms/ui is available to it, the same as it would be to any other custom Field.

tsx
// File: src/payload/admin-components/matching-users-preview/MatchingUsersPreviewFieldClient.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { Banner, FieldLabel, Pill, useFormFields } from "@payloadcms/ui";

export function MatchingUsersPreviewFieldClient({ path }: { path: string }) {
  const site = useFormFields(([fields]) => fields.site?.value);
  const departments = useFormFields(([fields]) => fields.departments?.value);
  const subDepartments = useFormFields(([fields]) => fields.subDepartments?.value);
  const roles = useFormFields(([fields]) => fields.roles?.value);
  const users = useFormFields(([fields]) => fields.users?.value);

  const siteId = typeof site === "string" || typeof site === "number" ? site : null;
  const [fetchState, setFetchState] = useState(null);
  const requestId = useRef(0);

  useEffect(() => {
    if (siteId == null) {
      requestId.current += 1;
      return;
    }
    const currentRequest = ++requestId.current;
    const timer = setTimeout(async () => {
      setFetchState({ kind: "loading" });
      const params = new URLSearchParams({ site: String(siteId) });
      // ...append departments/subDepartments/roles/users as CSV params
      const res = await fetch(
        `/api/collection-access-rules/preview-matching-users?${params}`,
        { credentials: "include" },
      );
      const data = await res.json();
      if (currentRequest === requestId.current) {
        setFetchState({ kind: "ready", data });
      }
    }, 300);
    return () => clearTimeout(timer);
  }, [siteId, departments, subDepartments, roles, users]);

  // ...render Banner / loading text / Pill list based on fetchState
}

Three things are doing the actual work here, and they're worth calling out individually because they're the pattern you'll reuse in every ui field you build.

useFormFields reads sibling field values reactively. This is the hook that makes a ui field aware of the rest of the form. I'm not reading departments because this field owns it — I'm reading it because it's a sibling field elsewhere in the same document, and I want my component to re-render whenever an admin changes it. The selector pattern useFormFields(([fields]) => fields.someName?.value) subscribes narrowly to just that one field's value, so changing an unrelated field elsewhere in the form doesn't trigger a re-render here. If you need several sibling values, call the hook once per field rather than trying to select an object — Payload's docs are explicit that pulling a single field is the performant path, since selecting a whole slice of state causes broader re-renders every time any field in that slice changes.

A debounced fetch calls a real REST endpoint, not the Local API. A ui field's client component runs in the browser, so it can't call payload.find() directly — that's server-only. Instead I added a small custom endpoints entry to the same collection config, which the client hits over HTTP. I cover that below; the point here is that "live preview" almost always means "client component calling an endpoint," and the debounce (setTimeout plus a requestId ref to discard stale in-flight responses) exists purely so five quick relationship-field changes in a row produce one request, not five racing ones.

The render itself is built entirely from @payloadcms/ui primitives — Banner for the "select a Site first" and error states, Pill for each matched user, FieldLabel for the heading. None of this is custom-styled HTML. Payload ships its actual admin-panel component library at the flat @payloadcms/ui import — the same Button, Banner, Pill, Collapsible, FieldLabel, FieldDescription, TextInput, and ReactSelect components the built-in fields themselves are built from. I wrote about the full inventory of that package in more depth in my guide to Payload's custom admin components; the short version for this article is that a ui field is the ideal place to use them, because you have an entire empty canvas and no default rendering to fight against or override.

The endpoint it calls

For completeness, here's the shape of the endpoint side, registered directly on the collection:

ts
// File: src/payload/collections/governance/access/CollectionAccessRules/index.ts
export const CollectionAccessRules: CollectionConfig = {
  slug: "collection-access-rules",
  endpoints: [previewMatchingUsersEndpoint],
  // ...
};
ts
// File: .../CollectionAccessRules/endpoints/preview-matching-users.ts
export const previewMatchingUsersEndpoint: Endpoint = {
  path: "/preview-matching-users",
  method: "get",
  handler: async (req) => {
    // parse site/departments/subDepartments/roles/users from the query string
    // build a draft selector with the same toSelectorRule() the runtime uses
    // payload.find() every user on that Site
    // filter with governanceRuleMatches() — the exact predicate saved rules are evaluated with
    // return { matchedCount, totalSiteUsers, users, truncated }
  },
};

The detail that matters most here isn't the endpoint plumbing — it's that the preview reuses the exact same matching function the runtime access-control layer evaluates saved rules with. That's a deliberate design choice worth calling out on its own: a preview that reimplements its own approximate version of the real logic is worse than no preview at all, because it can show a result that doesn't match what actually happens once the rule is saved. Building the preview as a thin UI wrapper around the real predicate function is what makes it trustworthy.

Full reference: every ui field option

Here's the complete set of configuration a ui field accepts, as of Payload 3.88.0. Most of it lives under admin, since — again — there's no "value" half of the field to configure.

PropertyTypePurpose
namestringRequired. Used as the field's key/path in the form schema — this is what path resolves to in your component, and what other fields' condition functions would reference if they needed to. Not saved anywhere.
type"ui"Required. Marks this as a non-data field.
labelstring \| Record<string, string>Optional label, usable by your component (or a FieldLabel you render) — Payload doesn't auto-render one for you the way it does for data fields.
customRecord<string, any>Server-only extension point. Use this to pass configuration into your server component without hardcoding it — see the "generalizing a field" pattern below.
admin.components.FieldPayloadComponentThe component actually rendered at this position. This is the one property that matters for almost every ui field.
admin.components.CellPayloadComponentWhat renders in the List View table if you add this field to defaultColumns. Rare for ui fields, but available — useful for a computed status badge in a list row.
admin.components.Description / Diff / FilterPayloadComponentSame override points every field type gets. Filter lets a ui field participate in the List View's filter dropdown if you build one; uncommon but not disallowed.
admin.condition(data, siblingData) => booleanShow or hide the field based on other fields' values — runs client- and server-side. This is how you'd hide a preview panel until a prerequisite field (like site, in my case) is filled in, as an alternative to handling the empty state inside the component itself.
admin.customRecord<string, any>Like top-level custom, but available on both server and client — use this instead of the top-level one if your client component also needs the config value, not just the server component.
admin.positionstringSet to "sidebar" to render in the document sidebar instead of the main form body. I used this for a related "conflict preview" panel on the same collection so it stays visible while scrolling the main tabs.
admin.widthCSSProperties["width"]Same width control every field gets inside a row layout.
admin.disableBulkEditboolean, default true for ui fieldsWhether this field appears in the bulk-edit field picker. Since it has no value, you almost always want the default.
admin.disableListColumn / disableListFilter / disableGroupBybooleanControl whether the field shows up as a List View column/filter/group-by option — relevant mainly if you've also given it a Cell component.

Passing configuration through custom, not hardcoded imports

One mistake I made on the first pass of a related component — a segmented Inherit/Allow/Deny permissions matrix built the same way, as a custom Field on a group field rather than a ui field — was hardcoding its list of actions by importing a constant from one specific collection's field file. That worked until I needed the identical matrix on a second collection with a different, shorter action list. The fix was field.custom:

ts
// File: .../FieldAccessRules/fields/permissions.ts
{
  name: "permissions",
  type: "group",
  admin: {
    components: { Field: ".../PermissionsMatrixField#PermissionsMatrixField" },
  },
  custom: { actions: ["read", "edit"] },
  fields: [ /* ... */ ],
}
tsx
// File: .../PermissionsMatrixField.tsx (server)
export async function PermissionsMatrixField({ field, path }: GroupFieldServerProps) {
  const actions = (field.custom as { actions?: string[] })?.actions ?? [];
  return <PermissionsMatrixFieldClient actions={actions} path={path} />;
}

The same technique applies directly to ui fields. If you're building a preview panel you expect to reuse — across collections, or in more than one place on the same collection — read its configuration from field.custom in the server component and pass it down as a prop, rather than importing collection-specific constants into a component meant to be generic. It's the same discipline as any other reusable component: configuration in, not assumptions baked in.

FAQ

Can a ui field have validation or be required? No. Since it holds no value, required and validate don't apply to it — there's nothing to validate. Any validation your preview logic implies (like "this rule needs at least one matching user") has to live on a real field elsewhere, or in a beforeValidate collection hook.

Does a ui field show up in the API response or generated TypeScript types? No. It's excluded from the document shape entirely — it's a schema/form-only construct. If you need the computed value available over the API (not just visible in the admin), you want a virtual field instead, which does get persisted and returned.

Can I use the Local API (payload.find) directly inside a ui field's client component? Not from the browser — the Local API is server-only. Your client component has to call a REST endpoint (either a built-in Payload collection endpoint or a custom one you register via endpoints on the collection config) or a Next.js Route Handler. Server Components rendered as part of the admin panel can use the Local API directly, which is why splitting server/client and doing as much as possible on the server side is worth it when the data doesn't depend on live, unsaved form state.

When should I use admin.position: "sidebar" versus leaving it in the main form? Sidebar is for anything that should stay visible regardless of which tab or section the admin has scrolled to — status indicators, quick actions, short always-relevant summaries. A live preview tied to specific fields elsewhere in the main form (like mine, which depends on the department/role selectors sitting right above it) usually reads better placed directly beneath those fields, so the relationship between "what you changed" and "what changed as a result" is spatially obvious.

Do I need the server/client split, or can I write one client-only component? You can skip the server half and point admin.components.Field straight at a "use client" component if it needs nothing server-side. I use the split habitually because it's cheap and keeps the door open — the moment a ui field needs anything from payload, req, or the sanitized config, you'll want that logic on the server rather than fetched over an extra network round trip.

Wrapping up

The ui field type is Payload's answer to "I need something in this form that isn't a value" — a live computed preview, a status panel, an action row, anything that reads from other fields or other collections but has nothing of its own to save. The field config itself is close to empty; almost everything happens in the component you point admin.components.Field at, and because that component renders inside Payload's own FormProvider, you get full access to useFormFields, useField, and the entire native @payloadcms/ui component set — the same building blocks Payload's own fields are made of. Reuse the runtime logic your preview is standing in for (don't reimplement an approximate version of it), read configuration through custom instead of hardcoding it if you expect to reuse the component, and reach for virtual fields instead the moment you need the computed value to actually persist.

Let me know in the comments if you have questions, and subscribe for more practical Payload CMS guides.

Thanks, Matija