Master WordPress theme development - theme structure, functions.php, template files, child themes, theme customizer, widgets, menus, and custom page templates.
Master WordPress theme development - theme structure, functions.php, template files, child themes, theme customizer, widgets, menus, and custom page templates.
WordPress theme structure and required files
functions.php and theme setup hooks
Template hierarchy and template files
Child themes and theme inheritance
Theme Customizer API
Custom widgets and sidebars
Navigation menus and menu walkers
Custom page templates and conditional tags
Theme localization and internationalization
Theme security and best practices
Learn about 1. WordPress Theme Structure.
A WordPress theme requires at minimum two files:
.
However, a production-ready theme includes many more files for different template types and
functionality.
Standard WordPress Theme Structure
1mytheme/
2├── style.css # Main stylesheet (REQUIRED, includes theme metadata)
3├── index.php # Main template fallback (REQUIRED)
4├── functions.php # Theme functions and features
5├── screenshot.png # Theme preview (880x660px recommended)
6│
7├── header.php # Header template
8├── footer.php # Footer template
9├── sidebar.php # Default sidebar template
10│
11├── front-page.php # Front page template
12├── home.php # Blog posts index template
13├── single.php # Single post template
14├── page.php # Page template
15├── archive.php # Archive template
16├── category.php # Category archive template
17├── tag.php # Tag archive template
18├── author.php # Author archive template
19├── date.php # Date archive template
20├── search.php # Search results template
21├── 404.php # 404 error template
22│
23├── comments.php # Comments template
24├── attachment.php # Attachment template
25│
26├── template-parts/ # Reusable template parts
27│ ├── content.php # Default content template
28│ ├── content-single.php # Single post content
29│ ├── content-page.php # Page content
30│ └── content-none.php # No results content
31│
32├── inc/ # Helper functions and features
33│ ├── customizer.php # Theme Customizer settings
34│ ├── template-functions.php
35│ ├── template-tags.php
36│ └── custom-widgets.php
37│
38├── assets/ # Theme assets
39│ ├── css/
40│ │ ├── main.css
41│ │ └── admin.css
42│ ├── js/
43│ │ ├── navigation.js
44│ │ ├── customizer.js
45│ │ └── main.js
46│ └── images/
47│
48├── languages/ # Translation files
49│ └── mytheme.pot
50│
51└── page-templates/ # Custom page templates
52 ├── full-width.php
53 ├── landing-page.php
54 └── contact.phpfile must include a header comment that provides metadata about
the theme. This is how WordPress identifies and displays theme information in the admin area.
1/*
2Theme Name: My Custom Theme
3Theme URI: https://example.com/my-custom-theme
4Author: Your Name
5Author URI: https://example.com
6Description: A powerful, flexible WordPress theme built for modern websites
7Version: 1.0.0
8Requires at least: 6.0
9Tested up to: 6.4
10Requires PHP: 8.0
11License: GNU General Public License v2 or later
12License URI: http://www.gnu.org/licenses/gpl-2.0.html
13Text Domain: my-custom-theme
14Domain Path: /languages
15Tags: blog, custom-menu, featured-images, threaded-comments, translation-ready
16*/
17
18/*
19 * Theme Styles
20 * ============================================================================
21 */
22
23/* Normalize and Reset */
24*, *::before, *::after {
25 box-sizing: border-box;
26}
27
28body {
29 margin: 0;
30 padding: 0;
31 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
32 line-height: 1.6;
33 color: #333;
34}
35
36/* Add your theme styles here */Learn about 2. The functions.php File.
functions.php
file is the heart of your theme's functionality. It's where
you register theme support for various WordPress features, enqueue scripts and styles,
register menus and sidebars, and add custom functionality.
1<?php
2/**
3 * Theme Functions
4 *
5 * @package MyCustomTheme
6 */
7
8// Exit if accessed directly
9if (!defined('ABSPATH')) {
10 exit;
11}
12
13/**
14 * Theme Setup
15 */
16function mytheme_setup() {
17 // Add default posts and comments RSS feed links to head
18 add_theme_support('automatic-feed-links');
19
20 // Let WordPress manage the document title
21 add_theme_support('title-tag');
22
23 // Enable support for Post Thumbnails
24 add_theme_support('post-thumbnails');
25
26 // Set custom image sizes
27 add_image_size('mytheme-featured', 1200, 630, true);
28 add_image_size('mytheme-thumbnail', 400, 300, true);
29 add_image_size('mytheme-medium', 800, 600, true);
30
31 // Register navigation menus
32 register_nav_menus(array(
33 'primary' => __('Primary Menu', 'my-custom-theme'),
34 'footer' => __('Footer Menu', 'my-custom-theme'),
35 'social' => __('Social Links Menu', 'my-custom-theme'),
36 ));
37
38 // Switch default core markup to output valid HTML5
39 add_theme_support('html5', array(
40 'search-form',
41 'comment-form',
42 'comment-list',
43 'gallery',
44 'caption',
45 'style',
46 'script',
47 ));
48
49 // Add theme support for selective refresh for widgets
50 add_theme_support('customize-selective-refresh-widgets');
51
52 // Add support for custom logo
53 add_theme_support('custom-logo', array(
54 'height' => 100,
55 'width' => 400,
56 'flex-height' => true,
57 'flex-width' => true,
58 ));
59
60 // Add support for custom header
61 add_theme_support('custom-header', array(
62 'default-image' => '',
63 'width' => 1920,
64 'height' => 600,
65 'flex-height' => true,
66 'flex-width' => true,
67 'header-text' => true,
68 'default-text-color' => '000000',
69 ));
70
71 // Add support for custom background
72 add_theme_support('custom-background', array(
73 'default-color' => 'ffffff',
74 ));
75
76 // Add support for wide and full alignment (Gutenberg)
77 add_theme_support('align-wide');
78
79 // Add support for responsive embeds
80 add_theme_support('responsive-embeds');
81
82 // Add support for editor styles
83 add_theme_support('editor-styles');
84 add_editor_style('assets/css/editor-style.css');
85
86 // Define content width
87 if (!isset($content_width)) {
88 $content_width = 1200;
89 }
90
91 // Load theme text domain for translations
92 load_theme_textdomain('my-custom-theme', get_template_directory() . '/languages');
93}
94add_action('after_setup_theme', 'mytheme_setup');
95
96/**
97 * Set the content width in pixels
98 */
99function mytheme_content_width() {
100 $GLOBALS['content_width'] = apply_filters('mytheme_content_width', 1200);
101}
102add_action('after_setup_theme', 'mytheme_content_width', 0);Always enqueue scripts and styles properly using WordPress's built-in functions. Never
hardcode script and style tags in your templates.
1/**
2 * Enqueue scripts and styles
3 */
4function mytheme_enqueue_scripts() {
5 // Main stylesheet
6 wp_enqueue_style(
7 'mytheme-style',
8 get_stylesheet_uri(),
9 array(),
10 wp_get_theme()->get('Version')
11 );
12
13 // Additional stylesheets
14 wp_enqueue_style(
15 'mytheme-main-css',
16 get_template_directory_uri() . '/assets/css/main.css',
17 array('mytheme-style'),
18 wp_get_theme()->get('Version')
19 );
20
21 // Google Fonts (example)
22 wp_enqueue_style(
23 'mytheme-google-fonts',
24 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
25 array(),
26 null
27 );
28
29 // Main JavaScript file
30 wp_enqueue_script(
31 'mytheme-main-js',
32 get_template_directory_uri() . '/assets/js/main.js',
33 array('jquery'),
34 wp_get_theme()->get('Version'),
35 true // Load in footer
36 );
37
38 // Navigation JavaScript
39 wp_enqueue_script(
40 'mytheme-navigation',
41 get_template_directory_uri() . '/assets/js/navigation.js',
42 array(),
43 wp_get_theme()->get('Version'),
44 true
45 );
46
47 // Localize script - pass data from PHP to JavaScript
48 wp_localize_script('mytheme-main-js', 'mythemeData', array(
49 'ajaxUrl' => admin_url('admin-ajax.php'),
50 'nonce' => wp_create_nonce('mytheme-nonce'),
51 'themePath' => get_template_directory_uri(),
52 ));
53
54 // Comments reply script for threaded comments
55 if (is_singular() && comments_open() && get_option('thread_comments')) {
56 wp_enqueue_script('comment-reply');
57 }
58}
59add_action('wp_enqueue_scripts', 'mytheme_enqueue_scripts');
60
61/**
62 * Enqueue admin scripts and styles
63 */
64function mytheme_admin_enqueue_scripts($hook) {
65 // Only load on specific admin pages if needed
66 if ('post.php' !== $hook && 'post-new.php' !== $hook) {
67 return;
68 }
69
70 wp_enqueue_style(
71 'mytheme-admin-css',
72 get_template_directory_uri() . '/assets/css/admin.css',
73 array(),
74 wp_get_theme()->get('Version')
75 );
76}
77add_action('admin_enqueue_scripts', 'mytheme_admin_enqueue_scripts');Learn about 2.3 Registering Sidebars and Widget Areas.
1/**
2 * Register widget areas
3 */
4function mytheme_widgets_init() {
5 // Primary sidebar
6 register_sidebar(array(
7 'name' => __('Primary Sidebar', 'my-custom-theme'),
8 'id' => 'sidebar-1',
9 'description' => __('Main sidebar that appears on the right.', 'my-custom-theme'),
10 'before_widget' => '<section id="%1$s" class="widget %2$s">',
11 'after_widget' => '</section>',
12 'before_title' => '<h2 class="widget-title">',
13 'after_title' => '</h2>',
14 ));
15
16 // Footer widget areas
17 $footer_widget_regions = 4;
18 for ($i = 1; $i <= $footer_widget_regions; $i++) {
19 register_sidebar(array(
20 'name' => sprintf(__('Footer Widget Area %d', 'my-custom-theme'), $i),
21 'id' => sprintf('footer-%d', $i),
22 'description' => sprintf(__('Footer widget area %d', 'my-custom-theme'), $i),
23 'before_widget' => '<div id="%1$s" class="widget %2$s">',
24 'after_widget' => '</div>',
25 'before_title' => '<h3 class="widget-title">',
26 'after_title' => '</h3>',
27 ));
28 }
29
30 // Shop sidebar (for WooCommerce)
31 if (class_exists('WooCommerce')) {
32 register_sidebar(array(
33 'name' => __('Shop Sidebar', 'my-custom-theme'),
34 'id' => 'sidebar-shop',
35 'description' => __('Sidebar for WooCommerce pages.', 'my-custom-theme'),
36 'before_widget' => '<aside id="%1$s" class="widget %2$s">',
37 'after_widget' => '</aside>',
38 'before_title' => '<h2 class="widget-title">',
39 'after_title' => '</h2>',
40 ));
41 }
42}
43add_action('widgets_init', 'mytheme_widgets_init');Learn about 3. Template Files and the Template Hierarchy.
WordPress uses a template hierarchy to determine which template file to use for rendering
different types of content. Understanding this hierarchy is crucial for theme development.
Template Hierarchy Flow
1Front Page (Homepage):
2 front-page.php → home.php → index.php
3
4Blog Posts Page:
5 home.php → index.php
6
7Single Post:
8 single-{post-type}-{slug}.php → single-{post-type}.php → single.php → singular.php → index.php
9
10Page:
11 page-{slug}.php → page-{id}.php → page.php → singular.php → index.php
12
13Category Archive:
14 category-{slug}.php → category-{id}.php → category.php → archive.php → index.php
15
16Tag Archive:
17 tag-{slug}.php → tag-{id}.php → tag.php → archive.php → index.php
18
19Custom Taxonomy Archive:
20 taxonomy-{taxonomy}-{term}.php → taxonomy-{taxonomy}.php → taxonomy.php → archive.php → index.php
21
22Author Archive:
23 author-{nicename}.php → author-{id}.php → author.php → archive.php → index.php
24
25Date Archive:
26 date.php → archive.php → index.php
27
28Search Results:
29 search.php → index.php
30
31404 Error:
32 404.php → index.php
33
34Attachment:
35 attachment.php → single-attachment-{type}.php → single.php → index.phpLet's create the essential template files for a WordPress theme.
header.php
footer.php
single.php
1<?php
2/**
3 * The header template
4 *
5 * @package MyCustomTheme
6 */
7?>
8<!DOCTYPE html>
9<html <?php language_attributes(); ?>>
10<head>
11 <meta charset="<?php bloginfo('charset'); ?>">
12 <meta name="viewport" content="width=device-width, initial-scale=1.0">
13 <?php wp_head(); ?>
14</head>
15
16<body <?php body_class(); ?>>
17<?php wp_body_open(); ?>
18
19<div id="page" class="site">
20 <a class="skip-link screen-reader-text" href="#primary">
21 <?php esc_html_e('Skip to content', 'my-custom-theme'); ?>
22 </a>
23
24 <header id="masthead" class="site-header">
25 <div class="container">
26 <div class="site-branding">
27 <?php
28 if (has_custom_logo()) {
29 the_custom_logo();
30 } else {
31 ?>
32 <h1 class="site-title">
33 <a href="<?php echo esc_url(home_url('/')); ?>" rel="home">
34 <?php bloginfo('name'); ?>
35 </a>
36 </h1>
37 <?php
38 $description = get_bloginfo('description', 'display');
39 if ($description || is_customize_preview()) {
40 ?>
41 <p class="site-description"><?php echo $description; ?></p>
42 <?php
43 }
44 }
45 ?>
46 </div><!-- .site-branding -->
47
48 <nav id="site-navigation" class="main-navigation">
49 <button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
50 <?php esc_html_e('Menu', 'my-custom-theme'); ?>
51 </button>
52 <?php
53 wp_nav_menu(array(
54 'theme_location' => 'primary',
55 'menu_id' => 'primary-menu',
56 'container' => 'div',
57 'container_class' => 'menu-primary-container',
58 'fallback_cb' => false,
59 ));
60 ?>
61 </nav><!-- #site-navigation -->
62 </div><!-- .container -->
63 </header><!-- #masthead -->1<?php
2/**
3 * The footer template
4 *
5 * @package MyCustomTheme
6 */
7?>
8
9 <footer id="colophon" class="site-footer">
10 <?php if (is_active_sidebar('footer-1') || is_active_sidebar('footer-2') ||
11 is_active_sidebar('footer-3') || is_active_sidebar('footer-4')) : ?>
12 <div class="footer-widgets">
13 <div class="container">
14 <div class="footer-widgets-grid">
15 <?php for ($i = 1; $i <= 4; $i++) : ?>
16 <?php if (is_active_sidebar("footer-$i")) : ?>
17 <div class="footer-widget-area footer-widget-<?php echo $i; ?>">
18 <?php dynamic_sidebar("footer-$i"); ?>
19 </div>
20 <?php endif; ?>
21 <?php endfor; ?>
22 </div>
23 </div>
24 </div><!-- .footer-widgets -->
25 <?php endif; ?>
26
27 <div class="site-info">
28 <div class="container">
29 <div class="footer-content">
30 <div class="copyright">
31 <?php
32 printf(
33 esc_html__('© %1$s %2$s. All rights reserved.', 'my-custom-theme'),
34 date('Y'),
35 get_bloginfo('name')
36 );
37 ?>
38 </div>
39
40 <?php
41 if (has_nav_menu('footer')) {
42 wp_nav_menu(array(
43 'theme_location' => 'footer',
44 'menu_id' => 'footer-menu',
45 'menu_class' => 'footer-menu',
46 'container' => 'nav',
47 'container_class' => 'footer-navigation',
48 'depth' => 1,
49 ));
50 }
51 ?>
52 </div>
53 </div><!-- .container -->
54 </div><!-- .site-info -->
55 </footer><!-- #colophon -->
56</div><!-- #page -->
57
58<?php wp_footer(); ?>
59
60</body>
61</html>1<?php
2/**
3 * The main template file
4 *
5 * @package MyCustomTheme
6 */
7
8get_header();
9?>
10
11<main id="primary" class="site-main">
12 <div class="container">
13 <div class="content-area">
14 <?php
15 if (have_posts()) :
16
17 // Check if this is the blog page
18 if (is_home() && !is_front_page()) :
19 ?>
20 <header class="page-header">
21 <h1 class="page-title">
22 <?php single_post_title(); ?>
23 </h1>
24 </header>
25 <?php
26 endif;
27
28 // Start the Loop
29 while (have_posts()) :
30 the_post();
31
32 /*
33 * Include the Post-Type-specific template for the content.
34 * If you want to override this in a child theme, include a file
35 * called content-{post_type}.php and that will be used instead.
36 */
37 get_template_part('template-parts/content', get_post_type());
38
39 endwhile;
40
41 // Pagination
42 the_posts_pagination(array(
43 'mid_size' => 2,
44 'prev_text' => __('« Previous', 'my-custom-theme'),
45 'next_text' => __('Next »', 'my-custom-theme'),
46 ));
47
48 else :
49
50 get_template_part('template-parts/content', 'none');
51
52 endif;
53 ?>
54 </div><!-- .content-area -->
55
56 <?php get_sidebar(); ?>
57 </div><!-- .container -->
58</main><!-- #primary -->
59
60<?php
61get_footer();1<?php
2/**
3 * The template for displaying single posts
4 *
5 * @package MyCustomTheme
6 */
7
8get_header();
9?>
10
11<main id="primary" class="site-main">
12 <div class="container">
13 <div class="content-area">
14 <?php
15 while (have_posts()) :
16 the_post();
17
18 get_template_part('template-parts/content', 'single');
19
20 // Post navigation
21 the_post_navigation(array(
22 'prev_text' => '<span class="nav-subtitle">' .
23 esc_html__('Previous:', 'my-custom-theme') . '</span> ' .
24 '<span class="nav-title">%title</span>',
25 'next_text' => '<span class="nav-subtitle">' .
26 esc_html__('Next:', 'my-custom-theme') . '</span> ' .
27 '<span class="nav-title">%title</span>',
28 ));
29
30 // If comments are open or there is at least one comment, load the comment template
31 if (comments_open() || get_comments_number()) :
32 comments_template();
33 endif;
34
35 endwhile;
36 ?>
37 </div><!-- .content-area -->
38
39 <?php get_sidebar(); ?>
40 </div><!-- .container -->
41</main><!-- #primary -->
42
43<?php
44get_footer();1<?php
2/**
3 * The template for displaying pages
4 *
5 * @package MyCustomTheme
6 */
7
8get_header();
9?>
10
11<main id="primary" class="site-main">
12 <div class="container">
13 <div class="content-area">
14 <?php
15 while (have_posts()) :
16 the_post();
17
18 get_template_part('template-parts/content', 'page');
19
20 // If comments are open or there is at least one comment, load the comment template
21 if (comments_open() || get_comments_number()) :
22 comments_template();
23 endif;
24
25 endwhile;
26 ?>
27 </div><!-- .content-area -->
28 </div><!-- .container -->
29</main><!-- #primary -->
30
31<?php
32get_footer();Template parts are reusable sections of templates that can be included using
get_template_part()
template-parts/content.php
1<?php
2/**
3 * Template part for displaying posts
4 *
5 * @package MyCustomTheme
6 */
7?>
8
9<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
10 <header class="entry-header">
11 <?php
12 if (is_singular()) :
13 the_title('<h1 class="entry-title">', '</h1>');
14 else :
15 the_title('<h2 class="entry-title"><a href="' . esc_url(get_permalink()) . '" rel="bookmark">', '</a></h2>');
16 endif;
17
18 if ('post' === get_post_type()) :
19 ?>
20 <div class="entry-meta">
21 <span class="posted-on">
22 <?php echo get_the_date(); ?>
23 </span>
24 <span class="byline">
25 <?php
26 printf(
27 esc_html__('by %s', 'my-custom-theme'),
28 '<a href="' . esc_url(get_author_posts_url(get_the_author_meta('ID'))) . '">' .
29 esc_html(get_the_author()) . '</a>'
30 );
31 ?>
32 </span>
33 <span class="cat-links">
34 <?php
35 $categories_list = get_the_category_list(esc_html__(', ', 'my-custom-theme'));
36 if ($categories_list) {
37 printf(esc_html__('in %s', 'my-custom-theme'), $categories_list);
38 }
39 ?>
40 </span>
41 <span class="comments-link">
42 <?php
43 if (!post_password_required() && (comments_open() || get_comments_number())) {
44 echo '<span class="sep"> | </span>';
45 comments_popup_link(
46 esc_html__('Leave a comment', 'my-custom-theme'),
47 esc_html__('1 Comment', 'my-custom-theme'),
48 esc_html__('% Comments', 'my-custom-theme')
49 );
50 }
51 ?>
52 </span>
53 </div><!-- .entry-meta -->
54 <?php
55 endif;
56 ?>
57 </header><!-- .entry-header -->
58
59 <?php if (has_post_thumbnail()) : ?>
60 <div class="post-thumbnail">
61 <?php
62 if (is_singular()) :
63 the_post_thumbnail('mytheme-featured', array('class' => 'img-fluid'));
64 else :
65 ?>
66 <a href="<?php the_permalink(); ?>">
67 <?php the_post_thumbnail('mytheme-featured', array('class' => 'img-fluid')); ?>
68 </a>
69 <?php
70 endif;
71 ?>
72 </div><!-- .post-thumbnail -->
73 <?php endif; ?>
74
75 <div class="entry-content">
76 <?php
77 if (is_singular()) :
78 the_content();
79
80 wp_link_pages(array(
81 'before' => '<div class="page-links">' . esc_html__('Pages:', 'my-custom-theme'),
82 'after' => '</div>',
83 ));
84 else :
85 the_excerpt();
86 ?>
87 <a href="<?php the_permalink(); ?>" class="read-more">
88 <?php esc_html_e('Read More', 'my-custom-theme'); ?>
89 </a>
90 <?php
91 endif;
92 ?>
93 </div><!-- .entry-content -->
94
95 <?php if (is_singular() && 'post' === get_post_type()) : ?>
96 <footer class="entry-footer">
97 <?php
98 $tags_list = get_the_tag_list('', esc_html__(', ', 'my-custom-theme'));
99 if ($tags_list) {
100 printf('<span class="tags-links">' . esc_html__('Tagged %s', 'my-custom-theme') . '</span>', $tags_list);
101 }
102 ?>
103 </footer><!-- .entry-footer -->
104 <?php endif; ?>
105</article><!-- #post-<?php the_ID(); ?> -->Learn about 4. Child Themes.
A child theme is a theme that inherits functionality and styling from another theme (the parent theme).
Child themes allow you to modify or add to the functionality of the parent theme without touching
the parent theme files, which means your changes won't be lost when the parent theme is updated.
When to Use a Child Theme
Customizing a third-party theme:
Always use a child theme to preserve your customizations
Learning theme development:
Experiment without breaking the parent theme
Client projects:
Maintain update compatibility while customizing
A/B testing:
Test design variations
Creating a child theme requires only two files:
functions.php
Child Theme Structure
Child Theme style.css
Child Theme functions.php
1mytheme-child/
2├── style.css # Child theme stylesheet (REQUIRED)
3├── functions.php # Child theme functions (REQUIRED)
4├── screenshot.png # Child theme preview
5├── template-parts/ # Override parent template parts
6├── page-templates/ # Custom page templates
7└── assets/ # Child theme assets1/*
2Theme Name: My Custom Theme Child
3Theme URI: https://example.com/my-custom-theme-child
4Description: Child theme for My Custom Theme
5Author: Your Name
6Author URI: https://example.com
7Template: my-custom-theme
8Version: 1.0.0
9License: GNU General Public License v2 or later
10License URI: http://www.gnu.org/licenses/gpl-2.0.html
11Text Domain: my-custom-theme-child
12*/
13
14/*
15 * The "Template" field is REQUIRED - it must match the directory name
16 * of the parent theme exactly (case-sensitive)
17 */
18
19/* Your custom styles here */
20.custom-header {
21 background-color: #f0f0f0;
22 padding: 20px;
23}
24
25/* Override parent theme styles */
26.site-title {
27 font-size: 2.5rem;
28 color: #333;
29}1<?php
2/**
3 * Child Theme Functions
4 *
5 * @package MyCustomThemeChild
6 */
7
8// Exit if accessed directly
9if (!defined('ABSPATH')) {
10 exit;
11}
12
13/**
14 * Enqueue parent and child theme styles
15 */
16function mytheme_child_enqueue_styles() {
17 // Enqueue parent theme stylesheet
18 wp_enqueue_style(
19 'mytheme-parent-style',
20 get_template_directory_uri() . '/style.css',
21 array(),
22 wp_get_theme()->parent()->get('Version')
23 );
24
25 // Enqueue child theme stylesheet
26 wp_enqueue_style(
27 'mytheme-child-style',
28 get_stylesheet_uri(),
29 array('mytheme-parent-style'),
30 wp_get_theme()->get('Version')
31 );
32}
33add_action('wp_enqueue_scripts', 'mytheme_child_enqueue_styles');
34
35/**
36 * Add custom functionality here
37 *
38 * You can override parent theme functions by using the same function name
39 * with higher priority, or by using remove_action/remove_filter
40 */
41
42// Example: Add custom image size
43add_action('after_setup_theme', function() {
44 add_image_size('mytheme-child-hero', 1920, 800, true);
45});
46
47// Example: Modify parent theme function output
48function mytheme_child_custom_excerpt_length($length) {
49 return 30; // Change excerpt length to 30 words
50}
51add_filter('excerpt_length', 'mytheme_child_custom_excerpt_length', 999);To override a parent theme template file, simply copy it to your child theme with the same
directory structure and filename. WordPress will automatically use the child theme version.
1Parent Theme: mytheme/single.php
2Child Override: mytheme-child/single.php (WordPress uses this)
3
4Parent Theme: mytheme/template-parts/content.php
5Child Override: mytheme-child/template-parts/content.php (WordPress uses this)
6
7Parent Theme: mytheme/page-templates/full-width.php
8Child Override: mytheme-child/page-templates/full-width.php (WordPress uses this)Learn about 5. Theme Customizer API.
The Theme Customizer provides a user-friendly interface for theme settings with live preview.
It's the recommended way to add theme options in modern WordPress themes.
Create a file
inc/customizer.php
and include it in
functions.php
1<?php
2/**
3 * Theme Customizer
4 *
5 * @package MyCustomTheme
6 */
7
8/**
9 * Add postMessage support for site title and description
10 */
11function mytheme_customize_register($wp_customize) {
12
13 // Modify existing controls
14 $wp_customize->get_setting('blogname')->transport = 'postMessage';
15 $wp_customize->get_setting('blogdescription')->transport = 'postMessage';
16
17 if (isset($wp_customize->selective_refresh)) {
18 $wp_customize->selective_refresh->add_partial('blogname', array(
19 'selector' => '.site-title a',
20 'render_callback' => function() {
21 bloginfo('name');
22 },
23 ));
24 }
25
26 /*
27 * Add custom section: Theme Options
28 */
29 $wp_customize->add_section('mytheme_options', array(
30 'title' => __('Theme Options', 'my-custom-theme'),
31 'description' => __('Customize your theme settings', 'my-custom-theme'),
32 'priority' => 30,
33 ));
34
35 /*
36 * Setting: Display Author Bio
37 */
38 $wp_customize->add_setting('mytheme_display_author_bio', array(
39 'default' => true,
40 'sanitize_callback' => 'wp_validate_boolean',
41 'transport' => 'refresh',
42 ));
43
44 $wp_customize->add_control('mytheme_display_author_bio', array(
45 'label' => __('Display Author Bio', 'my-custom-theme'),
46 'section' => 'mytheme_options',
47 'type' => 'checkbox',
48 ));
49
50 /*
51 * Setting: Copyright Text
52 */
53 $wp_customize->add_setting('mytheme_copyright_text', array(
54 'default' => '',
55 'sanitize_callback' => 'sanitize_text_field',
56 'transport' => 'postMessage',
57 ));
58
59 $wp_customize->add_control('mytheme_copyright_text', array(
60 'label' => __('Copyright Text', 'my-custom-theme'),
61 'description' => __('Enter custom copyright text for the footer', 'my-custom-theme'),
62 'section' => 'mytheme_options',
63 'type' => 'text',
64 ));
65
66 /*
67 * Setting: Header Layout
68 */
69 $wp_customize->add_setting('mytheme_header_layout', array(
70 'default' => 'layout-1',
71 'sanitize_callback' => 'mytheme_sanitize_select',
72 ));
73
74 $wp_customize->add_control('mytheme_header_layout', array(
75 'label' => __('Header Layout', 'my-custom-theme'),
76 'section' => 'mytheme_options',
77 'type' => 'select',
78 'choices' => array(
79 'layout-1' => __('Layout 1: Logo Left, Menu Right', 'my-custom-theme'),
80 'layout-2' => __('Layout 2: Logo Center, Menu Below', 'my-custom-theme'),
81 'layout-3' => __('Layout 3: Logo Right, Menu Left', 'my-custom-theme'),
82 ),
83 ));
84
85 /*
86 * Section: Colors
87 */
88 $wp_customize->add_section('mytheme_colors', array(
89 'title' => __('Theme Colors', 'my-custom-theme'),
90 'priority' => 40,
91 ));
92
93 // Primary Color
94 $wp_customize->add_setting('mytheme_primary_color', array(
95 'default' => '#0073aa',
96 'sanitize_callback' => 'sanitize_hex_color',
97 'transport' => 'postMessage',
98 ));
99
100 $wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'mytheme_primary_color', array(
101 'label' => __('Primary Color', 'my-custom-theme'),
102 'section' => 'mytheme_colors',
103 )));
104
105 // Secondary Color
106 $wp_customize->add_setting('mytheme_secondary_color', array(
107 'default' => '#23282d',
108 'sanitize_callback' => 'sanitize_hex_color',
109 'transport' => 'postMessage',
110 ));
111
112 $wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'mytheme_secondary_color', array(
113 'label' => __('Secondary Color', 'my-custom-theme'),
114 'section' => 'mytheme_colors',
115 )));
116}
117add_action('customize_register', 'mytheme_customize_register');
118
119/**
120 * Sanitize select field
121 */
122function mytheme_sanitize_select($input, $setting) {
123 $input = sanitize_key($input);
124 $choices = $setting->manager->get_control($setting->id)->choices;
125 return (array_key_exists($input, $choices) ? $input : $setting->default);
126}
127
128/**
129 * Enqueue customizer scripts
130 */
131function mytheme_customize_preview_js() {
132 wp_enqueue_script(
133 'mytheme-customizer',
134 get_template_directory_uri() . '/assets/js/customizer.js',
135 array('customize-preview'),
136 wp_get_theme()->get('Version'),
137 true
138 );
139}
140add_action('customize_preview_init', 'mytheme_customize_preview_js');assets/js/customizer.js
for postMessage transport (live preview).
1/**
2 * Customizer Live Preview
3 */
4(function($) {
5 'use strict';
6
7 // Site title
8 wp.customize('blogname', function(value) {
9 value.bind(function(newval) {
10 $('.site-title a').text(newval);
11 });
12 });
13
14 // Site description
15 wp.customize('blogdescription', function(value) {
16 value.bind(function(newval) {
17 $('.site-description').text(newval);
18 });
19 });
20
21 // Copyright text
22 wp.customize('mytheme_copyright_text', function(value) {
23 value.bind(function(newval) {
24 $('.copyright').html(newval);
25 });
26 });
27
28 // Primary color
29 wp.customize('mytheme_primary_color', function(value) {
30 value.bind(function(newval) {
31 $('<style id="mytheme-primary-color">')
32 .html('a, .primary-color { color: ' + newval + '; }')
33 .appendTo('head');
34 $('#mytheme-primary-color').remove();
35 $('<style id="mytheme-primary-color">')
36 .html('a, .primary-color { color: ' + newval + '; }')
37 .appendTo('head');
38 });
39 });
40
41})(jQuery);Learn about 6. Custom Widgets.
Create a file
inc/custom-widgets.php
for your custom widget classes.
1<?php
2/**
3 * Custom Widgets
4 *
5 * @package MyCustomTheme
6 */
7
8/**
9 * Recent Posts Widget with Thumbnails
10 */
11class MyTheme_Recent_Posts_Widget extends WP_Widget {
12
13 public function __construct() {
14 parent::__construct(
15 'mytheme_recent_posts',
16 __('Recent Posts (Custom)', 'my-custom-theme'),
17 array(
18 'description' => __('Display recent posts with thumbnails', 'my-custom-theme'),
19 )
20 );
21 }
22
23 /**
24 * Widget output
25 */
26 public function widget($args, $instance) {
27 $title = !empty($instance['title']) ? $instance['title'] : __('Recent Posts', 'my-custom-theme');
28 $title = apply_filters('widget_title', $title, $instance, $this->id_base);
29 $number = !empty($instance['number']) ? absint($instance['number']) : 5;
30
31 echo $args['before_widget'];
32
33 if ($title) {
34 echo $args['before_title'] . $title . $args['after_title'];
35 }
36
37 $recent_posts = new WP_Query(array(
38 'posts_per_page' => $number,
39 'post_status' => 'publish',
40 'ignore_sticky_posts' => true,
41 ));
42
43 if ($recent_posts->have_posts()) :
44 ?>
45 <ul class="recent-posts-widget">
46 <?php while ($recent_posts->have_posts()) : $recent_posts->the_post(); ?>
47 <li class="recent-post-item">
48 <?php if (has_post_thumbnail()) : ?>
49 <div class="recent-post-thumbnail">
50 <a href="<?php the_permalink(); ?>">
51 <?php the_post_thumbnail('thumbnail'); ?>
52 </a>
53 </div>
54 <?php endif; ?>
55 <div class="recent-post-content">
56 <h4 class="recent-post-title">
57 <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
58 </h4>
59 <span class="recent-post-date"><?php echo get_the_date(); ?></span>
60 </div>
61 </li>
62 <?php endwhile; ?>
63 </ul>
64 <?php
65 wp_reset_postdata();
66 endif;
67
68 echo $args['after_widget'];
69 }
70
71 /**
72 * Widget form in admin
73 */
74 public function form($instance) {
75 $title = !empty($instance['title']) ? $instance['title'] : '';
76 $number = !empty($instance['number']) ? absint($instance['number']) : 5;
77 ?>
78 <p>
79 <label for="<?php echo $this->get_field_id('title'); ?>">
80 <?php esc_html_e('Title:', 'my-custom-theme'); ?>
81 </label>
82 <input class="widefat"
83 id="<?php echo $this->get_field_id('title'); ?>"
84 name="<?php echo $this->get_field_name('title'); ?>"
85 type="text"
86 value="<?php echo esc_attr($title); ?>">
87 </p>
88 <p>
89 <label for="<?php echo $this->get_field_id('number'); ?>">
90 <?php esc_html_e('Number of posts:', 'my-custom-theme'); ?>
91 </label>
92 <input class="tiny-text"
93 id="<?php echo $this->get_field_id('number'); ?>"
94 name="<?php echo $this->get_field_name('number'); ?>"
95 type="number"
96 step="1"
97 min="1"
98 value="<?php echo $number; ?>"
99 size="3">
100 </p>
101 <?php
102 }
103
104 /**
105 * Update widget settings
106 */
107 public function update($new_instance, $old_instance) {
108 $instance = array();
109 $instance['title'] = !empty($new_instance['title']) ? sanitize_text_field($new_instance['title']) : '';
110 $instance['number'] = !empty($new_instance['number']) ? absint($new_instance['number']) : 5;
111 return $instance;
112 }
113}
114
115/**
116 * Register custom widgets
117 */
118function mytheme_register_widgets() {
119 register_widget('MyTheme_Recent_Posts_Widget');
120}
121add_action('widgets_init', 'mytheme_register_widgets');Learn about 7. Navigation Menus.
Menu walkers allow you to customize the HTML output of WordPress menus. This is useful for
implementing custom navigation styles or frameworks like Bootstrap.
1<?php
2/**
3 * Custom Menu Walker for Bootstrap 5
4 *
5 * @package MyCustomTheme
6 */
7
8class MyTheme_Bootstrap_Nav_Walker extends Walker_Nav_Menu {
9
10 /**
11 * Start the element output
12 */
13 public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) {
14 $indent = ($depth) ? str_repeat("\\t", $depth) : '';
15
16 $classes = empty($item->classes) ? array() : (array) $item->classes;
17 $classes[] = 'nav-item';
18 $classes[] = 'menu-item-' . $item->ID;
19
20 if ($args->walker->has_children) {
21 $classes[] = 'dropdown';
22 }
23
24 if (in_array('current-menu-item', $classes)) {
25 $classes[] = 'active';
26 }
27
28 $class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth));
29 $class_names = ' class="' . esc_attr($class_names) . '"';
30
31 $output .= $indent . '<li' . $class_names . '>';
32
33 $atts = array();
34 $atts['title'] = !empty($item->attr_title) ? $item->attr_title : '';
35 $atts['target'] = !empty($item->target) ? $item->target : '';
36 $atts['rel'] = !empty($item->xfn) ? $item->xfn : '';
37 $atts['href'] = !empty($item->url) ? $item->url : '';
38 $atts['class'] = 'nav-link';
39
40 if ($args->walker->has_children && $depth === 0) {
41 $atts['class'] .= ' dropdown-toggle';
42 $atts['data-bs-toggle'] = 'dropdown';
43 $atts['aria-expanded'] = 'false';
44 }
45
46 $atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth);
47
48 $attributes = '';
49 foreach ($atts as $attr => $value) {
50 if (!empty($value)) {
51 $value = ('href' === $attr) ? esc_url($value) : esc_attr($value);
52 $attributes .= ' ' . $attr . '="' . $value . '"';
53 }
54 }
55
56 $item_output = $args->before;
57 $item_output .= '<a' . $attributes . '>';
58 $item_output .= $args->link_before . apply_filters('the_title', $item->title, $item->ID) . $args->link_after;
59 $item_output .= '</a>';
60 $item_output .= $args->after;
61
62 $output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
63 }
64
65 /**
66 * Start the sub-menu output
67 */
68 public function start_lvl(&$output, $depth = 0, $args = null) {
69 $indent = str_repeat("\\t", $depth);
70 $output .= "\\n$indent<ul class=\\"dropdown-menu\\">\\n";
71 }
72}
73
74/**
75 * Use the custom walker
76 */
77wp_nav_menu(array(
78 'theme_location' => 'primary',
79 'menu_class' => 'navbar-nav',
80 'container' => false,
81 'walker' => new MyTheme_Bootstrap_Nav_Walker(),
82));Learn about 8. Custom Page Templates.
Custom page templates allow users to choose different layouts for individual pages.
1<?php
2/**
3 * Template Name: Full Width
4 * Template Post Type: page
5 *
6 * @package MyCustomTheme
7 */
8
9get_header();
10?>
11
12<main id="primary" class="site-main full-width-template">
13 <div class="container-fluid">
14 <?php
15 while (have_posts()) :
16 the_post();
17 ?>
18 <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
19 <?php if (has_post_thumbnail()) : ?>
20 <div class="featured-image">
21 <?php the_post_thumbnail('full'); ?>
22 </div>
23 <?php endif; ?>
24
25 <header class="entry-header">
26 <?php the_title('<h1 class="entry-title">', '</h1>'); ?>
27 </header>
28
29 <div class="entry-content">
30 <?php the_content(); ?>
31 </div>
32 </article>
33 <?php
34 endwhile;
35 ?>
36 </div><!-- .container-fluid -->
37</main><!-- #primary -->
38
39<?php
40get_footer();Learn about 9. Best Practices.
Escape all output:
Sanitize all input:
sanitize_text_field()
sanitize_email()
Validate data:
Check data types and ranges before processing
Use nonces:
Verify form submissions with
wp_nonce_field()
wp_verify_nonce()
Check capabilities:
current_user_can()
before performing privileged actions
Enqueue scripts properly:
Never hardcode script tags; use
wp_enqueue_script()
Load scripts in footer:
Set the last parameter to
when enqueuing
Minimize database queries:
efficiently, cache results when appropriate
Optimize images:
Define and use appropriate image sizes with
add_image_size()
Use transients:
Cache expensive operations with the Transients API
Follow WordPress Coding Standards:
Use proper indentation, spacing, and naming conventions
Prefix all functions:
Prevent naming conflicts (e.g.,
mytheme_function_name
Use proper DocBlocks:
Document all functions, classes, and hooks
Check if functions exist:
function_exists()
before defining functions
Use strict comparisons:
instead of
Make all strings translatable:
esc_html__()
Use consistent text domain:
Match your theme slug exactly
Load text domain:
load_theme_textdomain()
after_setup_theme
Provide context when needed:
for ambiguous strings
Use semantic HTML:
Proper heading hierarchy, landmarks, lists
Add ARIA labels:
For interactive elements and navigation
Keyboard navigation:
Ensure all interactive elements are keyboard accessible
Color contrast:
Maintain WCAG AA contrast ratios (4.5:1 for normal text)
Skip links:
Provide skip-to-content links for screen readers
Use specific templates:
Create templates for different post types and archives
Use template parts:
Break down templates into reusable parts with
get_template_part()
Provide fallbacks:
Always have fallback templates (e.g.,
Use pluggable functions:
Wrap functions in
function_exists()
Use filters and actions:
Allow child themes to modify behavior
Document customization points:
Provide clear documentation for child theme developers
No PHP errors or warnings:
Unique prefix:
All theme functions, classes, and constants must be prefixed
No hardcoded links:
Don't include credit links or upsell links in theme files
GPL compatible license:
Use GPL v2 or later for theme code
Include screenshot.png:
880x660px screenshot showing the theme
Translation ready:
All strings must be translatable
Learn about 10. Exercises.
Create a simple WordPress theme from scratch with the following requirements:
with proper theme header
with a basic loop
functions.php
with theme setup function
Add support for title tag, post thumbnails, and automatic feed links
Register one navigation menu location
Enqueue a custom stylesheet and JavaScript file
Success Criteria:
Theme appears in WordPress admin and displays posts correctly
Extend your theme with Theme Customizer options:
inc/customizer.php
Add a custom section "Theme Settings"
Add settings for primary color, secondary color, and accent color
Add a setting for footer copyright text
Add a setting to toggle author bio display on single posts
Implement live preview (postMessage transport) for all color settings
Create customizer JavaScript file for live preview
Add proper sanitization callbacks for all settings
Output custom colors as inline CSS in the header
Success Criteria:
All customizer options work with live preview
Create a production-ready WordPress theme with all features:
Parent Theme:
Complete template hierarchy (index, home, single, page, archive, search, 404)
Template parts for content variations
Multiple widget areas (primary sidebar, 4 footer areas)
Three navigation menu locations (primary, footer, social)
Custom widgets (recent posts with thumbnails, social media links)
Theme Customizer with sections for colors, typography, layout
Custom page templates (full-width, landing page)
Support for custom logo, custom header, custom background
Gutenberg support (wide alignment, responsive embeds, editor styles)
Translation ready with .pot file
Child Theme:
Proper style.css and functions.php
Override single.php to add author bio
Add custom widget for featured posts
Modify excerpt length to 25 words
Add custom image size for hero images (1920x800)
Additional Requirements:
All output properly escaped
All strings translatable
No PHP errors with WP_DEBUG enabled
Pass Theme Check plugin requirements
Accessibility ready (keyboard navigation, ARIA labels, skip links)
Mobile responsive
Performance optimized (scripts in footer, minimal database queries)
Success Criteria:
Theme passes WordPress.org theme review guidelines and Theme Check plugin
Learn about 11. Quiz.
A) index.php and functions.php
B) style.css and index.php
C) header.php and footer.php
D) style.css and functions.php
Answer: B) style.css and index.php
Explanation: These are the only two required files. style.css must contain the theme header comment,
and index.php serves as the fallback template. While functions.php is essential for functionality,
it's technically not required for the theme to be recognized by WordPress.
B) Template
C) Description
D) Version
Answer: B) Template
Explanation: The "Template" field must contain the exact directory name of the parent theme
(case-sensitive). Without this field, WordPress won't know which theme is the parent, and the
child theme won't work. This is the only field that's absolutely required for child themes
(beyond the standard Theme Name field).
A) @import url("../parent-theme/style.css"); in child theme's style.css
B) wp_enqueue_style() with get_template_directory_uri() in functions.php
C) Hardcode <link> tag in header.php
D) WordPress automatically loads it
Answer: B) wp_enqueue_style() with get_template_directory_uri() in functions.php
Explanation: The correct way is to enqueue the parent stylesheet using wp_enqueue_style() with
get_template_directory_uri() (which points to parent theme), then enqueue the child stylesheet
with get_stylesheet_uri() as a dependency. Using @import is deprecated and slower. WordPress
does NOT automatically load the parent stylesheet.
B) wp_loaded
C) after_setup_theme
D) wp_enqueue_scripts
Answer: C) after_setup_theme
Explanation: after_setup_theme is the correct hook for theme setup. It runs before the init hook,
which is important because some features like add_theme_support() need to be registered early.
wp_enqueue_scripts is for enqueuing scripts/styles, and init runs too late for theme setup.
wp_loaded runs even later.
A) index.php → single.php → singular.php
B) single.php → singular.php → index.php
C) singular.php → single.php → index.php
D) single-post.php → single.php → singular.php → index.php
Answer: D) single-post.php → single.php → singular.php → index.php
Explanation: WordPress looks for templates in this order from most specific to least specific.
For the default "post" post type, it checks: single-post-
.php, single-post.php, single.php,
singular.php, and finally index.php as the ultimate fallback. The template hierarchy always
goes from specific to general, with index.php as the last resort.
Learn about 12. Additional Resources.
Official Documentation
WordPress Theme Developer Handbook
Template Hierarchy
Theme Customizer API
Child Themes Documentation
Theme Development Tools
Underscores (_s) - Starter Theme
Theme Check Plugin
Query Monitor Plugin
GenerateWP - Code Generator
Coding Standards
WordPress Coding Standards
PHP Coding Standards
Accessibility Coding Standards
Theme Review
Theme Review Guidelines
Required Theme Features
Recommended Theme Features
Advanced Topics
Gutenberg Theme Support
Advanced Child Theme Techniques
Custom Widgets
Walker_Nav_Menu Class
Video Tutorials
WordPress.tv - Theme Development
YouTube - WordPress Theme Development
WordPress Support Forums - Themes
WordPress Stack Exchange
Make WordPress Themes
Now that you understand WordPress theme development, you're ready to move on to
Plugin Development
, where you'll learn how to extend WordPress functionality
with custom plugins, understand the plugin API, work with hooks (actions and filters),
and build custom admin interfaces.
Your progress is saved in this browser, on this device only.