Photo by BoliviaInteligente on Unsplash
How to Make Your Own Instagram-Like App
Create an Instagram Clone with HTML, CSS, and JavaScript
We will together create a simple Instagram (knockoff ha!) clone using epic self portraits from some amazing artists. We shall call it Oldagram.
We will start by creating this static post then we will use it as a template to dynamically generate multiple posts later.
You can get all the resources from my repository.
Project setup
In your terminal, create a directory where your work will be located, then create the three files we will be working with in the created directory:
~$ mkdir oldagram
~$ cd !$
~/oldagram$ touch index.html index.css index.js
From within the new directory, open VSCode:
~/oldagram$ code .
VSCode will open in the oldagram
directory. You should see the three files we created in there. Should you choose to use a different editor, that is quite alright.
The page structure
Let's start with the HTML and define the page structure.
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Oldagram</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<script src="index.js"></script>
</body>
</html>
In index.html
we link to the CSS file within the header and the JavaScript file in the body.
For the fonts, we shall use "Source Sans 3" from Google fonts. Let's link them within the head section:
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="index.css">
<title>Oldagram</title>
</head>
Favicons
It is a good idea to include a favicon in your app so the icon displays on your browser tab. You can use one of the many online tools to generate favicons (for example this one). Create a directory for the icons, then include them in your head section.
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="./images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="./images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="./images/favicon-16x16.png">
<link rel="manifest" href="./images/site.webmanifest">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<!-- Styles -->
<link rel="stylesheet" href="index.css">
<title>Oldagram</title>
</head>
The favicons are included in images
folder within our oldagram
directory.
Now that the head section is set up, let's move to the body section. We will use semantic elements to structure our page. We will wrap everything within a container class div:
<body>
<div class="container">
<header></header>
<main></main>
</div>
<script src="index.js"></script>
</body>
In the header
section, let's add the header image, and the user's avatar image.
<body>
<div class="container">
<header>
<img class="header-img" src="./images/logo.png">
<img class="user-avatar" src="./images/user-avatar.jpeg">
</header>
<main></main>
</div>
<script src="index.js"></script>
</body>
in the main
section, let's add markup for a single post. This is static for now, but we will later use it as a template to dynamically generate multiple posts.
<body>
<div class="container">
<header>
<img class="header-img" src="./images/logo.png">
<img class="user-avatar" src="./images/user-avatar.jpeg">
</header>
<main>
<div class="post">
<div class="post-header">
<img class="post-header-img" src="./images/avatar-vangogh.jpg">
<div class="post-header-info">
<h3 class="post-header-name">Vincent van Gogh</h3>
<p class="post-header-location">Zundert, Netherlands</p>
</div>
</div>
<div class="post-image">
<img src="./images/post-vangogh.jpg">
</div>
<div class="post-user-actions">
<img class="heart" src="./images/icon-heart.png">
<img class="comment" src="./images/icon-comment.png">
<img class="dm" src="./images/icon-dm.png">
</div>
<div class="post-likes">
<p class="bold-text">21 likes</p>
</div>
<div class="post-comments">
<p class="comment-text"><span class="bold-text">vincey1853</span> just took a few mushrooms lol</p>
</div>
</div>
</main>
</div>
<script src="index.js"></script>
</body>
The markup looks ugly and does't make sense. Let's fix this with some styling.
Styling
In index.css
, let's bring in the font styles and default resets for our elements. We will also include a utility class to make text bold.
html, body {
font-family: "Source Sans 3", sans-serif;
font-size: 16px;
font-weight: normal;
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: #F5F5F5;
}
/* Utility class */
.bold-text {
font-weight: 700;
}
Next, let's style the container
class wrapping the entire HTML structure:
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
width: 375px;
margin: 20px auto;
background-color: #EDEDED;
}
We want the container to be 375 pixels and vertically centered. Next, let's style the header
and main
sections:
/* Header */
header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background-color: #FFF;
border-bottom: 1px solid #C6C6C6;
}
header .header-img {
width: 127px;
margin-left: 8px;
object-fit: contain;
}
header .user-avatar {
width: 34px;
margin: 16px;
border-radius: 50%;
}
/* main */
main {
display: flex;
flex-direction: column;
align-items: center;
gap: 25px;
width: 100%;
}
Finally, we style the post
section. This is what we will use as our template:
.post {
display: flex;
flex-direction: column;
width: 100%;
padding: 10px 0 16px;
background-color: #FFF;
}
/* Post header */
.post .post-header {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 0 10px 10px;
width: 100%;
}
.post .post-header .post-header-img {
width: 34px;
border-radius: 50%;
}
.post .post-header .post-header-info {
display: flex;
flex-direction: column;
justify-content: center;
}
.post .post-header .post-header-info h3,
.post .post-header .post-header-info p {
line-height: 1.4;
margin: 0;
}
.post .post-header .post-header-info h3{
font-size: .8rem;
}
.post .post-header .post-header-info p {
font-size: .75rem;
}
/* Post image */
.post .post-image img {
width: 100%;
}
/* User actions */
.post .post-user-actions {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 14px;
padding: 20px 10px 0;
width: 100%;
}
.post .post-user-actions img {
width: 24px;
transition: opacity 0.3s ease;
}
.post .post-user-actions img:hover {
cursor: pointer;
opacity: 0.7;
}
/* Post likes, comments */
.post .post-likes,
.post .post-comments {
padding: 12px 0 0 12px;
}
.post .post-likes p,
.post .post-comments p {
margin: 0;
font-size: 0.8rem;
}
Your app should now look like this:
Templating
Now that we have refined the HTML, we can make a template from it for dynamic element display.
The <template>
tag is used as a container to hold some HTML content hidden from the user when the page loads. The content inside <template>
can be rendered later with a JavaScript. [Source]
Below the main
section, create a template
tag:
...
<div class="post-comments">
<p class="comment-text"><span class="bold-text">vincey1853</span> just took a few mushrooms lol</p>
</div>
</div>
</main>
<template id="post-template"></template>
Next, move the content of the main
section inside this template and remove the static text and image content.
<body>
<div class="container">
<header>
<img class="header-img" src="./images/logo.png">
<img class="user-avatar" src="./images/user-avatar.jpeg">
</header>
<main></main>
<template id="post-template">
<div class="post">
<div class="post-header">
<img class="post-header-img" src="">
<div class="post-header-info">
<h3 class="post-header-name"></h3>
<p class="post-header-location"></p>
</div>
</div>
<div class="post-image">
<img src="">
</div>
<div class="post-user-actions">
<img class="heart" src="">
<img class="comment" src="">
<img class="dm" src="">
</div>
<div class="post-likes">
<p class="bold-text"></p>
</div>
<div class="post-comments">
<p class="comment-text"><span class="bold-text"></span></p>
</div>
</div>
</template>
</div>
<script src="index.js"></script>
</body>
main
should now be empty. The app will now only have the header. The earlier created post doesn't show any more.
Don't worry about this. We will render posts dynamically. This is where JavaScript comes in.
The JavaScript
In index.js
, we will create two arrays: one for the posts we will render dynamically, and another for the icons in the post-user-actions
section.
const posts = [
{
name: 'Vincent van Gogh',
username: 'vincey1853',
location: 'Zundert, Netherlands',
avatar: 'images/avatar-vangogh.jpg',
post: 'images/post-vangogh.jpg',
comment: 'just took a few mushrooms lol',
likes: 21,
},
{
name: 'Gustave Courbet',
username: 'gus1819',
location: 'Ornans, France',
avatar: 'images/avatar-courbet.jpg',
post: 'images/post-courbet.jpg',
comment: "i'm feelin a bit stressed tbh",
likes: 4,
},
{
name: 'Joseph Ducreux',
username: 'jd1735',
location: 'Paris, France',
avatar: 'images/avatar-ducreux.jpg',
post: 'images/post-ducreux.jpg',
comment:
'gm friends! which coin are YOU stacking up today?? post below and WAGMI!',
likes: 152,
},
];
const icons = ['heart', 'comment', 'dm'];
We then need to fetch two elements: the main
element on which the posts will be dynamically populated, and the template
that we will use to inject posts from our posts
array.
// Get main element
const mainEl = document.querySelector('main');
// Get template
const postTemplate = document.getElementById('post-template');
We will listen the window load
event before posts are loaded. [Source]. Before loading this content, we need to first clear the main
element:
window.addEventListener('load', () => {
mainEl.textContent = '';
});
Next, we will loop through the posts
. For each iteration, clone the contents of the template:
window.addEventListener('load', () => {
mainEl.textContent = '';
posts.forEach((post) => {
// Clone template
const templateClone = postTemplate.content.cloneNode(true);
});
});
Using this clone, we can then access the elements we are interested in and inject the relevant image and text content. For example, the header
section:
window.addEventListener('load', () => {
mainEl.textContent = '';
posts.forEach((post) => {
// Clone template
const templateClone = postTemplate.content.cloneNode(true);
// Header section
const postHeaderImg = templateClone.querySelector('.post-header-img');
postHeaderImg.src = post.avatar;
const postHeaderName = templateClone.querySelector('.post-header-name');
postHeaderName.textContent = post.name;
const postHeaderLocation = templateClone.querySelector(
'.post-header-location'
);
postHeaderLocation.textContent = post.location;
});
});
The code is accessing the elements using their classes then simply assigning the data for that element.
window.addEventListener('load', () => {
mainEl.textContent = '';
posts.forEach((post) => {
// Clone template
const templateClone = postTemplate.content.cloneNode(true);
// Header section
const postHeaderImg = templateClone.querySelector('.post-header-img');
postHeaderImg.src = post.avatar;
const postHeaderName = templateClone.querySelector('.post-header-name');
postHeaderName.textContent = post.name;
const postHeaderLocation = templateClone.querySelector(
'.post-header-location'
);
postHeaderLocation.textContent = post.location;
// Post section
const postImg = templateClone.querySelector('.post-image img');
postImg.src = post.post;
// User action section
icons.forEach((icon) => {
const iconEl = templateClone.querySelector(`.${icon}`);
iconEl.src = `./images/icon-${icon}.png`;
});
// Likes section
const likesEl = templateClone.querySelector('.post-likes p');
likesEl.textContent = `${post.likes} likes`;
// Comments section
const commentEl = templateClone.querySelector('.comment-text');
const username = templateClone.querySelector(
'.comment-text .bold-text'
);
username.textContent = post.username;
commentEl.innerHTML += ' ' + post.comment;
For the "User action section", we loop through the icons
array to get the icon names. Since the icon names correspond to the icon names in the images
folder, we can simply inject them in the image names.
Finally, once the clone is populated with the post data, we append it to the main element:
const posts = [
{
name: 'Vincent van Gogh',
username: 'vincey1853',
location: 'Zundert, Netherlands',
avatar: 'images/avatar-vangogh.jpg',
post: 'images/post-vangogh.jpg',
comment: 'just took a few mushrooms lol',
likes: 21,
},
{
name: 'Gustave Courbet',
username: 'gus1819',
location: 'Ornans, France',
avatar: 'images/avatar-courbet.jpg',
post: 'images/post-courbet.jpg',
comment: "i'm feelin a bit stressed tbh",
likes: 4,
},
{
name: 'Joseph Ducreux',
username: 'jd1735',
location: 'Paris, France',
avatar: 'images/avatar-ducreux.jpg',
post: 'images/post-ducreux.jpg',
comment:
'gm friends! which coin are YOU stacking up today?? post below and WAGMI!',
likes: 152,
},
];
const icons = ['heart', 'comment', 'dm'];
// Get main element
const mainEl = document.querySelector('main');
// Get post template
const postTemplate = document.getElementById('post-template');
window.addEventListener('load', () => {
mainEl.textContent = '';
posts.forEach((post) => {
// Clone template
const templateClone = postTemplate.content.cloneNode(true);
// Header section
const postHeaderImg = templateClone.querySelector('.post-header-img');
postHeaderImg.src = post.avatar;
const postHeaderName = templateClone.querySelector('.post-header-name');
postHeaderName.textContent = post.name;
const postHeaderLocation = templateClone.querySelector(
'.post-header-location'
);
postHeaderLocation.textContent = post.location;
// Post section
const postImg = templateClone.querySelector('.post-image img');
postImg.src = post.post;
// User action section
icons.forEach((icon) => {
const iconEl = templateClone.querySelector(`.${icon}`);
iconEl.src = `./images/icon-${icon}.png`;
});
// Likes section
const likesEl = templateClone.querySelector('.post-likes p');
likesEl.textContent = `${post.likes} likes`;
// Comments section
const commentEl = templateClone.querySelector('.comment-text');
const username = templateClone.querySelector(
'.comment-text .bold-text'
);
username.textContent = post.username;
commentEl.innerHTML += ' ' + post.comment;
// Attach post element to main
mainEl.appendChild(templateClone);
});
});
Your app should now display the three posts dynamically. You can add or remove posts and they will be rendered automatically.
Stretch goals
Congratulations for having made it this far. I now leave you with an enjoyable challenge:
- When user clicks on the heart icon (or double-clicks the post image), the likes should increase 1. If they click (double-click again) the decrease by 1. In other words, you can only like once.
If you can add on any other challenges, contact me and let know how it was. I would love to hear from you.