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 |
Field types
Single-line text input.
['id' => 'heading', 'label' => 'Heading', 'type' => 'text', 'placeholder' => 'Enter heading…', 'required' => true]
$heading = Metabox::get($postId, 'heading');
echo esc_html($heading);
Multi-line plain text. Accepts rows (default 4) and placeholder.
['id' => 'summary', 'label' => 'Summary', 'type' => 'textarea', 'rows' => 4]
$summary = Metabox::get($postId, 'summary');
echo esc_html($summary);
WordPress TinyMCE rich-text editor. Accepts rows (default 8), media_buttons (bool), and teeny (bool).
['id' => 'body', 'label' => 'Body', 'type' => 'wysiwyg', 'rows' => 8, 'media_buttons' => true]
$body = Metabox::get($postId, 'body');
echo wp_kses_post($body);
URL input with browser-level validation.
['id' => 'cta_url', 'label' => 'CTA URL', 'type' => 'url', 'placeholder' => 'https://…']
$url = Metabox::get($postId, 'cta_url');
echo '<a href="' . esc_url($url) . '">Learn more</a>';
Numeric input. Accepts min, max, and step.
['id' => 'item_count', 'label' => 'Count', 'type' => 'number', 'min' => 1, 'max' => 100, 'step' => 1]
$count = (int) Metabox::get($postId, 'item_count');
Slider with live value display. Accepts min, max, step, and unit (e.g. 'px').
['id' => 'min_height', 'label' => 'Min Height', 'type' => 'range', 'min' => 400, 'max' => 900, 'step' => 50, 'unit' => 'px', 'default' => 600]
$height = Metabox::get($postId, 'min_height') ?: '600';
echo '<section style="min-height: ' . esc_attr($height) . 'px">';
Dropdown list. Provide an options map of value => label pairs.
['id' => 'style', 'label' => 'Style', 'type' => 'select', 'options' => ['light' => 'Light', 'dark' => 'Dark']]
$style = Metabox::get($postId, 'style');
echo '<section class="section--' . esc_attr($style) . '">';
Toggle switch. Stores '1' or '0'. Use Metabox::get_bool() to retrieve as a boolean.
['id' => 'show_cta', 'label' => 'Show CTA', 'type' => 'checkbox']
$showCta = Metabox::get_bool($postId, 'show_cta');
if ($showCta) { /* render CTA */ }
WordPress color picker (hex). Accepts a default value.
['id' => 'bg_color', 'label' => 'Background', 'type' => 'color', 'default' => '#ffffff']
$bgColor = Metabox::get_color($postId, 'bg_color', '#ffffff');
echo '<section style="background-color: ' . esc_attr($bgColor) . '">';
WordPress media library picker. Stores an attachment ID. Retrieve as a URL with get_image_url().
['id' => 'hero_image', 'label' => 'Hero Image', 'type' => 'image']
$imageUrl = Metabox::get_image_url($postId, 'hero_image', 'large');
// Inside a MetaBlock:
$imageUrl = $this->getImageUrl($postId, 'hero_image', 'large');
Nested group of sub-fields under a shared key prefix. Unlike a repeater, there is always exactly one row. Sub-fields are stored as separate meta keys using the group id as a prefix.
[
'id' => 'hero_cta',
'label' => 'CTA Button',
'type' => 'group',
'fields' => [
['id' => 'text', 'label' => 'Text', 'type' => 'text', 'width' => '50'],
['id' => 'url', 'label' => 'URL', 'type' => 'url', 'width' => '50'],
],
]
// Stored as _taw_hero_cta_text and _taw_hero_cta_url
$ctaText = Metabox::get($postId, 'hero_cta_text');
$ctaUrl = Metabox::get($postId, 'hero_cta_url');
Multi-file picker with drag-to-reorder. Stores an array of attachment IDs. Accepts limit to cap the number of files that can be selected.
['id' => 'gallery', 'label' => 'Gallery', 'type' => 'files', 'limit' => 10]
$attachmentIds = Metabox::get($postId, 'gallery'); // array of attachment IDs
foreach ($attachmentIds as $id) {
echo wp_get_attachment_image($id, 'medium');
}
Dynamic sortable list of rows. Each row has the same set of sub-fields. Accepts min and max (0 = unlimited). Rows are drag-and-drop sortable and individually collapsible in the admin.
Repeater fields support nested repeaters — you can place a repeater inside another repeater's fields array for hierarchical data structures (up to three levels deep).
[
'id' => 'team_members',
'label' => 'Team Members',
'type' => 'repeater',
'min' => 1,
'max' => 10,
'fields' => [
['id' => 'name', 'label' => 'Name', 'type' => 'text', 'width' => '50'],
['id' => 'role', 'label' => 'Role', 'type' => 'text', 'width' => '50'],
['id' => 'photo', 'label' => 'Photo', 'type' => 'image'],
['id' => 'bio', 'label' => 'Bio', 'type' => 'textarea'],
],
]
$members = Metabox::get_repeater($postId, 'team_members');
foreach ($members as $member) {
echo esc_html($member['name'] ?? '');
echo esc_html($member['role'] ?? '');
}
By default rows render as a collapsible accordion. Use layout to switch to a tabbed UI:
layout value | Description |
|---|---|
| (omitted) | Accordion rows (default) |
tabbed_horizontal | Tabs along the top, content below |
tabbed_vertical | Tabs stacked in a left column, content on the right |
[
'id' => 'slides',
'label' => 'Slides',
'type' => 'repeater',
'layout' => 'tabbed_horizontal',
'fields' => [
['id' => 'title', 'label' => 'Title', 'type' => 'text'],
['id' => 'image', 'label' => 'Image', 'type' => 'image'],
],
]
Nested repeater example:
[
'id' => 'sections',
'label' => 'Sections',
'type' => 'repeater',
'fields' => [
['id' => 'title', 'label' => 'Title', 'type' => 'text'],
[
'id' => 'items',
'label' => 'Items',
'type' => 'repeater',
'fields' => [
['id' => 'label', 'label' => 'Label', 'type' => 'text'],
],
],
],
]
Searchable post picker. Accepts post_type, multiple (bool), and max.
// Single post
['id' => 'featured_post', 'label' => 'Featured Post', 'type' => 'post_select', 'post_type' => 'post']
// Multiple with cap
['id' => 'related', 'label' => 'Related Posts', 'type' => 'post_select', 'post_type' => 'post', 'multiple' => true, 'max' => 3]
$featuredId = Metabox::get_posts($postId, 'featured_post')[0] ?? null;
$relatedIds = Metabox::get_posts($postId, 'related'); // array of IDs
jQuery UI date picker. Stored as a date string. Accepts date_format (default yy-mm-dd), min_date, and max_date (ISO format YYYY-MM-DD).
['id' => 'event_date', 'label' => 'Event Date', 'type' => 'datepicker', 'min_date' => '2024-01-01', 'max_date' => '2030-12-31']
$eventDate = Metabox::get($postId, 'event_date'); // e.g. '2025-06-15'
echo esc_html($eventDate);
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
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 |
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'),
];
}