<div id="app">

    <header>
        <h1><img src="https://iamketan.design/wp-content/themes/iakwp/assets/site_v2/images/icons/apple-music.svg" alt="Apple Music" title="Apple Music"> Recently Played Albums</h1>
        <p>A simple example of css grid in action.</p>
    </header>

    <section id="controls">
        <div>
            <h4>CSS Grid</h4>
            <p>The CSS Grid Layout Module offers a grid-based layout system, with rows and columns, making it easier to design web pages. Grids can be used to layout major page areas or small user interface elements. </p>
            <p>Use the sliders to change the size of the grid and number of items per row. </p>
            <code>
            <span>display:</span> grid;<br>
                <span>grid-template-columns:</span> repeat(<strong v-if="gridItems > 0">{{ gridItems }}</strong><strong v-else>auto-fill</strong>, minmax(<strong>{{ gridMin }}px</strong>, 1fr));<br>
            <span>grid-gap:</span> <strong>{{ gridGap }}px</strong>;
            </code>
        </div>
        <div>
            <h4>Grid Gap <em>&mdash; the size of the gutter</em> <label for="grid-gap">{{ gridGap }}</label></h4>
            <input type="range" id="grid-gap" min="10" max="80" step="5" v-model="gridGap" v-on:change="changeGridGap">

            <h4>Grid Size <em>&mdash; the minimum size of each grid item</em> <label for="grip-min">{{ gridMin }}</label></h4>
            <input type="range" id="grid-min" min="100" max="375" step="5" v-model="gridMin" v-on:change="changeGridMin">
            
            <h4>Grid Items <em>&mdash; number of items per row</em>
                <label for="grid-items" v-if="gridItems > 0">{{ gridItems }}</label>
                <label for="grid-items" v-else>auto-fill</label>
            </h4>
            <input type="range" id="grid-items" min="0" max="10" step="1" v-model="gridItems" v-on:change="changeGridItems">
        </div>
    </section>

    <transition-group tag="main" name="card">
        <article v-for="(album, index) in albums" :key="index" class="card" >
            <a :href="album.url" target="_blank">
                <div class="image" v-for="image in album.image" v-if="image.size == 'extralarge'">
                    <img v-if="image['#text'] !== ''" :src="image['#text']" :alt="album.name" v-on:load="isLoaded()" v-bind:class="{ active: isActive }">
                    <img v-else src="https://source.unsplash.com/random/300x300" :alt="album.name" v-on:load="isLoaded()" v-bind:class="{ active: isActive }">
                </div>
                <div class="description">
                    <span class="playcount">
                        <span v-bind:style="{width: m_percentage(album.playcount) + '%'}"></span>
                    </span>
                    <h3 class="title" :data-mbid="album.mbid">{{ album.name }}</h3>
                    <p class="artist">{{ album.artist.name }}</p>
                </div>
            </a>
        </article>
    </transition-group>

</div>
$background: #222129;
$card: #2B2A34;
$spotify-green: rgb(30, 215, 96);
$spotify-black: rgb(25, 20, 20);

:root {
    --grid-gap: 30px;
    --grid-min: 175px;
    --grid-items: auto-fill;
}

* {
    margin: 0;
    padding: 0;
    line-height: 1.5em;
}

body {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    background-color: $background;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    font-size: 14px;
    color: white;
}

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

h1 {
    font-size: 25px;
    color: white;
}

h3 {
    font-size: 14px;
    margin-bottom: 5px;
}

h4 {
    font-size: 14px;
    margin-bottom: 10px;
    font-weight: 700;
}

p {
    margin-bottom: 16px;
    line-height: 1.5em;
}

code {
    display: block;
    font-family: monospace;
    font-size: 14px;
    color: $spotify-green;
    border-left: 2px solid rgba($spotify-black, 0.75);
    padding-left: 10px;
    line-height: 2em;
    span {
        color: white;
    }
    strong {
        background-color: $spotify-black;
        padding: 2px;
    }
}

// HEADER
header {
    display: flex;
    align-items: center;
    padding: 20px 4%;
    margin-bottom: 20px;
    background: $spotify-black;
    @media screen and (max-width:768px) {
        flex-flow: column;
    }
    h1, p {
        width: 50%;
        @media screen and (max-width:768px) {
            width: 100%;
        }
    }
    p {
        text-align: right;
        color: rgba(white, 0.65);
        @media screen and (max-width:768px) {
            text-align: left !important;
            margin-top: 10px;
            text-indent: 63px;
        }
    }
    img {
        width: 48px;
        height: 48px;
        vertical-align: middle;
        margin-right: 10px;
    }
}

// GRID CONTROLS
section#controls {
    margin: 3% 2%;
    display: flex;
    & > div {
        width: 50%;
        padding: 0 2%;
        input[type="range"] {
            width: 100%;
            margin-bottom: 30px;
        }
        label {
            background: $spotify-green;
            color: white;
            padding: 2px 4px;
            border-radius: 2px;
            float: right;
        }
    }
    h4 em {
        font-style: normal;
        font-weight: 400;
        color: rgba(white, 0.4);
    }
    p {
        color: rgba(white, 0.70);
    }
}

// GRID
#app main {
    display: grid;
    grid-template-columns: repeat(var(--grid-items), minmax(var(--grid-min), 1fr));
    grid-gap: var(--grid-gap);
    counter-reset: rank;
    margin: 4%;

    article {
        counter-increment: rank;
        position: relative;
        background: $card;
        box-shadow: 0 1px 5px rgba(0,0,0,0.2);
        border-radius: 4px;
        overflow: hidden;
        animation: mouseOut 0.3s ease-in;
        .image {
            position: relative;
            width: 100%;
            &:after {
                // This forces the image container to be a square
                content: '';
                display: block;
                padding-bottom: 100%;
            }
            &:before {
                content: '•••';
                font-size: 24px;
                position: absolute;
                display: flex;
                width: 100%;
                height: 100%;
                align-items: center;
                justify-content: center;
                color: rgba(white, 0.1);
                z-index: 0;
            }
            img {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                z-index: 10;
                opacity: 0;
                &.active {
                    animation: imageFadeIn 0.5s ease-in forwards;
                    animation-delay: 0.5s;
                }
            }
        }
        .description {
            padding-bottom: 10px;
            h3, p {
                padding: 0 10px;
            }
            p.artist {
                color: #666;
                text-transform: uppercase;
                font-size: 11px;
                font-weight: 700;
                margin-bottom: 0;
                &:before {
                    content: '';
                    display: block;
                    width: 25px;
                    height: 2px;
                    margin-bottom: 4px;
                    background: $background;
                }
            }
        }
        &:before {
            content: '#'counter(rank);
            display: block;
            width: 25px;
            height: 20px;
            line-height: 20px;
            background: rgba($background, 0.75);
            color: white;
            position: absolute;
            z-index: 20;
            right: 0px;
            top: 0px;
            text-align: center;
            font-weight: 500;
            font-size: 12px;
        }
        .playcount {
            display: block;
            width: 100%;
            margin-bottom: 10px;
            font-size: 12px;
            span {
                position: relative;
                display: block;
                height: 2px;
                background: $spotify-green;
            }
        }
    }
    article:hover {
        animation: mouseOver 0.3s ease-in forwards;
    }
}

// ANIMATIONS
@keyframes mouseOver {
    0% {
        top: 0;
    }
    100% {
        top: -5px;
    }
}

@keyframes mouseOut {
    0% {
        top: -5px;
    }
    100% {
        top: 0;
    }
}

@keyframes imageFadeIn {
    0% {
        opacity: 0;
    }
    50% {
        opacity: 0.1;
    }
    100% {
        opacity: 1;
    }
}

// VUE TRANSITIONS: CARD FADEIN
.card-enter {
    opacity: 0;
}

.card-enter-to {
    opacity: 1;
}

.card-enter-active {
    transition: opacity 0.3s ease-in;
}
View Compiled
var app = new Vue({
    el: '#app',
    data: {
        albums: [],
        isActive: false,
        maxPlayCount: 0,
        gridGap: 30,
        gridMin: 175,
        gridItems: 0
    },
    mounted() {
        axios.get('https://foo.iamketan.design/.netlify/functions/spotify-albums')
            .then(response => (this.albums = response.data.topalbums.album, this.maxPlayCount = response.data.topalbums.album[0].playcount));
    },
    methods: {
        isLoaded: function() {
            this.isActive = true;
        },
        m_percentage: function(value) {
            return parseInt((value * 100) / app.$data.maxPlayCount);
        },
        changeGridGap: function() {
            document.querySelector('main').style.setProperty('--grid-gap', this.gridGap + 'px');
        },
        changeGridMin: function() {
            document.querySelector('main').style.setProperty('--grid-min', this.gridMin + 'px');
        },
        changeGridItems: function() {
            var gridItemSetting = this.gridItems;
            if (this.gridItems == 0) {
                gridItemSetting = 'auto-fill';
            }
            document.querySelector('main').style.setProperty('--grid-items', gridItemSetting);
        }
    },
    computed: {
    },
    filters: {
        percentage: function(value) {
            return parseInt((value * 100) / app.$data.maxPlayCount);
        }
    }
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17-beta.0/vue.js
  2. https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.js