Create a stunning swinging pinned photo gallery with scroll-triggered animation using HTML, CSS, and JavaScript.
Table of Contents
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:
- Basic understanding of HTML, CSS, and JavaScript.
- A code editor like Visual Studio Code.
- A browser to test your code, such as Google Chrome or Firefox.
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 theactive
class usingclassList.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 theactive
class after the definedtime
(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:
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 😊