Build classic and block WordPress themes from scratch: the template hierarchy, The Loop, the_title() and the_content(), template parts, menus, the Customizer, child themes and theme.json.
Lessons
Navigation menus in a classic theme are built in two halves: the theme registers named menu locations with register_nav_menus(), and prints whatever menu the site owner assigned to a location with wp_nav_menu().
This separation is the point. You decide where menus can go; the site owner decides what is in them, from Appearance → Menus, without touching code.
<?php
function gdc_starter_menus() {
register_nav_menus(
array(
'primary' => esc_html__( 'Primary Menu', 'gdc-starter' ),
'footer' => esc_html__( 'Footer Menu', 'gdc-starter' ),
'social' => esc_html__( 'Social Links Menu', 'gdc-starter' ),
)
);
}
add_action( 'after_setup_theme', 'gdc_starter_menus' );Each array key — primary — is the location ID your template uses. Each value is the label the site owner sees.
<nav class="main-navigation" aria-label="<?php esc_attr_e( 'Primary', 'gdc-starter' ); ?>">
<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
<?php esc_html_e( 'Menu', 'gdc-starter' ); ?>
</button>
<?php
wp_nav_menu(
array(
'theme_location' => 'primary',
'menu_id' => 'primary-menu',
'menu_class' => 'menu',
'container' => false,
'depth' => 2,
'fallback_cb' => false,
)
);
?>
</nav>| Argument | Effect |
|---|---|
theme_location | Which registered location to print. |
container | Wrapper element. false removes the default <div>, since you already have <nav>. |
menu_class / menu_id | Class and ID on the <ul>. |
depth | How many levels of sub-menus to show. 0 means unlimited. |
fallback_cb | What to print when no menu is assigned. The default lists every page — usually unwanted. false prints nothing. |
Wrap optional menus so their surrounding markup disappears when nothing is assigned:
<?php if ( has_nav_menu( 'footer' ) ) : ?>
<nav class="footer-navigation" aria-label="<?php esc_attr_e( 'Footer', 'gdc-starter' ); ?>">
<?php
wp_nav_menu(
array(
'theme_location' => 'footer',
'container' => false,
'depth' => 1,
)
);
?>
</nav>
<?php endif; ?>WordPress adds classes to menu items automatically. The current page gets current-menu-item, and its parents get current-menu-ancestor. Items with sub-menus get menu-item-has-children.
.main-navigation .menu {
display: flex;
gap: 24px;
list-style: none;
margin: 0;
padding: 0;
}
.main-navigation .current-menu-item > a,
.main-navigation .current-menu-ancestor > a {
color: #6c63ff;
font-weight: 700;
}
.main-navigation .sub-menu {
display: none;
}
.main-navigation .menu-item-has-children:focus-within > .sub-menu,
.main-navigation .menu-item-has-children:hover > .sub-menu {
display: block;
}Tip: Use:focus-within, not only:hover, to reveal sub-menus. Keyboard users never hover, so a hover-only menu is unreachable for them.
Need a class on every <a> for a CSS framework? Filter it rather than replacing the whole menu output:
<?php
function gdc_starter_menu_link_class( $atts, $item, $args ) {
if ( 'primary' === $args->theme_location ) {
$atts['class'] = 'nav-link';
}
return $atts;
}
add_filter( 'nav_menu_link_attributes', 'gdc_starter_menu_link_class', 10, 3 );primary.container => false and style current-menu-item.In accordance with Section 6 & Section 7 of the DPDP Act 2023, third-party analytics and marketing trackers are strictly gated behind your affirmative consent. You may withdraw consent at any time under Section 6(4).
Essential for session authentication (ci_session), Cross-Site Request Forgery security (csrf_cookie_name), and Cloudflare DDoS/bot prevention. Cannot be disabled.
Gated under DPDP Section 7. Allows Google Analytics 4 (Google Consent Mode v2) to measure aggregate, anonymized tool usage.
Enables browser local storage to preserve your theme selection (dark/light mode) and code editor layout between visits.
Used to display developer sponsorships and native tool partner notices.