eddev
ACF

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.

On this page