I've recently been getting into VueJS and CSS Grid. In the past I've always used frameworks like Bootstrap to built up my layout. However, since CSS Grids and VueJS have caught my attention, I'm trying to build a responsive layout using both.

My question now is, can (or should) one generally use vue components for the basic layout? I've built an off-canvas navigation for smaller viewpoints and turned it into a vue component:

hamburger.vue:

<template>
    <div class="hamburger">
        <div class="bar" :class="{ 'bar-top' : this.text }"></div>
        <div class="bar" :class="{ 'bar-middle' : this.text }"></div>
        <div class="bar" :class="{ 'bar-bottom' : this.text }"></div>
    </div>
</template>

<script>
    export default {
        props: ['text']
    }
</script>

<style lang="scss" scoped>
    @import "../../../sass/core/_variables";

    .hamburger {
        position: absolute;
        right: 15px;
        top: 15px;
        display: block;
        cursor: pointer;
        transition: transform .7s ease-in-out;

        @media only screen and (min-width: $tablet) {
            display: none;
        }
    }
    .bar {
        width: 30px;
        height: 4px;
        margin-bottom: 5px;
        background-color: #000000;
        border-radius: 3px;
        transition: all .7s ease;
    }
    .bar:last-child {
        margin-bottom: 0px;
    }
    .bar-top {
        transform: translateY(9px) rotateZ(45deg);
    }
    .bar-middle {
        opacity: 0;
    }
    .bar-bottom {
        transform: translateY(-10px) rotateZ(-45deg);
    }
</style>

And I am including it in my main layout like so:

<div id="root">
        <header role="banner" :class="{ 'nav-open' : mobile }">
            <div class="container">
                <h2>Header</h2>
                <hamburger :text="mobile" @click.native="mobile = !mobile"> </hamburger>
            </div>
            <section class="hero is-dark is-bold">
                <div class="hero-body">
                    <div class="container">
                        <h1 class="title">Hero title</h1>
                        <h2 class="subtitle">Hero subtitle</h2>
                    </div>
                </div>
            </section>
        </header>
        <nav role="navigation" :class="{ 'nav-open' : mobile }">
            <div class="container">
                <ul>
                    <li>Item 1</li>
                    <li>Item 2</li>
                    <li>Item 3</li>
                    <li>Item 4</li>
                    <li>Item 5</li>
                    <li>Item 6</li>
                </ul>
            </div>
        </nav>
        <section id="main" :class="{ 'nav-open' : mobile }">
            <main role="main">
                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
            </main>
            <aside>
                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim.</p>
            </aside>
        </section>
        <footer role="footer" :class="{ 'nav-open' : mobile }">
            <div class="container">
                <h2>Footer</h2>
            </div>
        </footer>
    </div>

So the way it works is, on my vue instance I set mobile = false, then by clicking the hamburger I can toggle it and therefore transition the off-canvas nav in.

navigation.scss:

@media only screen and (min-width: $tablet) {
    ul {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(100px, auto));
        > li {
            margin: 0 auto;
        }
    }
    .toggle {
        display: none;
    }
}

@media only screen and (max-width: $tablet - 1px) {
    nav {
        position: fixed;
        top: 0;
        bottom: 0;
        width: $nav;
        right: -$nav;
    }
    nav,
    header,
    section#main,
    footer {
        transition: transform .7s ease-in-out;
    }
    .nav-open {
        transform: translateX(-$nav);
    }
}

My main layout.scss:

/*! Mobile View */

#root {
    display: grid;
    grid-template-columns: 1fr;
}

/*! Desktop View */

#root > section #main {
    margin: 0 auto;
    @media only screen and (min-width: $tablet) {
        display: grid;
        grid-column-gap: 20px;
        grid-template-columns: repeat(10, 1fr);
        main {
            grid-column: span 7;
        }
        aside {
            grid-column: span 3;
        }
    }
    @media only screen and (min-width: $desktop) {
        width: 90%;
    }
}

Obviously this is just scaffolding. In the end I'm not sure about 3 things:

  • Should I keep the mobile / off-canvas navigation as a vue component, or incorporate in my layout scss files? I did it because because this way all the styling, JavaScript and HTML is contained in a single file. On the other hand, the way I learned it, one should create a component if one wants to reuse a particular piece of code more than once, which I'm obviously not doing here. I'm only using it once. Is it still viable (also in terms of performance)?

  • One thing I'm unsure about is :class="{ 'nav-open' : mobile }" in my markup. The way I build it I have repeat it several times in my markup (once for sliding in the off-canvas nav, but also three times for sliding out (to the right) the header, main section and footer). It's repetitive, but I don't know if it can be done better.

  • Would you have setup the Grid similarly using CSS Grid, or should I change something? I was trying to scaffold it with mobile-first in mind, while being as minimal / performant as possible.

I noticed that this question has been unanswered for some time, so I'll try to at least partially review your code.

Components re-use

Components are simply abstractions, an encapsulation of something that can stand on its own. If it is re-usable, that would be great, but it is not a requirement to make something their own component.

A navigation bar is something that can stand on it's own, so I would recommend putting it in a separate component, together with its styling. If most of your components are self-contained, your code will stay easy to reason about.

Repeated class definitions

You should move :class="{ 'nav-open' : mobile }" to the top-most element in your application. In your separate navigation, and footer, and header component you can then use the following SCSS:

// Namespacing of all your css
.navigation {
    // css if your navigation is closed

    .nav-open & {
        // css if your navigation is opened
    }
}

This makes sure that whatever future element you make that needs to do something when your navigation opens can do so without a problem, even if you change your layout significantly.

Opening and closing your navigation

You currently have your navigation and hamburger icon in the same file. This works, for now, but if your header, footer and navigation get bigger, your App.vue will become really bloated. I recommend using Vuex to keep the global state of the navigation bar, or a bus such as this one if you do not want to create a state.

You can then do this:

App.vue

<template>
  <div :class="appClasses">
    <app-header />
    <app-navigation />
    <app-footer />
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'app',
  computed: {
    ...mapGetters('global', ['navigationOpen']),

    appClasses () {
      return {
        'my-app': true,
        'nav-open': this.navigationOpen
      }
    }
  }
}
</script>

<style lang="scss">
  .my-app {
    // There should be very little css in your App.vue file
  }
</style>

A header.vue with a hamburger that toggles the menu

<template>
  <header class="app-header">
    <hamburger-menu @click="toggleMenu" />
  </header>
</template>

<script>
export default {
  name: 'app-header',
  methods: {
    toggleMenu () {
      this.$store.commit('global/toggleNavigation')
    }
  }
}
</script>

<style lang="scss">
.app-header {
  // Hopefully a bit more css here
}
</style>

And a store/modules/global.js file:

const state = {
  navigationOpen: false
}

const getters = {
  navigationOpen (state) {
    return state.navigationOpen
  }
}

const mutations = {
  toggleNavigation (state) {
    state.navigationOpen = !state.navigationOpen
  }
}

export default {
  state,
  getters,
  mutations
}

Avoid external scss files

The strength of Vue comes in that all javascript, styling and markup is in a single component file. If you move styling to a separate file, you are sort of defeating this strength as developers are required to switch between files again.

As a rule of thumb, try to limit external scss files to only contain variables, mixins and external dependencies.

Imports in components

You import a file with variables in one of your components. From experience I know this will get old really fast. I recommend splitting your external scss files in two parts:

  • Files that only contain variables and mixins, but no actual styling
  • Files that contain styling

You include the main styling file in your App.vue, so it only gets included once. The main variable/mixin file can be added to your webpack config, so that any scss in your component files can use those variables and mixins without an explicit import.

Odd naming of property

Your property this.text in your hamburger menu is named oddly. It seems to serve some purpose of making the hamburger visible, so I would recommend renaming it to visible. You can then add it as a single class to the div with class hamburger and style it from there.

Scss and scoped

The scoped attribute on your style tag makes Vue add a component identifying attribute to all html elements in that component. This is great when you have classes that could be used by other components or other elements on the same page, but also makes the generated html look rather ugly.

In most cases, the same can be accomplished by using unique names for your components and adding a class to the root element of that component. Then you can style your components by "namespacing" your css:

<template>
  <div class="my-component">
  </div>
</template>

<script>
export default {
  name: 'my-component'
}
</script>

<style lang="scss">
.my-component {
  .btn {
    // Styling will only be applied to buttons within this component
  }
}
</style>

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.