TAW FrameworkMetabox and editor tooling

Metabox and editor tooling

Declare structured metaboxes in PHP, use the full field-type library, wire conditional logic, and retrieve saved values in your templates.

What metaboxes are in TAW

TAW Core's metabox engine (TAW\Core\Metabox\Metabox) lets you declare custom admin fields in pure PHP — no plugin required. It handles rendering in the WordPress editor, validation and sanitization on save, and retrieval helpers for templates.

You attach a metabox to a block by calling new Metabox([...]) inside registerMetaboxes() on any MetaBlock.

Minimal example

use TAW\Core\Metabox\Metabox;

new Metabox([
    'id'     => 'taw_hero',
    'title'  => 'Hero Section',
    'screens' => ['page'],
    'fields' => [
        ['id' => 'heading', 'label' => 'Heading', 'type' => 'text', 'required' => true],
        ['id' => 'image',   'label' => 'Image',   'type' => 'image'],
    ],
]);

Common field options

All field types share a base set of options:

OptionTypeDescription
idstringUnique field key (without prefix)
labelstringLabel shown above the field
typestringField type (see below)
descriptionstringHelp text shown below the field
placeholderstringInput placeholder text
defaultmixedDefault value
requiredboolMark field as required — validation runs on save
widthstringColumn width as a percentage, e.g. '50', '33.33'. Default '100'
conditionsarrayConditional logic — show/hide based on other field values
readonlyboolRender as non-interactive text; never written on save (see Readonly fields)

Field types

Conditional fields

Fields can show or hide based on other field values. Conditions are evaluated live in the admin using Alpine.js, and also server-side on save. All conditions in the array use AND logic.

'fields' => [
    ['id' => 'show_cta', 'label' => 'Show CTA', 'type' => 'checkbox'],
    ['id' => 'cta_text', 'label' => 'CTA Text', 'type' => 'text',
     'conditions' => [
         ['field' => 'show_cta', 'operator' => '==', 'value' => '1'],
     ]],
    ['id' => 'cta_url', 'label' => 'CTA URL', 'type' => 'url',
     'conditions' => [
         ['field' => 'show_cta', 'operator' => '==', 'value' => '1'],
     ]],
],

Supported operators: ==, !=, contains, empty, !empty

Readonly fields

Set 'readonly' => true on any field whose value is authoritatively owned by something other than the wp-admin form — an external .md-file sync pipeline, a computed value — so the metabox stops implying it's editable there.

['id' => 'case_client', 'label' => 'Client', 'type' => 'text', 'readonly' => true],

A small lock icon appears next to the field's label (with a "Managed externally — read-only" tooltip), so the field reads as intentionally locked rather than as a bug in the admin UI.

This is enforced on both ends, not just visually. No <input>/<select>/<textarea> is rendered — there's nothing for devtools to re-enable and submit — and Metabox::save() skips the field entirely, so a forged $_POST key is ignored regardless of what the rendered markup looked like.

readonly composes with group and repeater rather than being its own field type:

  • On a group field, it propagates to every sub-field.
  • On a repeater field, it disables Add/Remove/reorder for the whole row list — not just the sub-field values — and propagates to every sub-field in every row. A synced list whose membership could still be edited would be just as misleading as an editable input. A readonly sub-field inside an otherwise-editable repeater is also supported and enforced per row.

Not currently enforced by TAW\Core\Rest\VisualEditorEndpoint or the fields:set/seo:inject CLI commands — those are separate write paths that don't consult this flag yet.

Tabbed metaboxes

Use the tabs key to group fields into tabs. Each tab references field IDs from the fields array.

new Metabox([
    'id'     => 'taw_hero',
    'title'  => 'Hero Section',
    'screens' => ['page'],
    'fields' => [
        ['id' => 'heading',  'label' => 'Heading',    'type' => 'text'],
        ['id' => 'image',    'label' => 'Image',      'type' => 'image'],
        ['id' => 'bg_color', 'label' => 'Background', 'type' => 'color'],
        ['id' => 'show_cta', 'label' => 'Show CTA',   'type' => 'checkbox'],
        ['id' => 'cta_text', 'label' => 'CTA Text',   'type' => 'text'],
    ],
    'tabs' => [
        ['id' => 'content', 'label' => 'Content', 'fields' => ['heading', 'image']],
        ['id' => 'design',  'label' => 'Design',  'fields' => ['bg_color']],
        ['id' => 'cta',     'label' => 'CTA',     'fields' => ['show_cta', 'cta_text']],
    ],
]);

Metabox config options

OptionDefaultDescription
screens['page']Post types, page slugs, or page template filenames to attach to — accepts a mixed array
context'normal'Position: 'normal', 'side', 'advanced'
priority'high'Order: 'high', 'default', 'low'
prefix'_taw_'Meta key prefix applied to all field IDs
icon(none)SVG string — displayed as the metabox icon
show_on(none)callable(WP_Post): bool — return false to hide the metabox

Field availability in nav menu context

When metabox fields are rendered on the WordPress nav menu editor screen, only a subset of field types are available: text, url, number, textarea, select, and checkbox. Complex fields such as image, wysiwyg, color, files, and repeater are not supported in the nav menu context due to asset availability constraints.

Retrieval API

Use these static helpers inside getData() or anywhere in your templates:

use TAW\Core\Metabox\Metabox;

// Plain text / any scalar value
$heading = Metabox::get($postId, 'hero_heading');

// Checkbox → boolean
$showCta = Metabox::get_bool($postId, 'show_cta');

// Image attachment ID → URL
$imageUrl = Metabox::get_image_url($postId, 'hero_image', 'large');

// Color with fallback
$bgColor = Metabox::get_color($postId, 'bg_color', '#ffffff');

// post_select → array of post IDs
$featuredId = Metabox::get_posts($postId, 'featured_post')[0] ?? null;
$relatedIds = Metabox::get_posts($postId, 'related_posts');

// repeater → array of row arrays
$members = Metabox::get_repeater($postId, 'team_members');
foreach ($members as $member) {
    echo esc_html($member['name'] ?? '');
}

Inside a MetaBlock, the convenience wrappers delegate to the same methods:

protected function getData(int|false $postId): array
{
    return [
        'heading'   => $this->getMeta($postId, 'hero_heading'),
        'image_url' => $this->getImageUrl($postId, 'hero_image', 'large'),
    ];
}