Want to replace the default “Next Posts” and “Previous Posts” links with numbered pagination? While plugins like WP-PageNavi can do this, adding pagination manually keeps your theme lightweight and avoids unnecessary CSS and JavaScript files.
WordPress includes a built-in function called paginate_links() that creates numbered pagination for any query in your theme.
By default, the function comes with the following settings:
<?php $args = array(
'base' => '%_%',
'format' => '?page=%#%', // the '%#%' will be replaced with the page number
'total' => 1,
'current' => 0,
'show_all' => False,
'end_size' => 1,
'mid_size' => 2,
'prev_next' => True,
'prev_text' => __('« Previous'),
'next_text' => __('Next »'),
'type' => 'plain',
'add_args' => False,
'add_fragment' => '',
'before_page_number' => '',
'after_page_number' => ''
); ?>For a full list of parameters, see the official documentation. Below is a step-by-step guide to adding numbered pagination to your WordPress theme.
It’s worth mentioning that you can forgo traditional pagination and add the infinite scroll option or alternatively, the option to load more posts via a button.
Modern alternative: Since WordPress 4.1, you can use the_posts_pagination() for the main query. It generates accessible pagination markup out of the box. The manual approach below gives you more control over custom queries and styling.
Adding Pagination to a WordPress Theme
Add the following code to your theme’s functions.php file (or any other file you find appropriate in your theme):
/**
* Pagination for archive, taxonomy, category, tag and search results pages
*
* @global $wp_query https://developer.wordpress.org/reference/classes/wp_query/
* @return Prints the HTML for the pagination if a template is $paged
*/
function sv_pagination() {
global $wp_query;
$next_arrow = is_rtl() ? esc_html( '<' ) : esc_html( '>' );
$prev_arrow = is_rtl() ? esc_html( '>' ) : esc_html( '<' );
$total = $wp_query->max_num_pages;
$big = 999999999; // This needs to be an unlikely integer
// For more options and info view the docs for paginate_links()
// https://developer.wordpress.org/reference/functions/paginate_links/
$paginate_links = paginate_links( array(
'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link($big) ) ),
'current' => max( 1, get_query_var('paged') ),
'total' => $total,
'show_all' => true,
'prev_text' => $prev_arrow,
'next_text' => $next_arrow
) );
// Display the pagination if more than one page is found
if ( $paginate_links ) {
echo '<div class="pagination">';
echo $paginate_links;
echo '</div>';
}
}We’ve set the basic parameters for paginate_links(). To make the pagination accessible (and you should), add the following variable at the beginning of the function:
$translated = __( 'Page', 'mytextdomain' );Then, add the following parameter to the paginate_links array in the above code:
'before_page_number' => '<span class="accessible-txt">'.$translated.' </span>'You might want to hide the
accessible-txtclass in your CSS file…
Styling the Pagination
We’ll use Flexbox to make things easier. To style the numbered pagination like this blog, add the following code to your child theme’s style.css file:
.pagination {
display: flex;
justify-content: space-between;
flex-flow: row wrap;
}
.pagination > * {
color: #2d2d2d;
border-radius: 50%;
text-align: center;
flex-basis: 36px;
line-height: 36px;
min-width: 36px;
margin: 4px 0;
background: #f3f3ef;
}
.pagination > a:hover,
.pagination span.current {
background-color: #942762;
color: #fefefe;
}Adding Pagination to Your Theme
It’s very simple. To call the pagination function in your theme, just add the following line of code where you want the pagination to appear. Typically, you’ll add this code to index.php, category.php, archive.php, and similar files, but if you have a custom page template, add it there:
<?php sv_pagination(); ?>If you’re using a template file or page that already has the default WordPress pagination, replace it with this code.
Custom Query Loop?
If you are using a custom loop with WP_Query, the code won’t work unless you assign your query to the $wp_query variable (don’t do this). To fix this, when I create a custom loop, it usually looks like this:
$sv_query = new WP_Query( $args );Then modify the pagination function to detect your custom query and adjust $total accordingly:
global $wp_query, $sv_query;
if ( $sv_query ) {
$total = $sv_query->max_num_pages;
}
else {
$total = $wp_query->max_num_pages;
}Your custom loop should basically look like the following:
<?php
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = array(
'post_type' => 'post', // You can add a custom post type if you like
'posts_per_page' => 6,
'paged' => $paged
);
$sv_query = new WP_Query( $args );
if ( $sv_query->have_posts() ) : while ( $sv_query->have_posts() ) : $sv_query->the_post();
//*** Post content goes here
endwhile;
sv_pagination();
wp_reset_postdata();
else :
//*** If no posts found message goes here
endif;
?>Note that after the loop ends, we call wp_reset_postdata() to restore the global $post variable. This is essential when using custom WP_Query loops.
FAQs
Summary
Numbered pagination provides a clear way for users to navigate your blog and helps search engines discover all your content. With just a few lines of PHP and CSS, you can add custom pagination to any WordPress theme.
For SEO considerations around paginated pages, check out our guide on pagination and the rel=next and rel=prev tags.

