Bootstrap is a very popular css/js framework. It provides a variety of tools to develop responsive, mobile-first web pages. In this article we will try to lay down the basics of how to develop a bootstrap-based theme.

For the purpose of our example, we will need to use bootstrap, bootstrap-icons and popper.js. There are two ways to include those in our theme: either pack the necessary files as part of the theme, or load them from a CDN. We will take the first route and assume that those files are downloaded in an assets subfolder with the follownig structure:

wp-content/themes/tutorial/assets/
 |--css/
 |   |--bootstrap.min.css
 |   |--bootstrap-icons/
 |       |--(.svg files)
 |       |--font/
 |           |--bootstrap-icons.min.css
 |--js/
     |--popper.min.js
     |--boorstrap.min.js 

Next, we will need to create a simple style.css like so:

/*
Theme Name: Tutorial
Text Domain: tutorial
*/
CSS

Including the assets

Obviously, we couldn’t develop a bootstrap theme without including bootstrap in our HTML. To do that, we will add the following lines in our function.php:

function tutorial_enqueue_scripts() {

	wp_register_style(
	  'bootstrap-style',
	  get_parent_theme_file_uri('assets/css/bootstrap.min.css')
	);
	wp_register_style(
	  'bootstrap-icons-style',
	  get_parent_theme_file_uri('assets/css/bootstrap-icons/font/bootstrap-icons.min.css')
	);
	wp_enqueue_style(
	  'tutorial-style',
	  get_stylesheet_uri(),
	  ['bootstrap-style','bootstrap-icons-style']
	);
	
	wp_register_script(
	  'popper-script',
	  get_parent_theme_file_uri('assets/js/popper.min.js')
	);
	wp_enqueue_script(
	  'bootstrap-script',
	  get_parent_theme_file_uri('assets/js/bootstrap.min.js'),
	  ['popper-script']
	);
	
}

add_action('wp_enqueue_scripts','tutorial_enqueue_scripts');
PHP

The above will make sure that all the necessary styles and scripts are loaded. We enqueue the theme’s main stylesheet after bootstrap and bootstrap-icons, because we want to be able to override styles where necessary.

The basic templates

Let’s start with 404.php, since it is the simplest one:

<?php get_header(); ?>

<div class="m-2 p-5 border rounded bg-light">
	<h1 class="display-3 border-bottom">404 - Not found</h1>
	<p class="lead"><?php _e('The page you are looking for does not exist','tutorial'); ?></p>
</div>

<?php get_footer(); ?>
PHP

The above code uses bootstrap classes to format the static error message. It also implies the existence of two template parts: header.php and footer.php. For now, we could create empty versions of those files for testing purposes.

Now, any valid content falls into one of two main categories: single posts/pages/custom post types, and archive pages (including the main blog index). We will build two more templates, one for each of those content categories.

The first one, singular.php, will be used to render single pages, posts etc:

<?php get_header(); ?>

<h1 class="mb-3 pb-3 border-bottom"><?php the_title(); ?></h1>
<div><?php the_content(); ?></div>

<?php get_footer(); ?>
PHP

For simplicity, we only include the title and the actual content. In a real world scenario we would probably add the author, the publication date, maybe some taxonomies and a comments section.

The second one, index.php will cover all cases where we have a list of items to render. This includes the main blog index, category/tag archives, search results, author archives etc:

<?php get_header(); ?>

<?php if(have_posts()): ?>

	<?php while(have_posts()): the_post(); ?>

	<div class="border rounded mb-3 p-3">
	
		<h1 class="fs-3 text-truncate">
			<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
		</h1>
		
		<div class="p-1">
			<i class="bi bi-calendar"></i>
			<small><?php the_date('F j, Y'); ?></small>
			 
			<i class="bi bi-person"></i>
			<small><?php the_author_posts_link(); ?></small>
		</div>
		
		<div><?php the_excerpt(); ?></div>
		
	</div>

	<?php endwhile; ?>
	
	<div class="d-flex justify-content-between">
		<div><?php previous_posts_link(__('« Previous page','tutorial')); ?></div>
		<div><?php next_posts_link(__('Next page »','tutorial')); ?></div>
	</div>
	
<?php else: ?>

	<p class="lead text-center"><?php _e('No posts matched your criteria','tutorial'); ?></p>

<?php endif; ?>

<?php get_footer(); ?>
PHP

For each relevant post, we output the post title wrapped in a link to the actual post, and the post excerpt. We also output the publication date and the author, utilizing two bootstrap icons in the process. Moreover, we end the list with a simple pagination section. Finally, we provider the user with feedback in case there’s no relevant post.

Template parts

Now let’s implement the missing template parts, starting with header.php:

<!DOCTYPE html>
<html <?php language_attributes(); ?>>

	<head>
    <meta charset="<?php bloginfo('charset'); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="profile" href="//gmpg.org/xfn/11">
    <link rel="pingback" href="<?php bloginfo('pingback_url'); ?>">
		<?php wp_head(); ?>
	</head>

	<body <?php body_class();?>>
		
		<?php wp_body_open(); ?>
			
		<header class="border-bottom">

			<div class="container-md">
				<?php get_template_part('main-nav'); ?>
			</div>
							
		</header>
		
		<main>
			<div class="container-lg">
				<div class="row row-cols-1 row-cols-md-2">
					<div class="col-md-8 p-3">
PHP

Apart from the <html> and <body> tags, there is also a <main> and three <div> tags that need to be closed in footer.php:

					</div>
					
					<div class="col-md-4 p-3">
						<?php dynamic_sidebar('sidebar'); ?>
					</div>
					
				</div>
			</div>
		</main>
		
		<footer class="text-white bg-black py-2">
			<div class="container-md">
				<div class="row g-2">
					<?php for($i=1;$i<5;$i++): ?>
						<div class="col-12 col-md-6 col-lg-3 p-2">
							<?php dynamic_sidebar("footer-widgets-$i"); ?>
						</div>
					<?php endfor; ?>
				</div>	
			</div>	
		</footer>

		<?php wp_footer(); ?>
	
	</body>
</html>
PHP

Also, the header uses another template part, main-nav.php which would look like this:

<nav class="navbar navbar-expand-lg">
	
	<!-- brand -->
	
	<a class="navbar-brand" href="/">
		<?php if(has_custom_logo()): ?>
			<div id="site-logo"><?php the_custom_logo(); ?></div>
		<?php else: ?>
			<h1 id="site-title" class="m-0"><?php bloginfo('name'); ?></h1>
		<?php endif; ?>
	</a>
	
	<!-- search -->
	
	<div class="px-5">
		<button type="button" class="btn btn-secondary" 
		  data-bs-toggle="modal" data-bs-target="#search-modal"
		>
			<i class="bi bi-search"></i>
		</button>

		<div class="modal fade" id="search-modal" tabindex="-1" 
		  aria-labelledby="search-title" aria-hidden="true"
		>
			<div class="modal-dialog">
				<div class="modal-content">

					<div class="modal-header">
						<h1 class="modal-title fs-5" id="search-title"><?php _e('Search','tutorial'); ?></h1>
						<button type="button" class="btn-close" 
						  data-bs-dismiss="modal" aria-label="<?php _e('Close','tutorial'); ?>"
						>
						</button>
					</div>

					<div class="modal-body">
						<form method="GET" action="/">
							<div class="input-group mb-3">
								<input type="text" class="form-control" name="s" 
								  aria-label="<?php _e('Search term','tutorial'); ?>" aria-describedby="submit"
								>
								<button class="btn btn-outline-primary" type="submit" id="submit">
								  <i class="bi bi-search"></i>
								</button>
							</div>
						</form>
					</div>

				</div>
			</div>
		</div>
	</div>

	<!-- main menu -->
	
	<?php if(has_nav_menu('main')): ?>

		<button class="navbar-toggler" type="button" 
			data-bs-toggle="collapse" data-bs-target="#main-menu-container" 
			aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"
		>
			<span class="navbar-toggler-icon"></span>
		</button>
		
		<?php wp_nav_menu([
			'theme_location'=>'main',
			'container_id'=>'main-menu-container',
			'container_class'=>'collapse navbar-collapse justify-content-end',
			'menu_class'=>'menu navbar-nav mb-2 mb-lg-0',
			'fallback_cb'=>'__return_false',
			'depth'=>3,
			'walker'=>new TutorialMainMenuWalker,
		]); ?>

	<?php endif; ?>
		
</nav>
PHP

It is obvious that the main navigation section is divided into three parts: the brand, the search button (and dialog) and the main menu. Let’s break them down one by one.

Brand

This is a link to the front page of the site. The link content is the site name, or the site logo if one has been assigned in the site identity section of the customizer. In order for this option to be available, we must inform WordPress that our theme supports custom logos. This is done by adding the following code in functions.php:

function tutorial_setup_theme(){
	add_theme_support('custom-logo');
}

add_action('after_setup_theme','tutorial_setup_theme');
PHP

Search button

The site search will be implemented through a popup dialog. This dialog will appear when the user clicks on a search button, next to the brand link. It is a fully functional bootstrap modal with no extra code required. In order to connect the form to WordPress’s search functionality, the form’s action must be / and the <input> field’s name must be s.

Main menu

A good reason to develop a bootstrap theme is to take advantage of bootstrap’s responsive capabilities. This is exactly what we are trying to accomplish with the main menu. We want it to collapse to a button in tablet screen widths and below, as shown here. But, in order to do that the bootstrap way, we have to use the corresponding classes for the button, the menu as well as the menu items and submenus. For the button that’s easy, since its HTML is static. For the menu itself, we can define the elements and classes used as parameters to wp_nav_menu(). But what about the menu items and submenus? Well, for those, we will implement a menu walker descendant in functions.php, along with the menu location registration:

function tutorial_init() {
	register_nav_menus(['main'=>__('Main Navigation','tutorial'),]);
}

add_action('init','tutorial_init');

class TutorialMainMenuWalker extends Walker_Nav_Menu {

	function start_el(&$output,$item,$depth=0,$args=[],$id=0) {
		$item->classes[]='nav-item';
		$iid="nav-item-{$item->ID}";
		$link_classes=['nav-link',];
		$lid="nav-link-{$item->ID}";
		$link_rest='';
		if($depth) $link_classes[]='dropdown-item';
		if(in_array('menu-item-has-children',$item->classes)&&!$depth) {
			$item->classes[]="dropdown";
			$link_classes[]="dropdown-toggle";
			$link_rest=" role='button' data-bs-toggle='dropdown' aria-expanded='false'";
		}
		$ic=implode(" ",$item->classes);
		$lc=implode(" ",$link_classes);
		$output.="<li id='$iid' class='$ic'>";
		$output.="<a id='$lid' class='$lc' href='{$item->url}' $link_rest>";
		$output.=$item->title;
		$output.="</a>"; 
	}
	
	function start_lvl(&$output, $depth=0, $args=null) { 
		$output.="<ul class='sub-menu sub-menu-level-$depth ".((!$depth)?"dropdown-menu":"")."'>";
	}
	
}
PHP

In this class, we use the start_el() and start_lvl() methods to define the menu item and submenu list opening tags respectively. The default elements for those two components are <li> and <ul> and, since we use the same tags, we don’t have to override the corresponding end_el() and end_lvl() methods.

The widget areas

The last thing we will do for our tutorial bootstrap theme is to take care of its widget areas. If we take a closer look at footer.php, we will notice that it uses five widget areas: one sidebar and four footer widget columns. However, in order for them to work, we first have to register them in functions.php:

function tutorial_widgets_init() {

	register_sidebar([
		'id'=>'sidebar',
		'name'=>__('Sidebar','tutorial'),
		'description'=>__('The right sidebar','tutorial'),
		'class'=>'list-group',
		'before_sidebar'=>'<ul id="%1$s" class="%2$s">',
		'after_sidebar'=>'</ul>',
		'before_widget'=>'<li class="list-group-item">',
		'after_widget'=>'</li>',
	]);

	for($i=1;$i<5;$i++) {
		register_sidebar([
			'id'=>"footer-widgets-$i",
			'name'=>sprintf(__('Footer Widgets %d','tutorial'),$i),
			'description'=>sprintf(__('Footer Widget Column #','tutorial'),$i),
			'class'=>'footer-widgets list-unstyled',
			'before_sidebar'=>'<ul id="%1$s" class="%2$s">',
			'after_sidebar'=>'</ul>',
		]);
	}
	
}

add_action('widgets_init','tutorial_widgets_init');
PHP

Just like what we did with the main menu, we are injecting the classes needed for bootstrap to render our content the way we want. Notice that the sidebar is using the list group scheme, while the footer columns are just unstyled lists.