Whenever it comes to writing custom queries in WordPress, pagination always seems to give developers problems (myself included!).

I think this can be chalked up to the next / previous pagination links (so does next mean older, or newer?), paginating single posts as well as archive posts, and then occasionally having to write custom queries that include pagination.

One of the areas that I see most confusing – again for myself as well – is properly calculating page offsets especially when working with the WP_Query offset parameter.

The thing is, I think it can be much more simplified (or, perhaps, demystified?) when visualizing the data that you’re working with, and knowing how to use some of the existing API links.

So here’s what you need to know in order to get pagination working when working with the WP_Query offset, page, and number parameters.

A Few Assumptions

There’s several things I’m assuming while writing this article:

Nothing too complicated, right?

Visualizing Pagination

When working with WP_Query parameters specifically for pagination, there are really only three parameters that factor into the whole operation:

  • number. This is the number of posts that you want to display on the page at any given time.
  • page. The is the page that’s currently being displayed.
  • offset. This is where the query begins pulling posts. For example, if you’re on page one, then the offset will be zero; otherwise, it will be something else to “bypass” posts that’ve already been display.

Before looking at any code or trying to explain any further, here’s a way to visualize what’s going on when you’re paging through your posts:

WP_Query Offset

Visualizing Post Pagination in WordPress

In the photo above, notice that there’s a set of seven pages. This is represented by max_num_pages which will be used momentarily.

Next, only two posts will be displayed per page. Sure, it’s small but it works well enough for a demonstration.

Finally, the offset is actually represented by a formula. Basically, it’s the current page number multiplied by the current page number less one. This is what trips developers out more than anything else.

Understanding WP_Query Offset For Pagination

Offsets and Off-By-One

First, offset is nothing more than a parameter that tells WordPress where to begin pulling posts. If you’re on the first page, the offset should be zero.

If you’re on page two, then it should be however many posts you’ve already displayed (less one, since we start counting at zero) multiplied by the page that you’re on.

Make sense? Look at it this way:

  • Page 1: (1 – 1) * 1 = 0
  • Page 2: (2 – 1) * 1 = 1
  • Page 3: (3 – 1) * 1 = 2

Remember: The final value calculated for the offset is usually one less than what you’d expect because offsets start counting at zero.

Writing The Query

At this point, the only other missing piece is being able to calculate which page we’re on. Luckily, WordPress makes this available as a query string variable that can be accessed by using get_query_var.

The best way to get the current page is this:

// If the query var is set use it; otherwise, initialize it to one.
$page = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;

And yes, WordPress stores it in a variable called “paged.”

From there, we can begin writing the query. I’m assuming that we’re going to be doing something simple like pulling back posts ordered by descending date.

Obviously, your implementation may vary, but the pagination part should be the same:

// First, initialize how many posts to render per page
$display_count = 2;

// Next, get the current page
$page = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;

// After that, calculate the offset
$offset = ( $page - 1 ) * $display_count;

// Finally, we'll set the query arguments and instantiate WP_Query
$query_args = array(
  'post_type'  =>  'post',
  'orderby'    =>  'date',
  'order'      =>  'desc',
  'number'     =>  $display_count,
  'page'       =>  $page,
  'offset'     =>  $offset
$custom_query = new WP_Query ( $query_args );

 * Use your query here. Remember that if you make a call to $custom->the_post()
 * you'll need to reset the post data after the loop by calling wp_reset_postdata().

This will get you all you need in order to have a query properly paging except for one minor detail.

Defining The Pagination Links

Once you have your query parameters properly set, all that’s left to do is to define your pagination links. There are a couple of ways to do this, but I like to use next_posts_link and previous_posts_link.

The functions accept two optional parameters the second of which is a value for the maximum number of pages that the query has found. When working with custom queries, this is essential.

Here’s how I normally setup my pagination links:

<ul class="pagination">
	<li id="previous-posts">
		<?php previous_posts_link( '<< Previous Posts', $custom_query->max_num_pages ); ?>
	<li id="next-posts">
		<?php next_posts_link( 'Next Posts >>', $custom_query->max_num_pages ); ?>

And that does it.

Of course, pagination is always one of those fickle things when it comes to WordPress, so if you’ve got enhancements that you’d make, or alternative ways of working with paging through data, definitely share ’em in the comments.


Join the conversation! 38 Comments

  1. I remember figuring this out for myself while writing my membership plugin. It was painful. Once you have it figured out it’s pretty simple and straight forward, but doing it for the first time can definitely be an arduous task.

    • Totally. Early on, pagination was probably the biggest thing that tripped me up and mainly for two reasons:

      • Next posts / Previous post are somewhat counter-intuitive in how they work
      • Crafting manual queries, then compensating for pages, offset, and the ‘page’ versus ‘paged’ query variable parameter was tough to keep straight

      I finally wrote this post just as much as a note for myself as I did to help others ;).

  2. This post was a lifesaver. Simple and easy to follow. Thanks Tom!

  3. im locally developing a page right now for a client (i’m relatively new to wordpress) and am using this for their blog page. On the page itself i have 2 custom queries- one to display a featured post, and another query is basically the example you wrote above. 6 posts per page, etc etc.

    the pagination does not work on /blog or /blog/page/1 but will paginate properly on blog/page/2 (going back to the first page of posts) – is this due to the featured post query which has if(have_posts( ) && is_home( ) statement?

  4. Thank You,
    You Make My Day…

  5. thanks for putting this together. Other articles on WP_Query did not talk about the issue with pagination. Should probably be pointed out that if you use plugins such as QTranslate for multilingual sites, wordpress will control the language / text for the ‘Previous page’, ‘Next page’ links, so would need to omit the first arguments in the ‘next_posts_link’ and the ‘previous_posts_link’ methods, but a developer would notice this soon enough :)

  6. Great Post and very well explained. Thank a lot for this.

  7. Tanks! In this article i find my solution.

  8. Hey Tom,this article explained me exactly which I was looking for,now got all doubts cleared.

  9. If I do this on say page example.com/jobs/

    How does WP know to display the 2nd page of results on jobs/page/2 ?

    I suppose my question could also be: why isn’t the 2nd page posts available to a URL such as jobs/meh/blah/2 ?

  10. Hi Tom. Thanks for this, it’s great to see that someone out there still tries to explain things simply! The WP codex is just difficult to read!

    I may have a correction to add to your code above though. I apologise if anyone else has posted this already. I did check, but my brain is frazzled from trying to get this offset/pagination working for the last few hours!

    Where you have put:

    $offset = ( $page - 1 ) * $display_count;

    I think this is supposed to be:

    $off = 3 //chosen offset. For example

    $offset = $off + (($page - 1) * $display_count);

    This seemed to fix the errors I was getting, and is also stated in the codex.

    I have one other issue that I can’t figure out. On my local site, there are four remaining posts after the offset is applied, and three posts per page. This should equate to 3 posts on page 1 and one final one on page 2. However, I am getting 3 pages, the third of which is blank, and visiting it makes my pagination links vanish! Any thoughts?

    Here is my pagination code:

    str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
    'format' => '?page=%#%',
    'total' => $event_archive->max_num_pages,
    'current' => max( 1, get_query_var( 'paged') ),
    'show_all' => false,
    'end_size' => 2,
    'mid_size' => 2,
    'prev_next' => True,
    'prev_text' => __('« Previous'),
    'next_text' => __('Next »'),
    'type' => 'list',
    echo paginate_links($args); ?>

    • Whoops! Try again on that last part:

      str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
      'format' => '?page=%#%',
      'total' => $event_archive->max_num_pages,
      'current' => max( 1, get_query_var( 'paged') ),
      'show_all' => false,
      'end_size' => 2,
      'mid_size' => 2,
      'prev_next' => True,
      'prev_text' => __('« Previous'),
      'next_text' => __('Next »'),
      'type' => 'list',
      echo paginate_links($args); ?>

      • Hey Mike,

        Glad you stumbled across the article and glad you found it a bit easier to read :).

        As far as your chosen offset is concerned, your adding a number to the calculated offset is fine; however, it’s increasing the result by a hard number. This is okay in certain situations, but it all depends on your specific implementation.

        By that, I mean if you’re looking just to have a second (or third, or fourth, …) page of data, then you shouldn’t need to add anything to the offset calculation. If you find that you’re having to do that, and you’re just trying to paginate a basic post type, then there may be a problem with your pagination code – setting a somewhat-arbitrary number as your chosen offset seems odd (but I don’t know your actual situation, so that’s just my two cents).

        Secondly, I noticed that in your code, you’ve got a URL format that is not using pretty permalinks – instead, you’re using the vanilla URL structure.

        If that’s the case, I’m not sure what to offer as the code I’ve provided in the post uses the `/%postname%/` or the “Pretty Permalink” format. Perhaps try using your custom query with that format first to see if you still experience the same issues.

        I know it’s not much, but hopefully this helps!

        • The situation was that the first 5 posts would be displayed in an unpaginated area of the page, while the remaining posts would be paginated in an archive area elsewhere. I gave “3” as the value of $off above, but in reality $off was “5” on my site.

          I’ll be honest, a lot of your reply confused me…I am relatively new to web design and wordpress, and I have not really had to look too much into URL-related things yet, so this is not within my knowledge at the moment! Didn’t even know what Vanilla URLs were, haha!

          Either way, the implementation seems to be working now. I fixed it by replacing the line:

          'total' => $event_archive->max_num_pages ,


          'total' => $event_archive->max_num_pages - 1,

          I am not entirely clear on why I had to do this, but I was havgin a shot at some trial and error, and it seemed to work.

          Anyway, thanks for taking the time to get back to me, and thanks again for the initial post.

  11. Thanks for the great post and clear explanation Tom. Nice resource to have when I next think I am going crazy trying to work this out :)

  12. I was hoping to find a solution to my website which has its first post separated from the others but this looks very advanced for the php programmers. Is there any simple way of explaining this? I was thinking this would be solved by making a query before calling the posts into the div but it still doesn’t prevent pagination from beaking down.

    • Heya Tomas – Unfortnuately, I’m not sure if I understand your question (so please feel free to clarify); however, the code provided in this post along with the description is about as succinct and clear as I can make it.

      Sure, it’s PHP, but it’s primarily the WordPress API that it’s using.

      Perhaps I’m not understand your problem correctly, though.

  13. Thanks Tom , this is really great article for me as new guy in wordpress .
    Thank you so much

  14. Great Article it really saved me hours of pain . :)
    Thanks a lot Tom !

  15. It helped me a lot. Thank you so much :)

  16. Hi Tom,

    Many Thanks for saving me hours with your awesome post!!

    This is the best article on Pagination I’ve seen for WordPress after quite a few searches.

    No biggie, but I wasn’t sure how to use the ‘number’ index for the array in your example code. I simply replaced the name for that array index with ‘posts_per_page’ (from your earlier post), and it worked awesome!

    Thanks again!

  17. FYI, previous_posts_link() does not accept a second parameter for maximum number of pages.


    function previous_posts_link( $label = null ) {
    echo get_previous_posts_link( $label );

    • I’d have to confirm this by looking back in core because I’m honestly not sure, but something may have changed over the last few versions.

      Regardless, thanks for sharing this!

  18. Hi Tom,

    Many thanks for sharing your expertise on this. I am a front end developer with a limited amount of php background. I was wondering if I could ask you, using the code you provided if you could add a simple section that I could add to the code above that would pull in the information from your array. What I am using is below and it’s not working as hoped. I am getting all posts rather than the 2 indicated in the array.

    ‘orderby’ => ‘date’,
    ‘order’ => ‘desc’,
    ‘number’ => $display_count,
    ‘page’ => $page,
    ‘offset’ => $offset,
    ‘cat’ => 22
    $custom_query = new WP_Query ( $query_args );

    have_posts()) : $custom_query->the_post(); ?>

    <a href="”>

    • Look at your question:

      I am getting all posts rather than the 2 indicated in the array.

      What are the two that you’re trying to actually get? The only thing I can gather from your code is that you’re trying to grab posts from the category having the ID of 22. Are there are only two posts in that category?

  19. Thank you Tom!!!!!!

Leave a Reply