Custom Fields
Build a custom ACF field type with PHP storage and React editing UI.
Custom fields are for reusable editor controls that need more than a built-in ACF input. A field has two sides:
- a PHP file that registers the ACF field type and its GraphQL shape
- a React file that renders the editing control in WordPress admin
Put both files in backend/fields and keep the slug consistent across the PHP registration, the TSX filename, and the ACF field type.
Register The PHP Field
The PHP file is loaded by the backend include path. Starter themes include backend/fields/*.php from backend/includes.php.
<?php
ED()->registerFieldType("event-link", [
"label" => "Event Link",
"type" => "EventLink",
"objectType" => [
"fields" => [
"label" => ["type" => "String"],
"url" => ["type" => "String"],
"opensInNewTab" => ["type" => "Boolean"],
],
],
"resolve" => function ($root, $args, $context, $info, $value) {
return is_array($value) ? $value : null;
},
]);registerFieldType makes event-link available as an ACF field type. The type value is the GraphQL type returned when a GraphQL file selects this field.
Use objectType for small structured values. For richer project data, register GraphQL types and resolvers explicitly, then set type to the GraphQL type name.
Add The React Editor
The React file is discovered by eddev's admin manifest from backend/fields/*.tsx. Its filename must match the ACF field type slug.
import { defineField } from "eddev/admin"
type EventLinkValue = {
label?: string
url?: string
opensInNewTab?: boolean
}
export default defineField<EventLinkValue>({
defaultValue: {},
render({ value, onChange }) {
return (
<div className="stack-y-2">
<label>
<span>Label</span>
<input
className="regular-text"
value={value.label ?? ""}
onChange={(event) => {
onChange({ ...value, label: event.target.value })
}}
/>
</label>
<label>
<span>URL</span>
<input
className="regular-text"
value={value.url ?? ""}
onChange={(event) => {
onChange({ ...value, url: event.target.value })
}}
/>
</label>
<label>
<input
type="checkbox"
checked={!!value.opensInNewTab}
onChange={(event) => {
onChange({ ...value, opensInNewTab: event.target.checked })
}}
/>
Open in a new tab
</label>
</div>
)
},
})defineField receives the current value and an onChange callback. eddev stores the React value in ACF through a hidden JSON input, then the PHP side can normalize or resolve it for GraphQL.
Use The Field In ACF
After the PHP field type is registered, add it to a field group in ACF like any other field type. The field can live on a block, post type, options page, taxonomy, or other ACF location.
When a block or view selects that field, GraphQL receives the resolved value:
query {
block {
content_event_card {
cta {
label
url
opensInNewTab
}
}
}
}import { defineBlock } from "eddev/blocks"
export default defineBlock("content/event-card", (props) => {
if (!props.cta?.url) return null
return (
<a href={props.cta.url} target={props.cta.opensInNewTab ? "_blank" : undefined}>
{props.cta.label ?? "Read more"}
</a>
)
})Keep custom fields focused. If the editor only needs a fixed list of choices, use a custom enum field instead.