<ul class="listview"></ul>
body {
  margin: 0;
  background: #f2f2f2;
}

.listview {
  padding: 0;
  list-style: none;
  max-width: 600px;
  margin: 0 auto;
  box-sizing: border-box;
}
.listview::after {
  content: 'Loading...';
  text-align: center;
  text-transform: uppercase;
  font-size: 1rem;
  line-height: 1.5;
  display: none;
}

.listview__item {
  display: flex;
  margin-bottom: 21px;
  opacity: 0;
}

.listview__item__content {
  flex: 1 1 auto;
}

.listview__item.sentinel {
  border: 1px solid orange;
}

.listview.loading::after {
  display: block;
}
// Code based on this SitePoint article
// https://www.sitepoint.com/intersectionobserver-api
if (!('IntersectionObserver' in window))
		document.body.classList.add('polyfill');

var pageSize = 1,
	  sentinel = {
      el: null,
      set: function(el) {
        this.el = el;
        this.el.classList.add('sentinel');
        sentinelObserver.observe(this.el);
      },

      unset: function() {
        if (!this.el)
          return;
        sentinelObserver.unobserve(this.el);
        this.el.classList.remove('sentinel');
        this.el = null;
      }
	  },

	  sentinelListener = function(entries) {
		  console.log(entries);

	  	sentinel.unset();
		  listView.classList.add('loading');
		  nextPage().then(() => {
			  updateSentinel();
			  listView.classList.remove('loading');
		  });
	  },

		updateSentinel = function() {
			sentinel.set(listView.children[listView.children.length - pageSize]);
		},

		nextPage = function() {
			return loadNextPage().then((items) => {
				listView.insertAdjacentHTML(
					'beforeend',
					items.map(item => `
						<li class="listview__item loaded">
							<div class="listview__item__content">
                <img src="${item.src}">
							</div>
						</li>
					`).join('')
				);

				$('.listview__item.loaded').animate({
          opacity: 1
        }, 180, 'linear');
			});
		},

		loadNextPage = (function() {
			var pageNumber = 0;
			return function () {
				console.log(`Loading page #${pageNumber}`);

        return new Promise(function(resolve, reject) {
					var items = [];  
          $.ajax({
            url: 'https://jsonplaceholder.typicode.com/photos',
            method: 'GET',
            dataType: 'json'
          }).then(function(data) {
            for (var id = pageNumber * pageSize, lastId = id + pageSize - 1; id <= lastId; ++id) {
              items.push({
                id: id,
                src: data[getRandomInRange(0, data.length)]['url']
              });
            }
          });
					pageNumber++;
					setTimeout(function() { resolve(items); }, 600);
				});
			}
		})(),

		getRandomInRange = function(min, max) {
			return Math.floor(Math.random() * (max - min + 1)) + min;
		},

		listView = document.querySelector('.listview'),
		sentinelObserver = new IntersectionObserver(sentinelListener, {threshold: 1});
 
nextPage().then(() => { 
	nextPage().then(updateSentinel);
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
  2. https://cdn.rawgit.com/surma-dump/IntersectionObserver/polyfill/polyfill/intersectionobserver-polyfill.js