Swinging Pinned Photo Gallery with Scroll Animation using HTML, CSS, and JavaScript

Faraz

By Faraz -

Create a stunning swinging pinned photo gallery with scroll-triggered animation using HTML, CSS, and JavaScript.


swinging-photo-gallery-using-html-css-and-javascript.webp

Table of Contents

  1. Project Introduction
  2. HTML Code
  3. CSS Code
  4. JavaScript Code
  5. Conclusion
  6. Preview

Creating interactive and animated photo galleries can make your website more engaging. In this tutorial, we will show you how to create a swinging pinned photo gallery with a scroll-triggered animation using just HTML, CSS, and JavaScript. This guide is easy to follow and suitable for beginners.

Prerequisites:

Now, let’s dive into building this cool gallery step by step.

Source Code

Step 1 (HTML Code):

First, create a simple HTML structure that will hold the photos in the gallery. Here’s a breakdown of the key components:

<!DOCTYPE html>

Declares the document type as HTML5.

<html lang="en">

Defines the root of the HTML document with lang="en" specifying the language as English.

<head>

Contains metadata and resources for the page, such as:

  • <meta charset="UTF-8">: Specifies the character encoding as UTF-8.
  • <meta name="viewport" content="width=device-width, initial-scale=1.0">: Ensures the page is responsive on all devices.
  • <title>Swinging Pinned Photo Gallery with Scroll-Triggered Animation</title>: Sets the title of the webpage displayed in the browser tab.
  • <link rel="stylesheet" href="styles.css">: Links to an external CSS file that styles the page.

<body>

The main content of the page is inside the <body> tag. It contains:

<main>

The main section where the photo gallery resides.

<div id="gallery">

A container (div) that holds all the photo gallery content. Each photo and its caption are wrapped in a <figure> tag.

<figure>

Each <figure> element contains:

  • <img>: The image being displayed, including the source (src), alternative text (alt), and the title of the image.
  • <figcaption>: A caption that describes the photo, such as the time and season of the shot.

Here is a typical structure:

<figure>
    <img src="image_url" alt="image_description" title="image_title">
    <figcaption>Photo Caption (e.g., 8 PM, Summer)</figcaption>
</figure>

Some figures contain a block with additional information (Notes and related links) instead of an image.

Footer Section

At the bottom, there’s a footer with a link to the author's Codepen page:

<footer id='info'>
    Codepen by <a target="_blank" href="https://codepen.io/wakana-k/">Wakana Y.K.</a>
</footer>

External Resources

  • CSS (styles.css): This file is linked to styling the page. It is responsible for defining how the images, text, and layout appear.
  • JavaScript (script.js): This file is referenced at the end and is responsible for adding scroll-triggered animations to the gallery.

Images

The images are fetched from Unsplash via external URLs, with attributes like:

  • src: Defines the image URL.
  • alt: Provides alternative text for accessibility and SEO.
  • title: Displays a tooltip when hovering over the image.

The images show different cloud formations at various times of day and seasons, as described by the captions.

Step 2 (CSS Code):

Now, we’ll add basic CSS to style the gallery and photos. We will also use CSS animations to create the swinging effect. This will make them look nice on the page. Here's a breakdown of the CSS code:

1. Importing Font

@import url("https://fonts.googleapis.com/css2?family=Kalam:wght@400&display=swap");

This line imports the Kalam font from Google Fonts with a weight of 400 (normal).

2. Root and Global Styles

:root {
    --adjust-size: 0px;
}

:root is the highest-level selector in CSS (similar to html), and it defines a CSS variable --adjust-size, which can be reused throughout the stylesheet.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

The universal selector * resets the margin and padding for all elements and applies the box-sizing: border-box rule, which ensures that padding and border are included in an element's total width and height.

3. Base Styles for HTML and Body

html, body {
    overscroll-behavior-x: none;
    overscroll-behavior-y: none;
    scroll-behavior: smooth;
}

This prevents unwanted overscrolling and enables smooth scrolling when clicking on anchor links.

body {
    position: relative;
    color: #222;
    font-family: "Kalam", sans-serif;
    min-height: 100vh;
    overflow-x: hidden;
    background-image: url("image-url");
    background-size: cover;
}

The body element is styled with the Kalam font, a dark text color (#222), and a full-page background image that covers the entire screen.

4. Main Container

main {
    display: flex;
    justify-content: center;
    align-items: center;
    max-width: 100vw;
    min-height: 100vh;
    overflow-x: hidden;
}

The main element is a flex container that centers its content both horizontally and vertically.

5. Basic Element Styles

p {
    line-height: 1;
}

a {
    color: crimson;
    text-decoration: none;
}

img {
    user-select: none;
    pointer-events: none;
}
  • Paragraphs (p) have a line height of 1 (no extra space between lines).
  • Anchor tags (a) are styled with the color crimson and no underline.
  • Images (img) cannot be selected or interacted with (pointer-events: none).

6. Gallery Styles

The gallery section uses CSS Grid for layout and defines some animations for the gallery items.

#gallery {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 20px;
    max-width: 100vw;
    padding: 20px;
}
  • The gallery uses a responsive grid layout where each column has a minimum width of 150px and auto-fits to the available space.
  • There’s a 20px gap between grid items.

Each figure within the gallery is styled uniquely using the nth-child() selector:

#gallery figure:nth-child(7n) { --duration: 1s; --pin-color: crimson; }
#gallery figure:nth-child(odd) { --direction: alternate; }
#gallery figure:nth-child(even) { --direction: alternate-reverse; }
  • Different nth-child selectors assign specific animation durations and colors to the gallery items.

Each figure is set up for a swinging animation:

@keyframes swing {
    0% { transform: rotate3d(0, 0, 1, calc(-1 * var(--angle))); }
    100% { transform: rotate3d(0, 0, 1, var(--angle)); }
}
  • The keyframe swing rotates the element around the Z-axis based on the --angle variable.

7. Figure Styles

#gallery figure {
    margin: var(--adjust-size);
    padding: 0.5rem;
    box-shadow: 0 7px 8px rgba(0, 0, 0, 0.4);
    background-color: ghostwhite;
    transform-origin: center 0.22rem;
    will-change: transform;
}
  • Each figure in the gallery has a shadow, a light background color, and a defined origin for the transformation effects.

The figure tag contains both images and captions with styles:

figure img {
    width: 100%;
    object-fit: cover;
    border-radius: 5px;
}

figure figcaption {
    font-size: 14px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

8. Media Queries

@media (min-width: 800px) {
    #gallery {
 	   grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    }
}

A media query adjusts the minimum column width of the gallery to 180px for screens wider than 800px.

@import url("https://fonts.googleapis.com/css2?family=Kalam:wght@400&display=swap");
:root {
	--adjust-size: 0px; 
}
* {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}
html,
body {
	overscroll-behavior-x: none;
	overscroll-behavior-y: none;
	scroll-behavior: smooth;
}
body {
	position: relative;
	color: #222;
	font-family: "Kalam", sans-serif;
	min-height: 100vh;
	overflow-x: hidden;
	background-image: url("https://images.unsplash.com/photo-1531685250784-7569952593d2?crop=entropy&cs=srgb&fm=jpg&ixid=M3wzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2OTMyOTE2OTh8&ixlib=rb-4.0.3&q=100&w=3000");
	background-size: cover;
}
main {
	position: relative;
	display: flex;
	justify-content: center;
	align-items: center;
	max-width: 100vw;
	min-height: 100vh;
	overflow-x: hidden;
}
p {
	line-height: 1;
}
a {
	color: crimson;
	text-decoration: none;
}
img {
	-moz-user-select: none;
	-webkit-user-select: none;
	-ms-user-select: none;
	user-select: none;
	pointer-events: none;
}
#gallery {
	position: relative;
	left: calc(-1 * var(--adjust-size));
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
	gap: 20px;
	max-width: 100vw;
	padding: 20px;
	-webkit-perspective: 0;
	perspective: 0;
}
#gallery figure:nth-child(7n) {
	--duration: 1s;
	--pin-color: crimson;
}
#gallery figure:nth-child(7n + 1) {
	--duration: 1.8s;
	--pin-color: hotpink;
}
#gallery figure:nth-child(7n + 2) {
	--duration: 1.3s;
	--pin-color: magenta;
}
#gallery figure:nth-child(7n + 3) {
	--duration: 1.5s;
	--pin-color: orangered;
}
#gallery figure:nth-child(7n + 4) {
	--duration: 1.1s;
	--pin-color: darkorchid;
}
#gallery figure:nth-child(7n + 5) {
	--duration: 1.6s;
	--pin-color: deeppink;
}
#gallery figure:nth-child(7n + 6) {
	--duration: 1.2s;
	--pin-color: mediumvioletred;
}
#gallery figure:nth-child(3n) {
	--angle: 3deg;
}
#gallery figure:nth-child(3n + 1) {
	--angle: -3.3deg;
}
#gallery figure:nth-child(3n + 2) {
	--angle: 2.4deg;
}
#gallery figure:nth-child(odd) {
	--direction: alternate;
}
#gallery figure:nth-child(even) {
	--direction: alternate-reverse;
}
#gallery figure {
	--angle: 3deg;
	--count: 5;
	--duration: 1s;
	--delay: calc(-0.5 * var(--duration));
	--direction: alternate;
	--pin-color: red;

	position: relative;
	display: inline-block;
	margin: var(--adjust-size);
	padding: 0.5rem;
	border-radius: 5px;
	box-shadow: 0 7px 8px rgba(0, 0, 0, 0.4);
	width: 100%;
	height: auto;
	text-align: center;
	background-color: ghostwhite;
	background-image: url("https://images.unsplash.com/photo-1629968417850-3505f5180761?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2OTMzMjQ3ODJ8&ixlib=rb-4.0.3&q=80&w=500");
	background-size: cover;
	background-position: center;
	background-blend-mode: multiply;

	transform-origin: center 0.22rem;
	will-change: transform;
	break-inside: avoid;
	overflow: hidden;
	outline: 1px solid transparent;
	-webkit-backface-visibility: hidden;
	backface-visibility: hidden;
}
#gallery.active figure {
	animation-duration: var(--duration), 1.5s;
	animation-delay: var(--delay),
		calc(var(--delay) + var(--duration) * var(--count));
	animation-timing-function: ease-in-out;
	animation-iteration-count: var(--count), 1;
	animation-direction: var(--direction), normal;
	animation-fill-mode: both;
	animation-name: swing, swingEnd;
}
#gallery figure:after {
	position: absolute;
	top: 0.22rem;
	left: 50%;
	width: 0.7rem;
	height: 0.7rem;
	content: "";
	background: var(--pin-color);
	border-radius: 50%;
	box-shadow: -0.1rem -0.1rem 0.3rem 0.02rem rgba(0, 0, 0, 0.5) inset;
	filter: drop-shadow(0.3rem 0.15rem 0.2rem rgba(0, 0, 0, 0.5));
	transform: translateZ(0);
	z-index: 2;
}
figure img {
	aspect-ratio: 1 /1;
	width: 100%;
	object-fit: cover;
	display: block;
	border-radius: 5px;
	margin-bottom: 10px;
	z-index: 1;
}
figure figcaption {
	font-size: 14px;
	font-weight: 400;
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
	z-index: 1;
}
figure h2 {
	color: crimson;
	font-size: 22px;
}
figure p {
	font-size: 17px;
}
figure small {
	font-size: 12px;
}
figure > div {
	width: 100%;
	height: 100%;
	position: relative;
	display: flex;
	justify-content: center;
	align-items: center;
}
@keyframes swing {
	0% {
		transform: rotate3d(0, 0, 1, calc(-1 * var(--angle)));
	}
	100% {
		transform: rotate3d(0, 0, 1, var(--angle));
	}
}
@keyframes swingEnd {
	to {
		transform: rotate3d(0, 0, 1, 0deg);
	}
}
#info {
	position: relative;
	text-align: center;
	z-index: 1;
}
#info a {
	font-size: 1.1rem;
}

@media (min-width: 800px) {
	#gallery {
		grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
	}
} 

Step 3 (JavaScript Code):

This JavaScript code creates an animation for an element (the gallery) that starts when the user scrolls or resizes the window. Here's a breakdown of how the code works:

1. IIFE (Immediately Invoked Function Expression):

The entire script is wrapped in an IIFE to prevent polluting the global scope. This means the function is executed as soon as it's defined.

(function () {
    // Code inside runs immediately
})();

2. window.onload Event:

This ensures the script runs only after the entire page (HTML, CSS, images, etc.) is fully loaded. Once the page is loaded, the script begins.

window.onload = () => {
    // Code executed after the page loads
};

3. Selecting the Gallery Element:

The querySelector method is used to select the element with the id="gallery". This element is stored in the variable obj.

const obj = document.querySelector("#gallery");

4. Animation Time:

A constant time is defined, which sets the duration for how long the animation will run (10,000 milliseconds or 10 seconds).

const time = 10000;

5. Starting the Animation (animStart Function):

The animStart function starts the animation on the #gallery element if it doesn't already have the class active.

  • Check Active Class: The function checks if the #gallery element has the active class using classList.contains. If it doesn't have it, the class is added.
    if (obj.classList.contains("active") == false) {
        obj.classList.add("active");
    }
  • Timeout to End Animation: A setTimeout is used to remove the active class after the defined time (10 seconds), which stops the animation.
    setTimeout(() => {
        animEnd();
    }, time);

6. Ending the Animation (animEnd Function):

The animEnd function removes the active class from the #gallery element, effectively stopping the animation. The line obj.offsetWidth; is used to force a reflow (recalculating the layout), ensuring that subsequent animations will run smoothly.

obj.classList.remove("active");
obj.offsetWidth; // Force reflow to restart animation

7. Triggering Animation on Events:

The animation is triggered in three scenarios:

  • On Scroll: Every time the user scrolls, the animation starts.
    document.addEventListener("scroll", function () {
        animStart();
    });
  • On Window Resize: When the window is resized, the animation also starts.
    window.addEventListener("resize", animStart);
  • Initial Animation Start: The animation starts as soon as the page is loaded.
    animStart();
"use strict";
(function () {
	window.onload = () => {
		const obj = document.querySelector("#gallery");
		const time = 10000;
		function animStart() {
			if (obj.classList.contains("active") == false) {
				obj.classList.add("active");
				setTimeout(() => {
					animEnd();
				}, time);
			}
		}
		function animEnd() {
			obj.classList.remove("active");
			obj.offsetWidth;
		}
		document.addEventListener("scroll", function () {
			// scroll or scrollend
			animStart();
		});
		window.addEventListener("resize", animStart);
		animStart();
	};
})();

Final Output:

swinging-photo-gallery-using-html-css-and-javascript.gif

Conclusion:

That’s it! You've successfully created a swinging pinned photo gallery with a scroll-triggered animation using just HTML, CSS, and JavaScript. This effect is perfect for creating an engaging and modern look for your image galleries. You can easily customize it further by adding more animations or styles based on your project’s needs.

By following this guide, you’ve learned how to:

  • Set up an HTML photo gallery.
  • Style it using CSS.
  • Add swinging animations.
  • Trigger animations when the user scrolls.

This simple yet eye-catching gallery will surely capture the attention of your visitors.

Design and Code by: Wakana Y.K.

That’s a wrap!

I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Post

Please allow ads on our site🥺