How I made a fluid layout component - WotW
Tags: webdev, vue, tutorial, wotw
Published: 2018-05-09 22:40:54

---
title: How I made a fluid layout component - WotW
published: true
description: Making a layout that changes with a button click
tags: ['webdev', 'vue', 'tutorial', 'wotw']
cover_image: https://thepracticaldev.s3.amazonaws.com/i/90swn6zsr4eyfu53agh2.jpeg
---

Welcome to the Widget of the Week series, where I take gifs or videos of awesome UI/UX components, and bring them to life with code.

Today I'll be doing a component that changes the width of a two column layout when a button is clicked. The inspiration for this widget comes from [this submission](https://uimovement.com/ui/5303/vr-class/) and looks like this:

![gif](https://thepracticaldev.s3.amazonaws.com/i/zwbjc2fwo6sw2wci3s0b.gif)

## Preparations

Like I've been doing before, we will be using [Vue.js](https://vuejs.org/) for the interactions and the [GSAP platform](https://greensock.com/gsap) for animations. If you use something like [codepen](http://codepen.io) you can find them in the libraries.

## The basic markup

For this component I'll be simplifying the content of it, I won't add any text and we will work only with a left pane, right pane and the action button:

``` html
<div id="app">
  <div class="container">
    <div class="left-pane"></div>
    <div class="right-pane"></div>
  </div>
  <div class="action-btn">+</div>
</div>
```

Don't get deceived by the simplicity of it, because that means that the CSS and JS will be doing the heavy lifting today.

## Styling

Let's start styling the parts of our component, but first some initial css setup on the body of our HTML:

``` css
body {
  margin: 0;
  padding: 0;
}
```
Now we will define a height on our container for our panes to have some volume. On the left pane we will have a solid color and on the right one we will set an centered background image that covers the entire pane. For the image I will use a placeholder from [placeimg](https://placeimg.com/) but it can be anything you want:

``` css
.container {
  height: 600px;
}
.left-pane {
  display: inline-block;
  background-color: #307490;
  width: 30%;
  height: 600px; /* should match containers height */
}
.right-pane {
  display: inline-block;
  width: 70%;
  height: 100%;
  height: 600px;/* should match containers height */
  
  background-image: url("https://placeimg.com/1000/600/nature");
  background-size: cover;
  background-repeat: no-repeat;
  background-position: 50% 50%;
}
```
We have two `inline-blocks` now, one with `30%` width and the other one with `70%` so they should go side by side right?
Wrong! they look like this:

![not inline](https://thepracticaldev.s3.amazonaws.com/i/sm09ceb8cs9uylbhlnlm.png)

So why is that happening? Believe it or not, there's a small space between both panes cause by our own markup. Basically `30% + small-space + 70%` is greater that `100%` and causes the last element (right pane) to go down. 
But where exactly is this space? Right here:

``` html
    <div class="left-pane"></div>
    <div class="right-pane"></div>
```

Maybe you don't see it but is there, let me remove it so you can see what I'm talking about:
``` html
     <div class="left-pane"></div><!-- this comment removes the space
  --><div class="right-pane"></div>
```

Now you see it, if you want you can just put together both panes in the same line but I find that this "comment" approach is more readable.

Now let's style the button that will change the layout. For that we need it to be positioned `absolute`ly so it can be on top of both panes, also I'm using `border-radius` to make it a circle, some rules to have white and centered text and finally `cursor: pointer` so it looks clickable when hovered:
``` css
.action-btn {
  position: absolute;
  width: 50px;
  height: 50px;
  left: calc(30% - 25px); /* width of first pane - half circle */
  top: 300px;
  border-radius: 50%;
  background: black;
  color: white;
  font-size: 40px;
  text-align: center;
  cursor: pointer;
} 
```

We're almost done on the CSS side, we just need to prepare for transitions. Our panes will be changing their width so we need an animation for that:
``` css
.right-pane, .left-pane {
  transition: width 0.7s;
}
```
And lastly when they open we will be adding a `.open` class that should change their width, for that we need these two rules:
``` css
.left-pane.open {
  width: 70%;
}
.right-pane.open {
  width: 30%;
}
```
It's looking better now.
![almost there](https://thepracticaldev.s3.amazonaws.com/i/ljpijy2gd8nkr2sbwrpx.png)


## The interactions

To start programming the behavior of our component we need to create the Vue instance. After that we will just declare a data property called `open` to keep track of the state of our panes, and also we declare a method `toggle` to change the `open` property:

``` js
new Vue({
  el: '#app',
  data: {
    open: false,
  },
  methods: {
    toggle () {
      this.open = !this.open
    }
  }
})
```

Pretty straight forward right? Now we need to bind it to our HTML for it to work. We will add a class binding on both panes so the `open` class gets added when the `open` property is true, and after that bind the button click to the toggle method like this:

``` html
<div id="app">
  <div class="container">
    <div class="left-pane" :class="{'open': open}"></div><!-- this comment removes the space
  --><div class="right-pane" :class="{'open': open}"></div>
  </div>
  <div @click="toggle" class="action-btn">+</div>
</div>
```

If everything went well we should have our panes animated when clicking the button.
![animating panes](https://thepracticaldev.s3.amazonaws.com/i/ocqje2aqp5v67sd5bhjt.gif)


Here comes the tricky part, our button needs to be animated too to match what we saw before. To be able to reference our button directly lets put a `ref` in the following way:

``` html
...
    <div @click="toggle" ref="actionbtn" class="action-btn">+</div>
...
```

With that we will be able to reference the `actionbtn` inside our JS. At this moment our button is clickable all the time, to avoid double clicks we need an `animating` data property that will help us to run our toggle function only when the button and panes are not animating.

``` js
//...
  data: {
    open: false,
    animating: false
  },
  methods: {
    toggle () {
      if(this.animating) {
        // avoid any action if button clicked when animated
        return
      }
      
      this.open = !this.open
      this.animateButton()
    }
  }
//...
```

We just added an animating button method call inside the toggle method, so let's create it. In it we will set `animating` to `true` and then make our button disappear using TweenMax:

``` js
//... methods
    animateButton () {
      this.animating = true

      // animate out
      const propsOut = {
        scale: 0, // make it disappear
        ease: Back.easeIn,
        onComplete: this.animateIn // this function will be called when animation finishes
      }
      TweenMax.to(this.$refs.actionbtn, 0.2, propsOut)
      
    },
```
On the line `ease: Back.easeIn` we declare what kind of easing animation we will use, for reference you can check all the available options in [GSAP ease visualizer](https://greensock.com/ease-visualizer). Also notice that we use `this.$refs.actionbtn` to reference our button and start animating it.

The `animateIn` function that we call after the disappearing animation ends is really similar, we will just need a computed property to know where our button should appear depending on the `open` state:

``` js
//... methods 
    animateIn () {
      //set new position
      TweenMax.set(this.$refs.actionbtn, this.actionBtnPosition)
      
      const propsIn = {
        delay: 0.4,
        scale: 1,
        ease: Back.easeOut,
      }
      TweenMax.to(this.$refs.actionbtn, 0.2, propsIn)
    },
  }
  computed: {
    actionBtnPosition () {
      const perc = this.open ? '70%' : '30%'
      const top = this.open ? '200px' : '300px'
      return {
        top: top,
        left: `calc(${perc} - 25px)` 
      }
    },
  }
//...
```

Animation is almost ready, we just need to make the plus sign change to a close symbol. In our reference gif, they manage to do that by rotating the button and we will do the same.
Just after the code that makes the button appear we need to add a tween that changes the rotation. Also when everything finishes we can set `animating` back to false so we can click the button again:

``` js
//... methods
////... animateIn
        const propsRotation = {
        delay: 0.4,
        rotation: this.actionBtnRotation,
        onComplete: this.endedAnimating
      }
      TweenMax.to(this.$refs.actionbtn, 0.3, propsRotation)
    },
    endedAnimating () {
      this.animating = false
    }
//... computed
    actionBtnRotation () {
      return this.open ? -135 : 0
    }
//...
```

And that's it, this is the final result!
{% codepen https://codepen.io/ederdiaz/pen/MGQgXG %}


If you're hungry for more you can check other WotW:
- [Wizard](https://dev.to/ederchrono/how-to-make-an-animated-wizard-component---wotw-51le)
- [Animated card slider](https://dev.to/ederchrono/animated-card-slider-with-vue--gsap---wotw-45bn)
- [Submit button with loader](https://dev.to/ederchrono/making-a-submit-button-with-loader---wotw-2ma)

Also if you want to see a specific widget for next week, post it in the comments section.