CSS Grid + VueJS

A simple, responsive, example of css grid in use. Made with VueJS and Last.fm APIs.

Use the sliders to change the size of the grid and see how it scales responsively.

Made with

Html
Css/SCSS
Javascript

Html

<div id="app">

    <header>
        <h1><img src="https://iamketan.com.au/lab/spotify/spotify-icon-green.png" alt="Spotify" title="Spotify"> Recently Played Albums</h1>
        <p>A simple example of css grid in action.</p>
    </header>

    <section id="controls">
        <div>
            <h4>CSS Grid</h4>
            <p>Use the sliders to change the size of the grid and see how it scales responsively.</p>
            <code>
            <span>display:</span> grid;<br>
            <span>grid-template-columns:</span> repeat(auto-fill, 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="grip-min" min="100" max="375" step="5" v-model="gridMin" v-on:change="changeGridMin">
        </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>

Css

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

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

* {
    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;
}

p {
    margin-bottom: 16px;
}

code {
    display: block;
    font-family: monospace;
    font-size: 14px;
    color: $spotify-green;
    border-left: 2px solid rgba($spotify-black, 0.75);
    padding-left: 10px;
    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);
    }
}

// GRID
#app main {
    display: grid;
    grid-template-columns: repeat(auto-fill, 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;
}

Javascript

var app = new Vue({
    el: '#app',
    data: {
        albums: [],
        isActive: false,
        maxPlayCount: 0,
        gridGap: 30,
        gridMin: 175
    },
    mounted() {
        axios.get('https://iamketan.com.au/lab/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');
        }
    },
    computed: {
    },
    filters: {
        percentage: function(value) {
            return parseInt((value * 100) / app.$data.maxPlayCount);
        }
    }
});

Author

Ketan Mistry

Demo

See the Pen CSS Grid + VueJS by Ketan Mistry (@ketanmistry) on CodePen.