Skip to content

How to Create WordPress Pagination Without a Plugin

By ali 7 min read

Create WordPress pagination without a plugin using WP_Query and PHP

Pagination makes it easier for visitors to browse large collections of posts without loading everything on a single page. Instead of displaying all posts at once, WordPress can split them into multiple pages with links to move between them.

If you are creating a custom post loop using WP_Query, you may need to handle pagination separately. The default WordPress pagination functions usually work with the main query, so a custom query requires the correct page number and pagination settings.

In this tutorial, you will learn how to create pagination for a custom WordPress query without installing an additional plugin.

1. Get the Current Page Number

Before creating a custom query, you need to identify which page the visitor is viewing. WordPress provides the get_query_var() function to retrieve the current pagination value.

For a regular archive or blog page, you can use the paged query variable:

$paged = get_query_var( 'paged' )
    ? absint( get_query_var( 'paged' ) )
    : 1;

This returns the current page number. If the visitor is on the first page, the value defaults to 1.

There is one important exception: a static front page may use the page query variable instead of paged. You can account for both like this:

if ( get_query_var( 'paged' ) ) {
    $paged = absint( get_query_var( 'paged' ) );
} elseif ( get_query_var( 'page' ) ) {
    $paged = absint( get_query_var( 'page' ) );
} else {
    $paged = 1;
}

2. Add the Paged Parameter to WP_Query

Once you have the current page number, pass it to the paged argument of your custom query. This tells WordPress which set of posts to retrieve.

For example, the following query retrieves published blog posts and displays the number of posts configured in the WordPress Reading settings:

$paged = get_query_var( 'paged' )
    ? absint( get_query_var( 'paged' ) )
    : 1;

$custom_query_args = array(
    'post_type'           => 'post',
    'post_status'         => 'publish',
    'posts_per_page'      => get_option( 'posts_per_page' ),
    'paged'               => $paged,
    'ignore_sticky_posts' => true,
    'order'               => 'DESC',
    'orderby'             => 'date',
);

$custom_query = new WP_Query( $custom_query_args );

The important part is the paged argument. Without it, your custom query may keep retrieving the first set of posts even when visitors click another pagination link.

3. Display Posts from the Custom Query

After creating the query, use the standard WordPress loop to display the posts.

if ( $custom_query->have_posts() ) {

    while ( $custom_query->have_posts() ) {

        $custom_query->the_post();

        echo '<article class="custom-post">';

        echo '<h2>';
        echo '<a href="' . esc_url( get_permalink() ) . '">';
        echo esc_html( get_the_title() );
        echo '</a>';
        echo '</h2>';

        echo '<div class="post-excerpt">';
        echo wp_kses_post( get_the_excerpt() );
        echo '</div>';

        echo '</article>';
    }

    wp_reset_postdata();

} else {

    echo '<p>Sorry, no posts matched your criteria.</p>';
}

This example displays each post title and excerpt. You can replace the output with your own HTML, featured images, or other post information.

WordPress provides the paginate_links() function to generate pagination links. You can use it with the custom query’s current page and total number of pages.

Add the following code after the custom post loop:

if ( $custom_query->max_num_pages > 1 ) {

    $big = 999999999;

    $pagination = paginate_links(
        array(
            'base'      => str_replace(
                $big,
                '%#%',
                esc_url( get_pagenum_link( $big ) )
            ),
            'format'    => '?paged=%#%',
            'current'   => max( 1, $paged ),
            'total'     => $custom_query->max_num_pages,
            'prev_text' => 'Previous',
            'next_text' => 'Next',
            'type'      => 'list',
        )
    );

    if ( $pagination ) {
        echo '<nav class="custom-pagination" aria-label="Posts pagination">';
        echo wp_kses_post( $pagination );
        echo '</nav>';
    }
}

The important arguments are:

  • base: Defines the base URL used to generate pagination links.
  • format: Defines the pagination URL format.
  • current: The current page number.
  • total: The total number of pages returned by the custom query.
  • prev_text: The text displayed for the previous-page link.
  • next_text: The text displayed for the next-page link.
  • type: Determines the HTML format of the pagination output. The example uses a list.

Using paginate_links() is generally more flexible than manually creating numbered links because WordPress generates the URLs based on the query’s pagination settings.

5. Complete Example: Custom WordPress Query with Pagination

Here is the complete example combining the custom query, post loop, and pagination. You can place this code in a suitable theme template file where you want the custom post list to appear.

<?php

// Get the current page number.
if ( get_query_var( 'paged' ) ) {
    $paged = absint( get_query_var( 'paged' ) );
} elseif ( get_query_var( 'page' ) ) {
    $paged = absint( get_query_var( 'page' ) );
} else {
    $paged = 1;
}

// Define custom query arguments.
$custom_query_args = array(
    'post_type'           => 'post',
    'post_status'         => 'publish',
    'posts_per_page'      => get_option( 'posts_per_page' ),
    'paged'               => $paged,
    'ignore_sticky_posts' => true,
    'order'               => 'DESC',
    'orderby'             => 'date',
    // 'category_name'    => 'custom-cat',
);

// Create the custom query.
$custom_query = new WP_Query( $custom_query_args );

if ( $custom_query->have_posts() ) :

    // Display posts.
    while ( $custom_query->have_posts() ) :
        $custom_query->the_post();
        ?>

        <article class="custom-post">

            <h2>
                <a href="<?php the_permalink(); ?>">
                    <?php the_title(); ?>
                </a>
            </h2>

            <?php if ( has_post_thumbnail() ) : ?>
                <div class="post-thumbnail">
                    <a href="<?php the_permalink(); ?>">
                        <?php the_post_thumbnail( 'medium' ); ?>
                    </a>
                </div>
            <?php endif; ?>

            <div class="post-excerpt">
                <?php the_excerpt(); ?>
            </div>

        </article>

    <?php
    endwhile;

    // Display pagination after the loop.
    if ( $custom_query->max_num_pages > 1 ) :

        $big = 999999999;

        $pagination = paginate_links(
            array(
                'base'      => str_replace(
                    $big,
                    '%#%',
                    esc_url( get_pagenum_link( $big ) )
                ),
                'format'    => '?paged=%#%',
                'current'   => max( 1, $paged ),
                'total'     => $custom_query->max_num_pages,
                'prev_text' => 'Previous',
                'next_text' => 'Next',
                'type'      => 'list',
            )
        );

        if ( $pagination ) :
            ?>

            <nav class="custom-pagination" aria-label="Posts pagination">
                <?php echo wp_kses_post( $pagination ); ?>
            </nav>

        <?php
        endif;

    endif;

else :

    echo '<p>Sorry, no posts matched your criteria.</p>';

endif;

// Restore the original post data.
wp_reset_postdata();

?>

This example displays the posts first and then generates pagination links below the loop. It uses the custom query’s max_num_pages value to determine how many pagination pages are available.

6. Add CSS to Style the Pagination

The paginate_links() function can generate pagination as an HTML list. You can style that list using CSS.

.custom-pagination {
    margin: 30px 0;
}

.custom-pagination ul {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: center;
    list-style: none;
    margin: 0;
    padding: 0;
}

.custom-pagination li {
    margin: 0;
}

.custom-pagination a,
.custom-pagination span {
    display: block;
    padding: 8px 14px;
    border: 1px solid #ddd;
    border-radius: 4px;
    text-decoration: none;
}

.custom-pagination .current {
    background: #222;
    border-color: #222;
    color: #fff;
}

You can change the colors, spacing, borders, and other styles to match your WordPress theme.

7. Important Notes About WordPress Pagination

  • Use the correct page variable: Most archive and blog queries use paged. A static front page may use page, so check the page context when building custom pagination.
  • Do not create fake pagination links: Avoid hardcoding page numbers or using placeholder URLs. Generate links using paginate_links().
  • Use the custom query’s page count: Set the total argument to $custom_query->max_num_pages, not the total number of posts.
  • Restore post data: Call wp_reset_postdata() after a custom loop so the main query’s post data is restored.
  • Use a suitable template location: The complete example is intended for a theme template. If you attach it to a theme-specific action hook, check that the hook runs in the intended location and does not execute the query more than once.
  • Test permalink settings: Check pagination with both pretty permalinks and plain permalinks, especially when using a static front page.

For more information about custom queries and pagination, refer to the official WordPress documentation for WP_Query, paginate_links(), and get_query_var().

If you work with custom post types, you may also find our guide on adding a sortable column to a custom post type useful when customizing WordPress admin screens.

Conclusion

You can create WordPress pagination for a custom query without installing a plugin. The key is to retrieve the current page number, pass it to the paged argument in WP_Query, and generate navigation links using paginate_links().

This approach gives you control over the posts displayed, the number of posts per page, and the appearance of the pagination. Test the implementation in your theme, particularly if you are using a static front page or custom permalink settings.