Payload CMS lets you replace a group field's default rendering with a fully custom component through admin.components.Field, and the underlying schema stays exactly as Payload validates and stores it. The pattern splits into two pieces: a server component that reads the field config and forwards only serializable props, and a "use client" component that owns every interactive useField() binding. To keep that client component reusable across more than one collection, pass the per-collection configuration through field.custom on the field definition, so the component reads its action list as a prop, with no import tying it to one specific collection's file. If the component renders one row per item in that list, give each row its own subcomponent, since useField() can't run inside a .map() in the parent without becoming a hook inside a loop.
I recently built exactly this for a client's Payload 3.88 admin panel: a seven-row segmented Inherit/Allow/Deny toggle that replaced seven separate dropdowns, then extended the same component to a second collection the same afternoon by moving its action list into field.custom.
The problem with a default-rendered group field
The client project has a governance and access-control system: editors set permissions like read, create, update, delete, submit, publish, and translate on a rule, and each one resolves to inherit, allow, or deny. The schema for that was a Payload group field with seven select sub-fields, each offering the same three options. That schema was correct from the start — validation worked, defaults worked, the data shape was exactly what the access-control logic downstream expected.
The rendering was the problem. Payload's default admin UI for a group field with seven select sub-fields is seven separate dropdown menus, stacked in declaration order. Setting a rule meant opening each dropdown individually. Reading a rule back meant scanning seven dropdown labels one at a time, with no way to take in the whole decision at a glance. That's exactly how the default group renderer handles an arbitrary set of sub-fields. For a small, fixed set of options repeated across every row, the result is a mismatch with what an editor actually needs to see.
The goal was a compact matrix: one row per action, a three-way segmented toggle per row, built from @payloadcms/ui primitives so it looks like a native part of the Payload admin panel.
The server/client split for a custom Field component
Payload's custom field components come in a server half and a client half. The server component receives the field's config directly — field, path, readOnly — and its job is to pull out whatever's needed and hand it to a client component as plain, serializable props. All the interactive work, including every useField() call, has to happen in a "use client" component, since useField() is a React hook tied to the admin panel's client-side form state.
Nothing in this component talks to the database, and it doesn't need to. There's no lookup to precompute — the action list is fixed per collection, so the whole job is narrowing field.label, field.admin?.description, and field.custom down to plain strings and arrays, then passing them along. That last part, reading actions out of field.custom, is what makes the whole pattern reusable — more on that below.
If you want the general reference for what's available in @payloadcms/ui beyond this — useField, ReactSelect, Popup, admin hooks, and the rest — this guide to building custom admin fields and views covers that ground. This article stays narrow: one specific override pattern for a group field, and how to make it work across collections.
Passing per-collection config through field.custom
The first version of this component imported its action list and decision type directly from the collection's own fields.ts file, which meant it was hardwired to that one collection's exact seven actions. That was a reasonable place to start — the component only had one caller.
A second collection in the same project needed the same segmented-toggle UI for a different, shorter list: just read and edit. Duplicating the component for a two-action list would have meant two components to maintain for one UI pattern. The fix was moving the action list out of the client component's imports and into field.custom on each field's config, then reading it server-side:
field.custom is an arbitrary metadata bag Payload attaches to a field's config and makes available to that field's components. It's the mechanism for getting a value across the server/client boundary without a client component importing from a specific collection's file — the client component just receives actions as a prop and renders whatever it's given. The same PermissionsMatrixField component now backs both collections: one passes a seven-item list, the other passes two.
Why each row needs its own component
The client component renders one row per action. The natural first instinct is a .map() inside the component, calling useField() once per row. That breaks React's Rules of Hooks — the number of hook calls a component makes has to stay identical between renders, and mapping over a dynamic array inside one component body doesn't guarantee that.
The fix is giving each row its own component, so each one calls useField() exactly once, for its own dot-path:
PermissionsMatrixFieldClient maps over actions to decide how many rows to render, and each PermissionsMatrixRow is its own component instance with its own useField() call at ${path}.${action} — the dot-notation path Payload's form state expects for a nested sub-field. Clicking a button calls setValue(decision) directly on that row's field. disabled={readOnly} on the Button threads the read-only state from the server component all the way down to each individual toggle.
Theming with Payload's CSS variables
The layout and color accents live in a small CSS module, built entirely on Payload's own theme variables:
--theme-success-500 marks a selected "allow," --theme-error-500 marks "deny," and --theme-elevation-500 marks the neutral "inherit" state. The chrome — borders, row backgrounds, text — pulls from the same --theme-elevation-* scale Payload's own components use. Every color comes from a theme variable on Payload's own scale, so the matrix follows the admin panel's light and dark mode automatically, with no separate dark-mode styles to maintain.
Wiring it in without touching the schema
The only edit to the collection's field definition is the admin.components.Field pointer and the custom.actions array shown earlier. The seven select sub-fields inside the group stay exactly as they were, including their defaultValue: "inherit". That's a deliberate property of this pattern: the custom component overrides rendering, and Payload's own schema, validation, and default-value handling keep doing their job underneath it. useField() in each row just reads whatever value is already there — on a new document, that's the schema's default, with no extra logic needed in the component to make "inherit" show up correctly.
From one collection to two, the same afternoon
The commit history for this feature is a clean before-and-after. The first commit created all three files in the form shown above, with one exception: the client component imported its action list and decision type directly from the first collection's fields.ts, with no actions prop yet in place. It worked, and it was scoped to exactly one collection.
About six hours later, a second collection needed the same three-way toggle for a shorter, two-item action list. A follow-up commit avoided duplicating the component: it extracted the decision type into a shared src/payload/access/types.ts constant, moved the action list into field.custom.actions as shown above, and rewired the second collection's permissions group to use the same PermissionsMatrixField component with its own two-action list and its own explanatory copy. The commit message states the reasoning plainly: reuse the same segmented-toggle UI for the second collection's field-level permissions group, without building and maintaining a second bespoke matrix component.
That's a fairly ordinary sequence for real projects — build the specific version first, generalize it once a second real use case shows up — and the field.custom pattern is what kept the generalization to a small, contained refactor. It's also worth saying plainly: there's no commit in this project's history that fixes a bug in these three files after the initial build. Anything that needed adjusting along the way went uncaptured, so this account sticks to what the history actually shows.
Default rendering vs. a custom Field component
Approach
When to use
Trade-off
Default group field rendering
The sub-fields don't share a visual relationship, or the group is edited rarely
Fast to set up, zero custom code, but scales poorly once the group represents one cohesive decision
Custom Field component, single collection
One group field needs a purpose-built UI and there's exactly one caller
Full control over the UI, but the component and its data source are coupled together
Custom Field component with field.custom config
The same UI pattern applies to more than one collection, with different field lists
One component to maintain across collections, at the cost of a small server-side prop-narrowing step per caller
FAQ
Does overriding a field's rendering change how Payload stores the data?
Overriding admin.components.Field changes only how the field renders in the admin panel. The fields array inside the group definition remains the actual schema Payload validates and stores against, so validation, defaults, and the stored data shape stay exactly as they were before the custom component existed.
Why can't useField() run inside a .map()?
React's Rules of Hooks require every component to call the same hooks, in the same order, on every render. Mapping over a dynamic array and calling useField() once per item inside that map violates that rule the moment the array's length can change. Giving each item its own component sidesteps the problem entirely, since each component instance calls the hook exactly once.
How do you pass different action lists into the same shared component?
Through field.custom on each field's definition. The server component reads field.custom.actions and forwards it as a plain prop to the client component, so the client component never needs to know which collection it's rendering for.
Does the custom component respect readOnly automatically?
Payload passes readOnly into the server component as part of GroupFieldServerProps, and from there it has to be threaded through explicitly — down to the client component's props, and down to each row's disabled attribute on its buttons. Once a custom component takes over rendering, readOnly gets accepted and passed along like any other prop.
Do you need to handle default values inside the custom component?
No separate logic for that lives in the component. Each select sub-field's defaultValue in the schema is what Payload's form state initializes with, and useField() simply reads whatever value is already in that state — including the default, on a brand-new document.
Wrapping up
A group field's default rendering is a fine fit for a set of unrelated settings edited independently. Here, the seven sub-fields represent one decision, read and set together — the case this custom component is built for. Payload's server/client Field component split handles that case cleanly: a server component narrows the field config down to serializable props, a client component owns the interactive state, and field.custom carries whatever per-collection configuration keeps that client component from being hardwired to a single caller. The one structural rule to keep in mind is that per-row hooks need per-row components — anything else runs into the Rules of Hooks the first time the row count changes.
Let me know in the comments if you have questions, and subscribe for more practical development guides.
Thanks, Matija
SEO metadata
Title: Override Payload CMS Group Fields With a Reusable Custom Admin Component
Meta description: Replace a Payload CMS group field's default dropdown rendering with a custom, reusable Field component using field.custom, useField, and the server/client split.
Suggested slug: payload-cms-custom-group-field-reusable-component
Keywords: Payload CMS, custom field component, admin.components.Field, field.custom, useField, group field, @payloadcms/ui, GroupFieldServerProps
Last updated: September 2026 (Payload 3.88.0, Next.js 16.3.3, React 19.2.7)