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:
| Option | Type | Description |
|---|---|---|
id | string | Unique field key (without prefix) |
label | string | Label shown above the field |
type | string | Field type (see below) |
description | string | Help text shown below the field |
placeholder | string | Input placeholder text |
default | mixed | Default value |
required | bool | Mark field as required — validation runs on save |
width | string | Column width as a percentage, e.g. '50', '33.33'. Default '100' |
conditions | array | Conditional logic — show/hide based on other field values |
readonly | bool | Render as non-interactive text; never written on save (see Readonly fields) |
validate | callable | Your own check: return true, or false / an error message |
on_invalid | string | When validation fails: 'store' (default) saves the input anyway and shows the error; 'keep' leaves the saved value as it was and shows what you typed again on the next load (taw/core v1.99.0+; a json field keeps by default) |
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
groupfield, it propagates to every sub-field. - On a
repeaterfield, 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. Areadonlysub-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
| Option | Default | Description |
|---|---|---|
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 |
defaults | (none) | ['field_id' => value] — what a field reads, and shows in the editor, while nothing is stored (taw/core v1.80.0+). See Defaults |
Defaults
Put a block's fallback content in defaults instead of writing getMeta(...) ?: 'text' in getData():
new Metabox([
'id' => 'taw_hero',
'screens' => ['page'],
'fields' => [
['id' => 'hero_heading', 'type' => 'text'],
['id' => 'hero_items', 'type' => 'repeater', 'fields' => [['id' => 'title', 'type' => 'text']]],
],
'defaults' => [
'hero_heading' => __('Welcome', 'taw-theme'),
'hero_items' => [['title' => 'One'], ['title' => 'Two']], // structured fields as arrays
],
]);
Metabox::get(),getMeta(),get_repeater()and the other getters, andTaw::post()->field(), return the default while the stored value is empty as?:saw it: nothing,'','0', or an empty JSON list (a repeater saved with no rows stores'[]', an image field saved blank'0'). A checkbox's'0'is a value. The page renders as it did with?:(taw/core v1.80.1+).- The edit screen shows the default in the field, so saving the post keeps it.
- A default reaches only the posts its metabox applies to: its post types, templates and slugs (taw/core v1.95.2+). If each page variation has its own metabox with the same field ids, each page gets its own variation's defaults, and a variation that declares none shows none.
- Options pages take the same key, and
OptionsPage::get()falls back to it. - A
?:fallback lives only in code. A default can become a record: Tools → TAW Data → Defaults ortaw content:defaults --applywrites the defaults into empty fields, with an undo. See Content Interchange. - The block editor's data panel doesn't show defaults yet.
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
Since taw/core v1.57.0 there's also a typed API: $this->fields($postId)->field('hero_image')->image()->url() in a block, or Taw::post() anywhere, with values escaped when echoed. See Reading fields.
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'),
];
}