CC Fields
CC Fields is a custom-fields framework for WordPress. It lets administrators build field groups in the WordPress dashboard and lets theme and plugin developers read, update, expose, and render those fields through a PHP API, shortcodes, REST, front-end forms, local JSON, and Gutenberg integrations.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/smalezan/cc-fields/archive/refs/heads/main.zipCC Fields is a custom-fields framework for WordPress. It lets administrators build field groups in the WordPress dashboard and lets theme and plugin developers read, update, expose, and render those fields through a PHP API, shortcodes, REST, front-end forms, local JSON, and Gutenberg integrations.
Current version: 1.0.3
Requirements
- WordPress 6.4 or newer
- PHP 8.0 or newer
- Administrator access to create and manage field groups
- A writable theme directory or another writable directory if local JSON sync is used
Installation
- Copy the plugin directory into
wp-content/plugins/cc-fields. - In WordPress, go to Plugins > Installed Plugins.
- Activate CC Fields.
- Open CC Fields in the WordPress admin menu.
For development dependencies and tests:
composer install
vendor/bin/phpunit
Composer packages are development-only; the plugin itself does not require Composer at runtime.
Quick start
1. Create a field group
Go to CC Fields > Add New, enter a group title such as “Page Details,” then select + Add Field.
For each field:
- Label is the human-readable title shown to editors.
- Name is the stable machine-readable name used in templates, for example
hero_title. Use lowercase letters, numbers, and underscores. - Field Key is an automatically generated internal identifier. Do not change or reuse it.
- Required prevents the form from accepting an empty value for the field.
- Instructions are displayed to editors near the input.
- Type-specific settings control the input, validation, and formatted return value.
Drag field rows to change their order.
2. Choose where the group appears
In Location, create one or more rules. Rules in the same group use AND. Separate rule groups use OR.
For example:
Post Type is equal to Page
AND Page Template is equal to Landing Page
OR
Post is equal to Home
The group is shown when all rules in any one rule group match.
3. Configure and save the group
Available group settings are:
- Active: enables or disables the group.
- REST API: includes its values in the
ccf_fieldsproperty on supported WordPress REST responses and enables the Gutenberg sidebar integration. - Position: normal, high, or side metabox position.
- Style: standard WordPress metabox or seamless presentation.
- Label Placement: above or to the left of inputs.
- Instruction Placement: below the label or above the input.
- Order: lower-numbered matching groups appear first.
Save the group, edit a matching WordPress object, enter values, and update it.
4. Display a value in a theme
<?php if (ccf_has_field('hero_title')) : ?>
<h1><?php echo esc_html(ccf_get_field('hero_title')); ?></h1>
<?php endif; ?>
ccf_get_field() returns the field's formatted value. Always escape for the output context even though CC Fields sanitizes values when they are stored.
Location rules
The == and != operators are supported for all location rules.
| Rule | What it matches |
|---|---|
| Post Type | A post, page, attachment, or another UI-enabled post type |
| Page Template | The default template or a template supplied by the active theme |
| Post Taxonomy | A post assigned to a particular taxonomy term |
| Post Format | Standard or a registered post format |
| Post Status | Published, draft, pending, private, or scheduled |
| User Role | The role of the current user viewing the editing screen |
| Page Type | Front page, posts page, top-level page, child page, or parent page |
| Options Page | A programmatically registered CC Fields options page |
| User Form | All users or a specific role on user-profile screens |
| Taxonomy Term | A term add/edit screen for a particular taxonomy |
| Post | One specific published post |
| Post Category | A post assigned to a selected category |
| Post Tag | A post assigned to a selected tag |
| Page Parent | A page whose direct parent is the selected page |
| Attachment | All attachments or an image, video, audio, or application MIME family |
| Comment | All comments, comments, trackbacks, or pingbacks |
Post-dependent rules may not match a brand-new unsaved post until WordPress has assigned it an ID or the relevant property has been saved.
Field types
Every data field supports a label, name, required state, instructions, and conditional logic. Layout-only fields do not store a value.
Basic
| Type | Important settings | Formatted value |
|---|---|---|
| Text | Default, placeholder, min/max length, regex pattern | String |
| Text Area | Default, placeholder, rows, max length | String |
| Number | Default, placeholder, minimum, maximum, step | Integer or float |
| Range | Default, minimum, maximum, step | Integer or float |
| Default, placeholder | Sanitized email string | |
| URL | Default, placeholder | Sanitized URL string |
| Password | Placeholder | String; this is stored as post meta, not hashed as a WordPress login password |
| Link | Return an array or URL only | ['url' => ..., 'title' => ..., 'target' => ...] or URL string |
Choice
| Type | Important settings | Formatted value |
|---|---|---|
| Select | Choices, default, multiple selection | String or array of strings |
| Checkbox | Choices, comma-separated defaults, vertical/horizontal layout | Array of selected values |
| Radio Button | Choices, default, vertical/horizontal layout | Selected string |
| Button Group | Choices, default | Selected string |
| True / False | Toggle message, default | Boolean |
Enter choices one per line. A line can contain value : Label; if no label is supplied, the value is also used as its label.
Content
| Type | Important settings | Formatted value |
|---|---|---|
| Image | Return format, preview size, allowed extensions, max size | Image array, URL, or attachment ID |
| File | Return format, allowed extensions, max size | File array, URL, or attachment ID |
| Gallery | Minimum/maximum images, preview size | Array of image data |
| WYSIWYG Editor | Default, toolbar, visual/text tabs, media button, rows | HTML processed through the_content |
| oEmbed | Width and height | Provider embed HTML when WordPress can resolve the URL |
An image array contains id, url, width, height, alt, title, and sizes. A file array contains id, url, filename, filesize, and mime_type.
Relational
| Type | Important settings | Formatted value |
|---|---|---|
| Post Object | Post type, single/multiple, object/ID return, bidirectional | WP_Post, ID, or an array of either |
| Relationship | Post type, taxonomy filter, min/max, object/ID return, bidirectional | Array of WP_Post objects or IDs |
| Page Link | Post type, single/multiple, allow null | Permalink string or array of permalinks |
| User | Role filter, single/multiple, object/ID/array return | WP_User, ID, compact user array, or arrays of them |
| Taxonomy Term | Taxonomy, single/multiple, object/ID/slug return | WP_Term, ID, slug, or arrays of them |
For a bidirectional Post Object or Relationship field, create the back-reference field on the related content and use the same field name. When the source relationship changes, CC Fields adds and removes the source post ID in that field on related posts. Only post relationships are synchronized.
Interactive
| Type | Important settings | Formatted value |
|---|---|---|
| Date Picker | Display format, min/max date, default | Stored Y-m-d date rendered in the configured display format |
| Time Picker | Display format, default | Stored time rendered in the configured display format |
| Date Time Picker | Display format, min/max date, default | Formatted date/time string |
| Color Picker | Default color, opacity, swatches | Color string |
| Google Map | Center, zoom, height | Array containing lat, lng, zoom, and address |
Configure Google Maps before using the map field:
add_filter('ccf/google_maps_api_key', function (): string {
return 'YOUR_GOOGLE_MAPS_API_KEY';
});
Restrict the key appropriately in Google Cloud and enable the Maps JavaScript API and Places library.
Layout
| Type | Purpose |
|---|---|
| Message | Displays editor guidance; stores no value |
| Tab | Starts a tab section; stores no value |
| Accordion | Starts an accordion section; stores no value |
| Group | Stores a structured set of sub-fields |
| Repeater | Stores zero or more rows using one sub-field schema |
| Flexible Content | Stores ordered rows selected from one or more layout schemas |
| Clone | Reuses the fields from another field group and stores them as flattened individual meta values |
Repeaters and Flexible Content support minimum and maximum rows and a configurable add button. A Repeater can display rows as a table, block, or row layout. Flexible Content layouts each have a stable name, label, and their own sub-fields.
Clone fields can prefix each cloned field name with the clone field name. For example, cloning subtitle into a clone named hero with prefixing enabled stores and reads hero_subtitle. Clone fields do not have a top-level meta value.
Conditional logic
Enable Conditional Logic in a field's settings to show or hide it based on other fields in the same form.
You can choose whether all or any rules must match. Operators are:
- is equal to
- is not equal to
- is greater than
- is less than
- contains
- does not contain
- is empty
- is not empty
Conditional logic controls presentation. It should not be treated as an authorization boundary or as a replacement for server-side validation.
PHP template API
Context identifiers
Most API functions accept an optional second argument:
| Argument | Context |
|---|---|
Omitted or false |
Current post from get_the_ID() |
123 |
Post ID 123 |
'option' |
Global options storage |
'user_5' |
User ID 5 |
'term_12' |
Term ID 12 |
'comment_9' |
Comment ID 9 |
Read values
$title = ccf_get_field('hero_title');
$other_title = ccf_get_field('hero_title', 123);
$phone = ccf_get_field('phone', 'option');
$job_title = ccf_get_field('job_title', 'user_5');
$term_color = ccf_get_field('color', 'term_12');
$rating = ccf_get_field('rating', 'comment_9');
Use ccf_get_raw_field() when you need the stored value without field-type formatting:
$attachment_id = ccf_get_raw_field('hero_image');
Use ccf_get_field_object() to retrieve the field definition together with value, raw_value, and context information:
$field = ccf_get_field_object('hero_title');
if ($field) {
echo esc_html($field['label']);
}
Check for a non-empty value:
if (ccf_has_field('subtitle')) {
echo esc_html(ccf_get_field('subtitle'));
}
ccf_the_field() echoes a scalar as escaped text and JSON-encodes arrays. For markup-sensitive output, return the value with ccf_get_field() and escape it for its actual context.
Update and delete values
ccf_update_field('hero_title', 'A new title', 123);
ccf_update_field('support_email', 'help@example.com', 'option');
ccf_delete_field('hero_title', 123);
Updates are sanitized through the registered field type when a field definition with that name exists. Field names should therefore be unique when their definitions or return formats differ.
Output examples
Text:
<p><?php echo esc_html(ccf_get_field('subtitle')); ?></p>
URL:
<?php $url = ccf_get_field('cta_url'); ?>
<?php if ($url) : ?>
<a href="<?php echo esc_url($url); ?>">Learn more</a>
<?php endif; ?>
Image configured to return an array:
<?php $image = ccf_get_field('hero_image'); ?>
<?php if (is_array($image) && !empty($image['id'])) : ?>
<?php echo wp_get_attachment_image($image['id'], 'large', false, [
'alt' => $image['alt'],
]); ?>
<?php endif; ?>
Relationship configured to return post objects:
<?php foreach ((array) ccf_get_field('related_articles') as $article) : ?>
<?php if ($article instanceof WP_Post) : ?>
<a href="<?php echo esc_url(get_permalink($article)); ?>">
<?php echo esc_html(get_the_title($article)); ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
WYSIWYG content is already processed through the_content. Render trusted editor content with wp_kses_post():
echo wp_kses_post(ccf_get_field('body_copy'));
Repeater loops
<?php if (ccf_have_rows('features')) : ?>
<ul>
<?php while (ccf_have_rows('features')) : ccf_the_row(); ?>
<li>
<strong><?php echo esc_html(ccf_get_sub_field('title')); ?></strong>
<?php echo esc_html(ccf_get_sub_field('description')); ?>
</li>
<?php endwhile; ?>
</ul>
<?php endif; ?>
Pass a context to the first ccf_have_rows() call when looping rows outside the current post:
while (ccf_have_rows('office_hours', 'option')) {
ccf_the_row();
echo esc_html(ccf_get_sub_field('day'));
}
Nested repeater loops are supported:
while (ccf_have_rows('sections')) {
ccf_the_row();
echo esc_html(ccf_get_sub_field('heading'));
while (ccf_have_rows('items')) {
ccf_the_row();
echo esc_html(ccf_get_sub_field('label'));
}
}
ccf_get_raw_sub_field() returns an unformatted sub-field value.
Flexible Content loops
while (ccf_have_rows('page_sections')) {
ccf_the_row();
switch (ccf_get_row_layout()) {
case 'hero':
get_template_part('template-parts/section', 'hero', [
'title' => ccf_get_sub_field('title'),
'image' => ccf_get_sub_field('image'),
]);
break;
case 'text_block':
echo wp_kses_post(ccf_get_sub_field('content'));
break;
}
}
Optional ACF-style loop aliases
If neither ACF nor another plugin has defined the equivalent functions, CC Fields registers these compatibility helpers late on init:
have_rows()the_row()get_sub_field()the_sub_field()get_fields()for post contexts
Prefer the ccf_* functions in reusable code because they are always available and cannot collide with another provider.
Shortcode
Use the [ccf] shortcode in post content or compatible widgets:
[ccf field="hero_title"]
[ccf field="support_email" post_id="option"]
[ccf field="job_title" post_id="user_5"]
[ccf field="hero_image" post_id="123" format="raw"]
Attributes:
fieldis required.post_iddefaults to the current post and also accepts context identifiers.format="formatted"is the default; useformat="raw"for the stored value.
Arrays are rendered as a comma-separated list; nested arrays are JSON-encoded. Complex layouts are better rendered through PHP templates.
Options pages
Register options pages in a plugin or theme on ccf/init:
add_action('ccf/init', function (): void {
ccf_add_options_page([
'page_title' => 'Site Settings',
'menu_title' => 'Site Settings',
'menu_slug' => 'site-settings',
'capability' => 'manage_options',
'icon_url' => 'dashicons-admin-generic',
'position' => 81,
]);
});
Then create a field group and set its location to Options Page is equal to Site Settings. Retrieve values with the option context:
$phone = ccf_get_field('company_phone', 'option');
Register a submenu page by setting parent_slug:
add_action('ccf/init', function (): void {
ccf_add_options_sub_page([
'page_title' => 'Theme Footer',
'menu_title' => 'Footer',
'menu_slug' => 'theme-footer',
'parent_slug' => 'themes.php',
'capability' => 'edit_theme_options',
]);
});
Programmatic field groups
Register code-defined groups on the ccf/init action. Use globally unique, stable group and field keys.
add_action('ccf/init', function (): void {
ccf_add_local_field_group([
'key' => 'group_product_details',
'title' => 'Product Details',
'fields' => [
[
'key' => 'field_product_subtitle',
'label' => 'Subtitle',
'name' => 'product_subtitle',
'type' => 'text',
'required' => true,
'instructions' => 'A short line shown below the product name.',
'settings' => [
'maxlength' => 120,
'placeholder' => 'Built for everyday use',
],
],
[
'key' => 'field_product_featured',
'label' => 'Featured',
'name' => 'product_featured',
'type' => 'true_false',
'required' => false,
'settings' => [
'message' => 'Feature this product',
],
],
],
'location' => [
[
[
'param' => 'post_type',
'operator' => '==',
'value' => 'product',
],
],
],
'settings' => [
'active' => true,
'position' => 'normal',
'style' => 'default',
'label_placement' => 'top',
'instruction_placement' => 'below_label',
'order' => 0,
'show_in_rest' => true,
],
]);
});
Add a field to a previously registered local group:
add_action('ccf/init', function (): void {
ccf_add_local_field([
'parent' => 'group_product_details',
'key' => 'field_product_note',
'label' => 'Internal Note',
'name' => 'product_note',
'type' => 'textarea',
'required' => false,
'settings' => ['rows' => 4],
]);
}, 20);
Local groups exist only in memory and are not edited in the dashboard or written to local JSON.
Programmatic conditional logic
Conditional rules refer to the controlling field's key:
'settings' => [
'conditional_logic' => [
'enabled' => true,
'action' => 'show',
'logic' => 'all',
'rules' => [
[
'field' => 'field_product_featured',
'operator' => '==',
'value' => '1',
],
],
],
],
Front-end editing forms
Front-end forms edit an existing post. Both AJAX and normal POST submissions require the current user to pass current_user_can('edit_post', $post_id). Registering the AJAX action for logged-out visitors does not grant guest editing.
Call ccf_form_head() before the page sends output. A reliable theme pattern is:
add_action('template_redirect', function (): void {
if (is_singular('post')) {
ccf_form_head();
}
});
Render a form using a database group ID:
ccf_form([
'group_id' => 42,
'post_id' => get_the_ID(),
'submit_value' => 'Save changes',
'method' => 'ajax',
]);
Or use a stable group key, which also works for a local group:
ccf_form([
'group_key' => 'group_product_details',
'post_id' => get_the_ID(),
'redirect' => get_permalink(),
'submit_value' => 'Update product',
'html_before' => '<section class="product-editor">',
'html_after' => '</section>',
'method' => 'post',
]);
Arguments:
| Argument | Default | Meaning |
|---|---|---|
group_id |
0 |
Database field-group post ID |
group_key |
empty | Stable group key; takes precedence over group_id |
post_id |
current post | Existing post to edit |
redirect |
empty | URL after a successful submission |
submit_value |
Save |
Submit button text |
html_before |
empty | Allowed HTML printed before the form |
html_after |
empty | Allowed HTML printed after the form |
method |
ajax |
Use ajax or post |
After a successful save, CC Fields fires:
add_action('ccf/frontend_form_saved', function (int $post_id, array $group): void {
// Run application-specific follow-up work.
}, 10, 2);
REST API
Post endpoint integration
When a matching field group has REST API enabled, supported WordPress post-type responses include a ccf_fields object:
GET /wp-json/wp/v2/posts/123
Example response fragment:
{
"id": 123,
"ccf_fields": {
"hero_title": "Welcome",
"featured": true
}
}
Update known fields by sending the same object through the normal post endpoint:
POST /wp-json/wp/v2/posts/123
Content-Type: application/json
X-WP-Nonce: ...
{
"ccf_fields": {
"hero_title": "Updated title"
}
}
The caller must be able to edit the post. Unknown field names are ignored. Values are sanitized using their field types. Only groups matching that specific post and marked show_in_rest are exposed.
Gutenberg editor-fields endpoint
The plugin uses this authenticated endpoint to render matching groups in the editor sidebar:
GET /wp-json/ccf/v1/editor-fields?post_id=123
The current user must be able to edit the requested post. Optional group_ids[] parameters restrict the response to particular group IDs.
Scalar fields with REST enabled are registered as individual post meta for Gutenberg. Complex array fields are intentionally excluded from individual meta registration and are not written by the sidebar as scalar values.
Gutenberg blocks
Register a server-rendered block backed by a field group:
add_action('ccf/init', function (): void {
ccf_register_block_type('mytheme/hero', [
'title' => 'Hero',
'icon' => 'cover-image',
'field_group' => 'group_hero',
'render_callback' => function (array $fields, array $args): void {
?>
<section class="hero">
<h2><?php echo esc_html($fields['heading'] ?? ''); ?></h2>
</section>
<?php
},
]);
});
The block name must include a namespace. The group key must resolve when registration runs. The callback receives raw post-meta values keyed by field name and the registration arguments. It must echo the block markup.
CC Fields block values are attached to the current post, not stored independently per block instance. Multiple instances of the same block on one post therefore read the same field values.
Local JSON sync
Saving a dashboard-managed group writes a JSON representation to:
{active-child-theme}/cc-fields/{group-slug}.json
Use CC Fields > Settings to provide a different absolute directory. The path must not contain parent-directory (..) segments.
You can configure the primary path in code:
add_filter('ccf/local_json_path', function (string $default): string {
return WP_CONTENT_DIR . '/cc-fields-json/';
});
Write copies to additional paths:
add_filter('ccf/local_json/save_paths', function (array $paths): array {
$paths[] = WP_CONTENT_DIR . '/backups/cc-fields/';
return $paths;
});
The primary path is the canonical path used for reading, comparisons, automatic imports, and conflict resolution. Extra paths are write destinations.
On init, JSON files from the primary directory are imported when no matching database group exists or when the JSON modified timestamp is newer. The Field Groups list shows sync status and provides export/sync/conflict actions. Keep generated JSON under version control when field definitions should travel between environments.
Avoid editing both the database group and its JSON file independently. If both have changed since their last sync, resolve the conflict deliberately in the dashboard.
Custom field types
A custom type extends CCF_Field and implements input rendering, settings rendering, formatting, validation, and sanitization:
class My_CCF_Field_Code extends CCF_Field {
public string $name = 'code';
public string $label = 'Code';
public string $category = 'basic';
public function render_input(array $field, mixed $value, int $post_id): void {
$this->render_field_wrap_open($field);
printf(
'<input class="ccf-input" type="text" id="ccf-%1$s" name="ccf[%1$s]" value="%2$s">',
esc_attr($field['key']),
esc_attr((string) $value)
);
$this->render_field_wrap_close($field);
}
public function render_settings(array $field): void {}
public function format_value(mixed $value, array $field, int $post_id): mixed {
return strtoupper((string) $value);
}
public function validate_value(mixed $value, array $field): true|string {
return preg_match('/^[A-Z0-9-]+$/i', (string) $value)
? true
: 'Use letters, numbers, and hyphens only.';
}
public function sanitize_value(mixed $value, array $field): mixed {
return sanitize_text_field((string) $value);
}
}
add_action('ccf/register_fields', function (): void {
ccf_register_field_type('code', My_CCF_Field_Code::class);
});
The HTML input name must use ccf[{field_key}]. Override enqueue_admin_assets() if the type needs scripts or styles. Override is_empty() when the base empty-value rules are not suitable.
Hooks and filters
Actions
| Hook | Arguments | Purpose |
|---|---|---|
ccf/init |
none | Register local groups, options pages, and blocks |
ccf/register_fields |
field registry | Register custom field types |
ccf/after_save_field |
post ID, field definition, sanitized value | React to a saved post field; also drives bidirectional relationships |
ccf/after_save_field_group |
group ID | React after a dashboard field group is saved |
ccf/delete_field_group |
group ID | React before the group and its JSON are deleted |
ccf/frontend_form_saved |
post ID, group | React after a successful front-end form submission |
Filters
| Filter | Arguments | Purpose |
|---|---|---|
ccf/format_value |
formatted value, raw value, field, object ID | Modify any formatted value |
ccf/format_value/type={type} |
formatted value, field, object ID | Modify formatted values for one type |
ccf/update_value |
incoming value, field, object ID | Modify a value before type sanitization |
ccf/google_maps_api_key |
empty string | Supply the Google Maps API key |
ccf/local_json_path |
configured/default path | Change the primary JSON path |
ccf/local_json/save_paths |
array of paths | Add or replace JSON write destinations |
Storage model
Post fields use normal WordPress post meta. User, term, and comment fields use the matching WordPress metadata table. Option fields use WordPress options.
For a field named hero_title, CC Fields stores:
hero_title: the sanitized raw value_hero_title: the stable field key used to locate the field definition and format the value later
Repeaters, galleries, flexible content, groups, and multi-value relational fields are stored as arrays using WordPress's normal serialization. Link and Google Map inputs use sanitized JSON strings internally and return structured arrays through the formatted API.
Use the public ccf_* API rather than reading meta directly when you want configured return formats and filters. Use WordPress metadata functions only when raw storage is explicitly required.
Security and escaping
- CC Fields verifies nonces and capabilities when saving through its admin and front-end forms.
- Front-end forms do not create posts and do not grant guest edit access.
- REST updates require normal WordPress edit capabilities.
- The Password field is a masked custom-meta input, not a credential store. Do not use it for WordPress passwords, API secrets, or encryption keys.
- Escape on output:
esc_html()for text,esc_attr()for attributes,esc_url()for URLs, andwp_kses_post()for allowed editor HTML. - Treat programmatic updates and custom hooks as trusted application code and validate authorization before calling them from a request handler.
Troubleshooting
A field group does not appear
- Confirm the group is Active.
- Check the AND/OR structure of its location rules.
- Save a new post once if the rule depends on its ID, taxonomy, template, status, parent, or format.
- Confirm the current user can edit the object.
- For options pages, register the page on
ccf/initand ensure the location rule uses the exact menu slug.
A formatted value looks raw
CC Fields needs the companion _{field_name} meta entry to resolve the field definition. Values saved with ccf_update_field() or through CC Fields forms create this reference. Values written directly with update_post_meta() do not. Either use ccf_update_field() or store the matching field-key reference intentionally.
A field is missing from REST
Confirm that:
- the post type has
show_in_restenabled; - a group matches that specific post;
- the group is active;
- REST API is enabled on the group; and
- the caller has the necessary WordPress access.
Local JSON does not save
Check that the configured directory is absolute, exists or can be created, and is writable by PHP. The primary child-theme path changes when the active child theme changes.
Google Map shows an API-key notice
Supply a key through ccf/google_maps_api_key, enable the required Google APIs, and verify HTTP referrer restrictions permit the WordPress admin origin.
Front-end form returns 403
The current user must be logged in and able to edit the specified post. A valid nonce alone is not sufficient.
Complex fields in Gutenberg are read-only or do not save as meta
Array-based and structured field types are deliberately excluded from scalar Gutenberg meta registration. Edit them in the CC Fields UI supplied by the plugin or use the ccf_fields REST object with correctly structured values.
Development notes
Run the automated test suite from the plugin root:
composer install
vendor/bin/phpunit
The test configuration and bootstrap are in phpunit.xml and tests/bootstrap.php.
License
CC Fields is licensed under GPL-2.0-or-later.