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
assets/css/sass/main.scss
and assets/js/customizer.js
builds.customize_controls_enqueue_scripts
and preview via customize_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
Zero_Control_Typography::render_content()
output?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
assets/css/sass/main.scss
and assets/js/customizer.js
builds.customize_controls_enqueue_scripts
and preview via customize_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
Zero_Control_Typography::render_content()
output?Any guidance or working minimal example would be hugely appreciated—thanks!
If 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.
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,
]
) );
}
/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>';
}
}