Matt Pignatore
Matt Pignatore

Matt Pignatore

Developing a UI with Vue, Part 2: Project Setup and Design System

Developing a UI with Vue, Part 2: Project Setup and Design System

This is the second in my series about developing an interface with Vue. We'll be setting up our design system, Vue Router, and first few components.

In my last post, I introduced the UI I'm going to be building for my first full-on side project with Vue. We did some basic setup of the project using the Vue CLI, but now we're going to get grounded with a design system for Medo, our movie watch list app.

You'll want to be familiar with HTML, SCSS, Flexbox, and the basics of front-end frameworks (components, state, props, etc.) in order to follow along. If you're not, head over to freecodecamp.org or something and dive in 🏊 The best way to learn is to build!

But first...

A basic design system 🎨

Lately I've made it a habit to begin projects by defining a design system up front.

The system I use is just a set of variables to be used throughout the application. The purpose it serves is not only for consistency in design, but it also makes our lives much easier: we'll no longer be getting bogged down choosing between 10px and 12px of padding, or between #this or #that shade of blue.

I came across this tip in a really great e-book called Refactoring UI, written by Adam Wathan and Steve Schoger of Tailwind fame.

They reason that, by limiting our options for spacing, font-size, color, etc. in our design, we're reducing the need to focus on these small decisions in the future.

A simple approach is to start with a sensible base value, then build a scale using factors and multiples of that value.

16px is a great number to start with because it divides nicely, and also happens to be the default font size in every major web browser.

The values at the small end of the scale should start pretty packed together, and get progressively more spaced apart as you get further up the scale.


You can check out the design system I'm using for Medo through the Github repo I have set up, but it generally looks something like this:

// color
$white: #ffffff;
$blue100: #f0f5ff;
$blue200: #c9ddff;
$gray100: #d6d6d6;
$gray200: #7c89a1;

// space 
$space1: 0.4rem;
$space2: 0.8rem;
$space3: 1.2rem;
$space4: 1.6rem;

// text size
$text1: 1.2rem;
$text2: 1.4rem;
$text3: 1.6rem;
$text4: 1.8rem;

// gradients
$bg: linear-gradient(180deg, white, $blue150);

I also often use mixins, which are just SCSS functions that take optional arguments (CSS properties and values) and spit out chunks of CSS code. I use Flexbox a lot, so this one has been particularly time saving:

@mixin flex(
  $direction: row, 
  $justify: flex-start, 
  $align: flex-start, 
  $grow: 0
) {
  display: flex;
  flex-direction: $direction;
  justify-content: $justify;
  align-items: $align;
  flex-grow: $grow;
}

// example use case
.some-flex-container {
  @include flex(row, justify-between, center);
}

Mixins are also great for things like media queries too!

I've been re-using these really simple design system settings in several projects recently and have found them very valuable! Highly recommended.


My First Mobile-First App 📱

Since my work on web apps hasn't required any mobile-friendly designs so far, I haven't developed enough as I'd like for smaller screen sizes. So, I want to take this chance to design this UI exclusively for mobile. Let's make that happen with a little bit of (S)CSS:

body {
  background: #a7cdff;
  font-size: 2.0rem;
  font-family: Lane, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased; // default preset with Vue CLI 
  -moz-osx-font-smoothing: grayscale; // default preset with Vue CLI
}
#app {
  height: 100vh;
  width: 100%;
  max-width: 428px;
  margin: 0 auto;
  background-color: $bg;
  background-attachment: fixed;
  box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.16);
  position: relative;
  overflow: scroll;
}

We’ll also set up a base template for our app in App.vue, which will house our <nav> element containing <router-link> tags, as well as the <router-view />.

<template>
  <nav id="nav" role="navigation" aria-label="Main Navigation">
    <ul>
      <li>
        <router-link to="/">Login</router-link>
      </li>
      <li>
        <router-link to="/register">Register</router-link>
      </li>
    </ul>
  </nav>
  <router-view />
</template>

We'll finish the nav off for now by extracting it to its own component called Navigation.vue in src/components/layout.

If the above route tags look unfamiliar to you, they're likely your introduction to Vue Router.

Routing with Vue 🛣️

Vue Router, like React Router, handles all of the users' URL requests for new pages and sub-pages. The <router-view /> you see in the template above is the window into each of our app's different pages/views, and the <router-link> tags are just anchor tags made special.

Rather than making a server request when new routes are visited through anchor tags (which is the default behavior of the browser), Vue Router makes the tags behave so that they're just rendering components instead; no page loads or server requests required.

In order to define our Medo routes, we utilize the index.js file in router/ and import our views. Since we selected to use Vue Router when we used Vue CLI for setup, the router has come already configured. All we need to do is import our own views, Login.vue and Register.vue, and define them in the routes array:

import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'

const routes = [
  {
    path: '/',
    name: 'Login',
    component: Login,
  },
  {
    path: '/register',
    name: 'Register',
    component: Register,
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
})

export default router

Note: I'm making the root of our app, /, route directly to the login page. In the future, we may make it so that this a marketing page that advertises features with a sign up CTA.

The final piece I want to cover is the footer, which will be appearing to the user on any public pages the user can view when not logged in. We're going to place that in the App.vue template just below the router, with a couple of changes to our HTML mark-up:

<template>
  <div class="page-wrapper">
    <Navigation />
    <div class="page">
      <router-view />
    </div>
    <footer role="navigation" aria-label="Footer Navigation">
      <img src="./assets/logo.png" alt="Medo logo" width="76" />
      <nav>
        <ul>
          <li>About</li>
          <li>Contact</li>
        </ul>
        <ul>
          <li>Login</li>
          <li>Register</li>
        </ul>
      </nav>
      <div class="copyright">
        <p>&copy; 2021, Medo</p>
      </div>
    </footer>
  </div>
</template>

We're doing a few things here:

  1. Wrapping the entire template in a div with class "page-container". This allows us to apply Flexbox to the app container, which has a min-height: 100vh.
  2. Wrapping the <router-view> element in a div with class "page". This class sets flex-grow: 1, pushing the footer to the bottom (as long as Step 1 is applied) no matter what content is placed here.
  3. Adding our footer markup. This includes an app logo, future navigation items, and a copyright tag.

Like we did with Navigation.vue, we'll extract the footer to its own component, so that the App.vue template looks like this:

<template>
  <div class="page-wrapper">
    <Navigation />
    <div class="page">
      <router-view />
    </div>
    <Footer />
  </div>
</template>

The script tag looks like this:

<script>
import Navigation from '@/components/layout/Navigation'
import Footer from '@/components/layout/Footer'

export default {
  name: 'App',
  components: {
    Navigation,
    Footer,
  },
}
</script>

And the style tag looks like this:

<style lang="scss">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html {
  font-size: 62.5%;
}
body {
  font-size: 2.0rem;
  font-family: Lane, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background: #a7cdff;
}
h1,
h2,
h3,
h4,
h5,
h6 {
  font-weight: 100;
}
p {
  line-height: 1.5;
}
ul {
  list-style: none;
  margin: 0;
}
#app {
  background: $bg;
  background-attachment: fixed;
  width: 100%;
  max-width: 428px;
  height: 100vh;
  margin: 0 auto;
  position: relative;
  box-shadow: 0px 0px 10px -2px rgba(0,0,0,0.16);
  overflow: scroll;
}
/* Hide scrollbarfor Chrome, Safari and Opera */
#app::-webkit-scrollbar {
  display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
#app {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox */
}
.page-wrapper {
  @include flex(column);
  min-height: 100vh;
}
.page {
  flex-grow: 1;
}
</style>

And we're finished!

In my next post, I'll be focused on developing mobile navigation and introducing state into our application for the first time. In the meantime, check out what led me to choose Vue.


You can find me on Twitter and Github , or you can check out my portfolio here . Feel free to reach out, I'd love to hear from you!

 
Share this