<div class="container my-5">
	
	<div id="gallery-loader" class="text-center my-5">
		<div class="mb-3"> 
			<div class="spinner-border" role="status" aria-hidden="true"></div>
			<div class="small text-muted">Loading gallery...</div>
		</div>
	</div>
	
	<div id="gallery" class="row g-4 d-none">
		<!-- This part will be generated by JavaScript -->
	</div>
</div>
/* No CSS needed ;-) */
View Compiled
document.addEventListener('DOMContentLoaded',function(){
  
	const limit = 50;
	const gallery = document.getElementById('gallery');
	const galleryLoader = document.getElementById('gallery-loader');
	const imageSizes = ['400/300','300/400','400/400','800/400','400/800'];
	
	function getBunchOfPhotos(file, callback) {
    	var rawFile = new XMLHttpRequest();
    	rawFile.overrideMimeType("application/json");
    	rawFile.open("GET", file, true);
    	rawFile.onreadystatechange = function() {
			if (rawFile.readyState === 4 && rawFile.status == "200") {
				callback(rawFile.responseText);
			}
    	}
    	rawFile.send(null);
	}

	getBunchOfPhotos("https://picsum.photos/v2/list?page=2&limit="+limit, function(text){
		var data = JSON.parse(text);
		data.forEach(function(pic,i){
			const delay = (i+1)*400+'ms';
			const column = document.createElement('div');
			const imgLink = document.createElement('a');
			const img = document.createElement('img');
			const overlay = document.createElement('div');
			const imgSize = imageSizes[Math.floor(Math.random() * 5)];
			imgLink.href = pic['url'];
			imgLink.target = "_blank";
		
			overlay.classList.add('animate__animated','animate__zoomInRight','animate__delay-2s','position-absolute','bottom-0','py-1','px-2','bg-dark','bg-opacity-50','text-nowrap');
			overlay.setAttribute("style","animation-delay:"+delay+"; pointer-events:none;");
			overlay.innerHTML = '<p class="text-light small m-0">'+pic['author']+'</p>';
			img.classList.add('img-fluid','shadow','border');
			img.title = 'Photo by ' + pic['author'];
			img.alt = pic['author'];
			img.src = 'https://picsum.photos/id/'+pic['id']+'/'+imgSize;
			column.classList.add('col-6','col-md-3','col-xl-2');
			
			imgLink.appendChild(img);
			column.appendChild(imgLink);
			column.appendChild(overlay);
			gallery.appendChild(column);
		});
		
		imagesLoaded( gallery, function( instance ) {

			const galleryItems = document.querySelectorAll('#gallery > .col-6');

			galleryItems.forEach(function(item,i){
				const delay = (i+1)*200+'ms';
				item.classList.add('animate__animated','animate__fadeIn');
				item.setAttribute("style", "animation-delay:"+delay+";");
			});

			gallery.classList.remove('d-none');
			galleryLoader.classList.add('d-none');
			var msnry = new Masonry( gallery, {
				percentPosition: true
			});
		});
		
	});
   
})

External CSS

  1. https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css
  2. https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css

External JavaScript

  1. https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js
  2. https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js
  3. https://cdn.jsdelivr.net/npm/imagesloaded@5.0.0/imagesloaded.pkgd.min.js