As i'm starting as a web dev I had this little WordPress project which consist of retrieving all the posts from the tags you have clicked on, but without switching to another page,which result to this work.

The idea here is to be the most explicit possible if you are like me a beginner,also i have chosen to put everything in two files

  1. 1- Function.php
  2. 2 - ajax-filter-post.js

So it get's easier to give it a test,if you create a twentysixteen child theme and drop this two files in it, it should work straight forward. As for a production model i would recommend to split some of the code such as everything which is about web templating on the php side, to be closer to a MVC coding style.

So as i got great help to complete this code, i wanted to share it so everybody can copy,play,reuse and hack it. I have done a lot of in code comment to hopefully make it as clear as possible,also that code is largely inspired by Valdo Bosnajk work, and you can find the original version here:

https://www.bobz.co/filter-wordpress-posts-by-custom-taxonomy-term-with-ajax-and-pagination/

And let's get to the real things !

1- Function.php


<?php
/**
 *
 * Functions and definitons
 * This is a child theme of Twentysixteen theme
 * V1.0 - AJAX Filter posts
 * Largely inspired by Vlado Bosnjak work that you can find here:
 * https://www.bobz.co/filter-wordpress-posts-by-custom-taxonomy-term-with-ajax-and-pagination/
 *
 *
 */

// Here below we are loading the parent theme in my case the twentysixteen theme
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_styles' );
function my_theme_enqueue_styles() {
        $parent_style = 'twentysixteen-style'; 
         wp_enqueue_style( $parent_style, get_template_directory_uri() . '/style.css' );
         wp_enqueue_style( 'child-style',
                          get_stylesheet_directory_uri() . '/style.css',
                                array( $parent_style ),
                                wp_get_theme()->get('Version')
                          );
}
// End of Parent theme loads

// From here we will retreive the all the posts corresponding to the Tag we have clicked on.
function my_get_posts(){
//permission_check(); <-- check nonce and permissions here
   if( !isset( $_POST['nonce'] ) || !wp_verify_nonce( $_POST['nonce'], 'mdu' ) )
        die('MDU Permission denied' );


// Store in a variable the name of the tag that has been clicked on
$ajax_tag = ($_POST['params']['tag']); //This is coming from the JS side.
// Store in a variable the name of the category that has been grab on the JS side.
$ajax_cat = ($_POST['params']['cat']); //This is coming from the JS side.
// Store in a variable the page number that has been grab on the JS side.
$page = ($_POST['params']['pagenum']); //This is coming from the JS side.

    /**
     * Here we setup our custom WordPress query to retreive the post we want.
     */
    $args = [
        'paged'         => $page,
        'post_type'     => 'post',
        'post_status'   => 'publish',
        'tag'           => $ajax_tag,
        'category_name' => $ajax_cat, 
    ];//end of Custom query setup
        
//Create our Custom query called $my_query
$my_query = new WP_Query( $args );
//As i'm using the output buffering, first i'm making sure it's empty
if (ob_get_level()) ob_end_clean();
//Then start a first level of output buffering to grab store retreived posts.
ob_start();
//Running the Custom WP_query
    if ($my_query->have_posts()) :
        while ($my_query->have_posts()) : $my_query->the_post();  
          //I'm using a standard TwentySixteen template to have it working immediately.
          $post_content = get_template_part( 'template-parts/content', get_post_format() );

       endwhile;
/**
* I'm starting another level of output buffering to grab the navigation bar corresponding to my post
* previously retrieved
*/
ob_start();
        $nav_content = vb_ajax_pager($my_query,$page);

    endif;
//Get the buffer content and clean the output buffer at both level.
    $nav_content  = ob_get_clean();
    $post_content = ob_get_clean();
//Store those two variables in an array
    $response = ['content' => $post_content , 'navigation' => $nav_content,];
//Finally Json_encode to pass it to your JS
    die(json_encode($response));
}

//standard wordpress add_action
add_action('wp_ajax_my_get_posts', 'my_get_posts');
add_action('wp_ajax_nopriv_my_get_posts', 'my_get_posts');


/**
 * Pagination for the tag view.
 * Here I'm buildn't the pagination element for my custom query
 */
//$paged is the page number so it's default to 1
function vb_ajax_pager ( $query = NULL, $paged = 1 ) {
        $ajax_paginate[] = 1;

    if (!$query)
        return;
//Then build the pagingation using the paginate_links function
    $ajax_paginate = paginate_links([
        'base'      => '%_%', 
        'type'      => 'array',
        'total'     => $query->max_num_pages,
        'format'    => '#page=%#%',
        'current'   => max( 1, $paged ),
        'prev_text' => '&lt;',
        'next_text' => '&gt;'
    ]);

/*here below i'm building the navigation bar, 
 * i'm rebuild it to be exactly the same as the original one
 */
    if ($query->max_num_pages > 1) : ?>
        <nav class="navigation pagination" role="navigation">
          <h2 class="screen-reader-text">Posts navigation</h2>
            <div class="nav-links">
            <?php foreach ( $ajax_paginate as $ajax_page ): ?>
                <span class="page-numbers"><?php echo $ajax_page; ?></span>
            <?php endforeach; ?>
            </div>
        </nav>
<?php endif;
}//end of pagination rebuild



/*And finaly i'm doing the enqueue and localizing the Javascript.
* notice the wp_create_nonce which is the minimum of security but clearly 
* not enough in production environement.
*/
 function assets() {
     wp_register_script(
           'ajax_filter',
           get_stylesheet_directory_uri() . '/js/ajax-filter-posts.js',
           ['jquery'],
            "1.0",
             true
     );
     wp_enqueue_script('ajax_filter');
     wp_localize_script( 'ajax_filter', 'mdu', array(
         'nonce'    => wp_create_nonce( 'mdu' ),
         'ajax_url' => admin_url( 'admin-ajax.php' )
     ));
}
add_action('wp_enqueue_scripts', 'assets', 99);


2 - Javascript


Small comment here, for those who don't know (it was my case when i started) this JS/Jquery file is build this way

(function ($){
 //your code here
})(jquery);
It's a function which call itself immediately so we guaranty the 
variable inside won't clash any of the variable outside of this JS and vice et versa.
Also the ($) is a function parameter to gets replace by Jquery (the one you have at the end) 
so you can use the "$." writing style instead of the "Jquery." writing style, this is because 
Wordpress use the Jquery.noconflict() model.

(function($) {
        $doc = $(document);

        $doc.ready( function() {
                
        var container    = $('#main');
        var pagePosts    = container.find('article');
        var postNav       = $('nav.navigation');
        var catName       = "";
        var name          = "";
        var page          = "1";
        var clickTag      = "";
        var n             = "";

        //Here first i'm adding a class to my tags to be able to retrieve them.  
        $("a[rel='tag']").addClass("ajax_tag");

  //Here you have the ajax where AJAX and PHP will communicate to each other using JSON
  function get_posts($params){
    $.ajax({
        //This variable points to the Wordpress admin_ajax.php page (see function.php)
        url: mdu.ajax_url, 
            //below are the data we are sending to php, and what has to be done with it.
            data: {
                //action will trigger the php function my_get_posts
                action: 'my_get_posts', 
                //managing nonce for minimum of security
                nonce: mdu.nonce,
               /**
                *elements sent php stored in the table at the bottom of 
                * this JS at the Onclick event
                */
                        params: $params,
                        },
                        type: 'POST',
                        dataType:'json',
                                //once we received the success response
                        success: function(data,XMLHttpRequest){
                                //first we remove the old posts
                            pagePosts.remove();
                                //then remove the navigation bar
                            postNav.remove();
                                //Add the new posts that retreived with our WP_query
                            container.html(data.content);
                                //Add the new navigation bar corresponding to our new posts lists
                            container.append(data.navigation);
                                /*Here we reset the variable postNav which has changed during 
                                 * the previous operations
                                */
                            postNav = $('nav.navigation');
                                /*Here we add the ajax_tag class to the tags newly added
                                */
                            $("a[rel='tag']").addClass("ajax_tag");
                        },                      
                    });
                }
                
                //This function is used to grab the category name from the h1 HTML tag.
        function getCatName(name){
                 var pattern = /[^ *-]\w+$/g;
                 n = $("h1").html();
                 catName = pattern.exec(n);
        }
                /**
                 * Here below we are catching the onclick event either on a tag or 
                 * on the navigation bar.
                 */
        container.on('click', "a[rel='tag'], .pagination a", function(event) {
                        //we make sure this context doesn't change so we store it in a variable.
                        $this = $(this);
                        //prevent the default beahviour which consist of following the link.
                        event.preventDefault();
                        /**
                         * Here below simple check if we have clicked on a Tag or 
                         * clicked on the nav bar
                         */
                if ($this.hasClass('ajax_tag')) {
                         /**
                          * if it's a tag we wan't to grab the tag nam, category name and
                          *  the page number initialized at 1.
                          */
                        clickTag = $this.text();
                        getCatName(name);
                        page = "1";
                        
                        }
                        else {
                                /**
                                 * Or if it's the nav bar then we want to grab the page number
                                 */
                                page = parseInt($this.attr('href').replace(/\D/g,''));
                        }
                                //those are the parameters we are passing to our ajax
                                $params = {
                                        'tag' : clickTag,
                                        'cat' : catName,
                                        'pagenum': page,
                                };
                        //finaly we trigger the function at the top with the required parameters
                        get_posts($params);

                        });

        });
})(jQuery);


You can grab the files directly from my GithubGist