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.