Problem
I’m building a WordPress theme and want a Global → Typography → Font Presets control in the Customizer that shows a grid of clickable cards (each card previews a heading/body Google-Font pair). Instead of my custom card UI, the section is either blank or falls back to a basic <select>
(or radio list) control. I’ve tried many variations of register_control_type()
, direct instantiation, OPcache resets, and cleanup of duplicate classes, but no luck.
What I’ve Done
Autoloader in functions.php
(recursively includes /inc/
files):
// in functions.php
$rii = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( __DIR__ . '/inc' )
);
foreach ( $rii as $file ) {
if ( ! $file->isDir() && $file->getExtension() === 'php' ) {
require_once $file->getPathname();
}
}
Bootstrap singleton in inc/class-zero-customizer.php
:
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
class Zero_Customizer {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
self::$instance->hooks();
}
return self::$instance;
}
private function hooks() {
error_log( 'Zero: hooks() running' );
add_action( 'customize_register', [ $this, 'register_typography_control' ] );
add_action( 'customize_controls_enqueue_scripts',[ $this, 'enqueue_control_assets' ] );
add_action( 'customize_preview_init', [ $this, 'enqueue_preview_assets' ] );
}
public function register_typography_control( $wp_customize ) {
error_log( 'Zero: register_typography_control() fired' );
require_once __DIR__ . '/customizer/controls/class-zero-control-typography.php';
// Panel & section
if ( ! $wp_customize->get_panel( 'zero_global_panel' ) ) {
$wp_customize->add_panel( 'zero_global_panel', [
'title' => __( 'Global Settings', 'zero' ),
'priority' => 10,
] );
}
$wp_customize->add_section( 'zero_typography_section', [
'title' => __( 'Typography', 'zero' ),
'panel' => 'zero_global_panel',
'priority' => 10,
] );
error_log( 'Zero: added section zero_typography_section' );
// Presets & setting
$presets = [
'playfair-open-sans' => esc_html__( 'Playfair Display / Open Sans', 'zero' ),
/* …other 9 pairs… */
];
$wp_customize->add_setting( 'zero_typography_preset', [
'default' => 'playfair-open-sans',
'sanitize_callback' => function( $val ) use ( $presets ) {
return isset( $presets[ $val ] ) ? $val : 'playfair-open-sans';
},
'transport' => 'postMessage',
] );
// Direct instantiation of custom control
$wp_customize->add_control( new Zero_Control_Typography(
$wp_customize,
'zero_typography_preset',
[
'label' => __( 'Font Presets', 'zero' ),
'description' => __( 'Click a card to choose your Heading/Body pair.', 'zero' ),
'section' => 'zero_typography_section',
'choices' => $presets,
]
) );
error_log( 'Zero: added custom-control for zero_typography_preset' );
}
public function enqueue_control_assets() {
// Load panel CSS & JS
wp_enqueue_style(
'zero-customizer-controls',
get_stylesheet_directory_uri() . '/assets/dist/css/main.min.css',
[], filemtime( get_stylesheet_directory() . '/assets/dist/css/main.min.css' )
);
wp_enqueue_script(
'zero-customizer-controls',
get_stylesheet_directory_uri() . '/assets/dist/js/customizer.bundle.js',
[ 'jquery','customize-controls' ],
filemtime( get_stylesheet_directory() . '/assets/dist/js/customizer.bundle.js' ),
true
);
}
public function enqueue_preview_assets() {
// Load iframe JS for live preview
wp_enqueue_script(
'zero-customizer-preview',
get_stylesheet_directory_uri() . '/assets/dist/js/customizer.bundle.js',
[ 'jquery','customize-preview' ],
filemtime( get_stylesheet_directory() . '/assets/dist/js/customizer.bundle.js' ),
true
);
}
}
add_action( 'after_setup_theme', [ 'Zero_Customizer', 'get_instance' ] );
Custom Control in inc/customizer/controls/class-zero-control-typography.php
:
<?php
if ( ! class_exists( 'WP_Customize_Control' ) ) {
return;
}
if ( ! class_exists( 'Zero_Control_Typography' ) ) {
class Zero_Control_Typography extends WP_Customize_Control {
public $type = 'typography';
public function render_content() {
error_log( 'Zero: Zero_Control_Typography::render_content()' );
if ( empty( $this->choices ) ) {
return;
}
echo '<span class="customize-control-title">' . esc_html( $this->label ) . '</span>';
if ( $this->description ) {
echo '<span class="description customize-control-description">'
. esc_html( $this->description ) . '</span>';
}
echo '<ul>';
foreach ( $this->choices as $slug => $name ) {
$checked = checked( $this->value(), $slug, false );
list( $h, $b ) = explode( '-', $slug, 2 );
printf(
'<li><label class="preset-card">'
. '<input type="radio" data-customize-setting-link="%1$s" value="%2$s"%3$s />'
. '<span class="preset-card__heading" style="font-family:\'%4$s\';">Heading</span>'
. '<span class="preset-card__body" style="font-family:\'%5$s\';">Body text</span>'
. '</label></li>',
esc_attr( $this->id ), esc_attr( $slug ), $checked,
esc_attr( ucwords( str_replace('-', ' ', $h)) ),
esc_attr( ucwords( str_replace('-', ' ', $b)) )
);
}
echo '</ul>';
}
}
}
SCSS & JS
- Imported into my normal
assets/css/sass/main.scss
andassets/js/customizer.js
builds. - Enqueued in the panel via
customize_controls_enqueue_scripts
and preview viacustomize_preview_init
Errors & Symptoms
Blank Typography section, despite
register_typography_control()
firing in the logs.If I try array‐style or
register_control_type()
, it instead renders a plain or radio list.Encountered duplicate‐class “Cannot declare class Zero_Control_Typography” until I deleted old files.
Tried OPcache resets, restarting Docker, multiple include patterns—still no card UI.
Questions
- Why is WP not rendering my
Zero_Control_Typography::render_content()
output? - Is there a necessary hook priority or missing argument I’m overlooking?
- What’s the minimal, fool-proof way to ensure WP uses my custom control subclass rather than falling back?
- Are there any Astra‐style patterns (specific enqueue hooks, control registration order) I should mimic?
Any guidance or working minimal example would be hugely appreciated—thanks!
Problem
I’m building a WordPress theme and want a Global → Typography → Font Presets control in the Customizer that shows a grid of clickable cards (each card previews a heading/body Google-Font pair). Instead of my custom card UI, the section is either blank or falls back to a basic <select>
(or radio list) control. I’ve tried many variations of register_control_type()
, direct instantiation, OPcache resets, and cleanup of duplicate classes, but no luck.
What I’ve Done
Autoloader in functions.php
(recursively includes /inc/
files):
// in functions.php
$rii = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( __DIR__ . '/inc' )
);
foreach ( $rii as $file ) {
if ( ! $file->isDir() && $file->getExtension() === 'php' ) {
require_once $file->getPathname();
}
}
Bootstrap singleton in inc/class-zero-customizer.php
:
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
class Zero_Customizer {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
self::$instance->hooks();
}
return self::$instance;
}
private function hooks() {
error_log( 'Zero: hooks() running' );
add_action( 'customize_register', [ $this, 'register_typography_control' ] );
add_action( 'customize_controls_enqueue_scripts',[ $this, 'enqueue_control_assets' ] );
add_action( 'customize_preview_init', [ $this, 'enqueue_preview_assets' ] );
}
public function register_typography_control( $wp_customize ) {
error_log( 'Zero: register_typography_control() fired' );
require_once __DIR__ . '/customizer/controls/class-zero-control-typography.php';
// Panel & section
if ( ! $wp_customize->get_panel( 'zero_global_panel' ) ) {
$wp_customize->add_panel( 'zero_global_panel', [
'title' => __( 'Global Settings', 'zero' ),
'priority' => 10,
] );
}
$wp_customize->add_section( 'zero_typography_section', [
'title' => __( 'Typography', 'zero' ),
'panel' => 'zero_global_panel',
'priority' => 10,
] );
error_log( 'Zero: added section zero_typography_section' );
// Presets & setting
$presets = [
'playfair-open-sans' => esc_html__( 'Playfair Display / Open Sans', 'zero' ),
/* …other 9 pairs… */
];
$wp_customize->add_setting( 'zero_typography_preset', [
'default' => 'playfair-open-sans',
'sanitize_callback' => function( $val ) use ( $presets ) {
return isset( $presets[ $val ] ) ? $val : 'playfair-open-sans';
},
'transport' => 'postMessage',
] );
// Direct instantiation of custom control
$wp_customize->add_control( new Zero_Control_Typography(
$wp_customize,
'zero_typography_preset',
[
'label' => __( 'Font Presets', 'zero' ),
'description' => __( 'Click a card to choose your Heading/Body pair.', 'zero' ),
'section' => 'zero_typography_section',
'choices' => $presets,
]
) );
error_log( 'Zero: added custom-control for zero_typography_preset' );
}
public function enqueue_control_assets() {
// Load panel CSS & JS
wp_enqueue_style(
'zero-customizer-controls',
get_stylesheet_directory_uri() . '/assets/dist/css/main.min.css',
[], filemtime( get_stylesheet_directory() . '/assets/dist/css/main.min.css' )
);
wp_enqueue_script(
'zero-customizer-controls',
get_stylesheet_directory_uri() . '/assets/dist/js/customizer.bundle.js',
[ 'jquery','customize-controls' ],
filemtime( get_stylesheet_directory() . '/assets/dist/js/customizer.bundle.js' ),
true
);
}
public function enqueue_preview_assets() {
// Load iframe JS for live preview
wp_enqueue_script(
'zero-customizer-preview',
get_stylesheet_directory_uri() . '/assets/dist/js/customizer.bundle.js',
[ 'jquery','customize-preview' ],
filemtime( get_stylesheet_directory() . '/assets/dist/js/customizer.bundle.js' ),
true
);
}
}
add_action( 'after_setup_theme', [ 'Zero_Customizer', 'get_instance' ] );
Custom Control in inc/customizer/controls/class-zero-control-typography.php
:
<?php
if ( ! class_exists( 'WP_Customize_Control' ) ) {
return;
}
if ( ! class_exists( 'Zero_Control_Typography' ) ) {
class Zero_Control_Typography extends WP_Customize_Control {
public $type = 'typography';
public function render_content() {
error_log( 'Zero: Zero_Control_Typography::render_content()' );
if ( empty( $this->choices ) ) {
return;
}
echo '<span class="customize-control-title">' . esc_html( $this->label ) . '</span>';
if ( $this->description ) {
echo '<span class="description customize-control-description">'
. esc_html( $this->description ) . '</span>';
}
echo '<ul>';
foreach ( $this->choices as $slug => $name ) {
$checked = checked( $this->value(), $slug, false );
list( $h, $b ) = explode( '-', $slug, 2 );
printf(
'<li><label class="preset-card">'
. '<input type="radio" data-customize-setting-link="%1$s" value="%2$s"%3$s />'
. '<span class="preset-card__heading" style="font-family:\'%4$s\';">Heading</span>'
. '<span class="preset-card__body" style="font-family:\'%5$s\';">Body text</span>'
. '</label></li>',
esc_attr( $this->id ), esc_attr( $slug ), $checked,
esc_attr( ucwords( str_replace('-', ' ', $h)) ),
esc_attr( ucwords( str_replace('-', ' ', $b)) )
);
}
echo '</ul>';
}
}
}
SCSS & JS
- Imported into my normal
assets/css/sass/main.scss
andassets/js/customizer.js
builds. - Enqueued in the panel via
customize_controls_enqueue_scripts
and preview viacustomize_preview_init
Errors & Symptoms
Blank Typography section, despite
register_typography_control()
firing in the logs.If I try array‐style or
register_control_type()
, it instead renders a plain or radio list.Encountered duplicate‐class “Cannot declare class Zero_Control_Typography” until I deleted old files.
Tried OPcache resets, restarting Docker, multiple include patterns—still no card UI.
Questions
- Why is WP not rendering my
Zero_Control_Typography::render_content()
output? - Is there a necessary hook priority or missing argument I’m overlooking?
- What’s the minimal, fool-proof way to ensure WP uses my custom control subclass rather than falling back?
- Are there any Astra‐style patterns (specific enqueue hooks, control registration order) I should mimic?
Any guidance or working minimal example would be hugely appreciated—thanks!
Share Improve this question asked May 2 at 16:11 Rajiv SarkarRajiv Sarkar 11 Answer
Reset to default 0If your custom control's render_content()
is not being called, the most likely cause is either a PHP error preventing the class from being loaded, or the file is not being included at all. Double-check your file paths and logs. Otherwise, your approach is solid and follows best practices for custom controls in the WordPress Customizer.
Minimal Working Example
1. Add to your theme's functions.php
or a customizer file:
add_action( 'customize_register', 'zero_register_typography_control' );
function zero_register_typography_control( $wp_customize ) {
require_once get_template_directory() . '/inc/customizer/controls/class-zero-control-typography.php';
$wp_customize->add_section( 'zero_typography_section', [
'title' => __( 'Typography', 'zero' ),
'priority' => 10,
] );
$presets = [
'playfair-open-sans' => esc_html__( 'Playfair Display / Open Sans', 'zero' ),
];
$wp_customize->add_setting( 'zero_typography_preset', [
'default' => 'playfair-open-sans',
'sanitize_callback' => function( $val ) use ( $presets ) {
return isset( $presets[ $val ] ) ? $val : 'playfair-open-sans';
},
'transport' => 'postMessage',
] );
$wp_customize->add_control( new Zero_Control_Typography(
$wp_customize,
'zero_typography_preset',
[
'label' => __( 'Font Presets', 'zero' ),
'section' => 'zero_typography_section',
'choices' => $presets,
]
) );
}
2. Place this in /inc/customizer/controls/class-zero-control-typography.php
:
if ( ! class_exists( 'WP_Customize_Control' ) ) return;
class Zero_Control_Typography extends WP_Customize_Control {
public $type = 'typography';
public function render_content() {
if ( empty( $this->choices ) ) return;
echo '<span class="customize-control-title">' . esc_html( $this->label ) . '</span>';
echo '<ul>';
foreach ( $this->choices as $slug => $name ) {
$checked = checked( $this->value(), $slug, false );
list( $h, $b ) = explode( '-', $slug, 2 );
printf(
'<li><label class="preset-card">'
. '<input type="radio" data-customize-setting-link="%1$s" value="%2$s"%3$s />'
. '<span class="preset-card__heading" style="font-family:\'%4$s\';">Heading</span>'
. '<span class="preset-card__body" style="font-family:\'%5$s\';">Body text</span>'
. '</label></li>',
esc_attr( $this->id ), esc_attr( $slug ), $checked,
esc_attr( ucwords( str_replace('-', ' ', $h)) ),
esc_attr( ucwords( str_replace('-', ' ', $b)) )
);
}
echo '</ul>';
}
}