DEV Community

Cover image for [Dribbble Challenge] — Coffee Ordering Animation
Roman Antonov
Roman Antonov

Posted on

[Dribbble Challenge] — Coffee Ordering Animation

Tutorial level: Beginner/Junior

Motivation

Sometimes, when we surf dribbble, uplabs and similar design clouds, we often find many concepts or prototypes with animations, micro interactions, application flow and so on.
I often find illustrations of mobile apps that are good and interesting, but of course they are still in the form of concept, so therefore, why don't we try to apply them as an interface for applications that we will build next.

Original concept

In the Dribbble Challenge we will try to build an interface for Coffee Ordering, as I found on Dribble.

Alt Text

The flow is quite simple:

  • User will choose the size of glass
  • User will place the order to basket
  • User redirected to the checkout page

Technologies

We will use quite simple technologies stack: HTML + CSS + JavaScript.
Final result can be fit in just one html file.
Of course, you can use SCSS, TypeScript, React, Angular and other tools, but the target of tutorial just a simplest interface demonstration.

Packages

We also will using 2 additional packages:

Let's Build

Firstly, create a simple index.html file in any new folder.
Open file and write default required tags

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Coffee Ordering</title>

   <style>
     <!-- Styles will be placed here -->
   </style>
  </head>
  <body>

    <script>
     <!-- Scripts will be placed here -->
    </script>
  </body>
</html>

I hope you already familiar with html tags and attributes above. If so, continue next, otherwise, take a quick look of html guidelines

Libraries installation

In this step we inject some libraries to our page. Add some lines to your <head>

<head>
    <meta charset="UTF-8">
    <title>Coffee Ordering</title>

    <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <script>var exports = {"__esModule": true};</script>
    <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
    <script src="https://unpkg.com/cupertino-pane/dist/cupertino-pane.min.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css"/> 

   <style>
     <!-- Styles will be placed here -->
   </style>
</head>

Note, that we are using all libraries from CDN and then, keep files locally isn't required.

Tag <meta name="viewport"> gives the browser instructions on how to control the page's dimensions and scaling.

And declaration of exports var exports = {"__esModule": true}; will resolve some libraries/environments variable scope issues.

With these all libraries are installed and we can going to developing.

First page state DOM elements

Let's add some new elements into our <body> tag.

<body>
  <ion-app>
  <ion-content scroll-y="false">
  <div class="content">
    <ion-header translucent="true">
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-button>
            <ion-icon name="chevron-back-outline"></ion-icon>
            Frappuccino
          </ion-button>
        </ion-buttons>
        <ion-buttons slot="end">
          <ion-button>
            <ion-icon name="heart-outline"></ion-icon>
          </ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>
    <div class="content-body">
      <img src="https://raw.githubusercontent.com/roman-rr/cupertino-pane/master/playground/img/starbucks.png" />
      <h1>Mocha Frappuccino®</h1>
      <p>
        Buttery caramel syrup meets coffee, milk and ice for a rendezvous in the blender.
      </p>
      <div class="line">
        <div class="price">
          £ 
          <div class="big">3</div>
          .45
        </div>
        <div class="sizes">
          <div class="active-frame"></div>
          <div class="size active" onclick="setActive(this, 3, 0, 'S')">
            S
            <ion-icon name="cafe-outline"></ion-icon>
          </div>
          <div class="size" onclick="setActive(this, 5, 1, 'M')">
            M
            <ion-icon name="cafe-outline"></ion-icon>
          </div>
          <div class="size" onclick="setActive(this, 7, 2, 'L')">
            L
            <ion-icon name="cafe-outline"></ion-icon>
          </div>
        </div>
      </div>
      <ion-button id="button-add"
        expand="block" 
        onclick="presentPane();">
        <span class="button-text">Add to Bag</span>
        <ion-icon name="checkmark-outline"></ion-icon>
      </ion-button>
      <div class="draggable">
        <div class="move"></div>
      </div>
    </div>
  </div>
  <ion-content>
  <ion-app>
</body>

All images we will using from CDN also. So, no any more local files needed and tests should be simple.

First page state styles

Add some styles into your <head>.
Styles will describe product information and size picker style.

ion-toolbar {
  --background: #ffffff;
  --border-color: #ffffff;
}

ion-content {
  --background: rgb(0, 112, 74);
}

.content {
  background: #ffffff;
  height: 100%;
  border-radius: 0 0 30px 30px;
  border-width: 1px;
  border: 1px solid #ffffff;
}

ion-toolbar ion-button {
  --color: #292929;
}

.content-body {
  padding-left: 20px;
  padding-right: 20px;
}

.content-body h1 {
  margin-top: 30px;
}

.content-body p {
  color: #828282;
  font-size: 14px;
  line-height: 20px;
}

.content-body img {
  display: block;
  max-width: 100%;
  margin: auto;
  margin-top: 10px;
}

.content-body ion-button {
  margin-left: 0;
  margin-right: 0;
  --border-radius: 30px;
  font-weight: 600;
  --background: rgb(0, 112, 74);
  margin-top: 15px;
}

.content-body ion-button:active {
  --background: rgb(39, 92, 65);
}

.content-body .price {
  display: flex;
  align-items: center;
  font-size: 26px;
  font-weight: 600;
  height: 60px;
  margin-left: 5px;
}

.content-body .price .big {
  margin-left: 5px;
  font-size: 50px;
}

.content-body .line {
  height: 60px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-direction: row;
}

.content-body .sizes {
   display: flex;
}

.content-body .sizes .size {
  font-size: 11px;
  font-weight: 700;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 48px;
  height: 48px;
  border: 1px solid #DADADA;
  border-radius: 3px;
  margin-right: 7px;
  color: #DADADA;
  background: rgb(248, 248, 248);
  padding-bottom: 3px;
  transition: all 200ms ease-in-out;
  position: relative;
}    

.content-body .sizes .size.active {
  font-size: 11px;
  background: rgb(232, 240, 236);
  color: rgb(48, 111, 78);
}

.content-body .sizes .active-frame {
  transform: translate3d(0px, 0px, 0px);
  transition: all 200ms ease-in-out;
  border-radius: 3px;
  width: 48px;
  height: 48px;
  position: absolute;
  border: 2px solid rgb(48, 111, 78);
  z-index: 2;
}

.content-body .sizes .size ion-icon {
  position: absolute;
  font-size: 37px;
  margin-top: 6px;
  top: 0;
  left: 2px;
  right: 0;
  margin-left: auto;
  margin-right: auto;
  z-index: 1;
}

.content-body .draggable {
  padding: 15px; 
  position: absolute; 
  bottom: 0px; 
  left: 0px; 
  right: 0px; 
  margin-left: auto; 
  margin-right: auto; 
  height: 30px;
}

.content-body .draggable .move {
  margin: 0px auto; 
  height: 5px; 
  background: rgba(202, 202, 202, 0.6); 
  width: 50px; 
  border-radius: 4px; 
  backdrop-filter: saturate(180%) blur(20px);
}

Open index.html file in browser and check what we got:

Alt Text

Size picker

On this step, first interface statement should be prepared well. Styles are applied and we can make first interaction works — picking a drink size.
Time to add some scripts into our <script> tag.

<script>
  function setActive(e, n, kfc, s) {
    itemprice = n;
    size = s;
    let frame = document.querySelector('.active-frame ');
    frame.style.transform = `translate3d(${55 * kfc}px, 0px, 0px)`;

    let elems = document.getElementsByClassName('size');
    for (var i = 0; i < elems.length; i++) {
      elems[i].classList.remove('active');
    }
    e.classList.add('active');
    document.getElementsByClassName('big')[0].innerHTML = itemprice;
  }
</script>

Now you can pick any drink size, frame will be moved according with css tranform/transition options, and price is also will be changed dynamically.

Add to Bag

We need to handle "Add to Bag" button and Pane opening.

Important part to understand is how we imitate pane behavior on our first state content. True moving pane will be appears from bottom, but our content is just "follower" of bottom pane transitions. To imitate this behavior we are intentionally rounded bottom corners of our content and unrounded pane corners.

Prepare DOM elements for bottom pane

<ion-content>
...
<ion-drawer>
  <!-- First step -->
  <div class="first-step">
    <div class="drinks">
      <div class="drink">
        <img src="https://raw.githubusercontent.com/roman-rr/cupertino-pane/master/playground/img/cup-1.png" />
        <div class="size-drink">M</div>
        <div class="bg"></div>
      </div>
      <div class="drink">
        <img src="https://raw.githubusercontent.com/roman-rr/cupertino-pane/master/playground/img/cup-2.png" />
        <div class="size-drink">L</div>
        <div class="bg"></div>
      </div>
    </div>
    <div class="price">
      £ 
      <div class="big">3</div>
      .45
    </div>
  </div>
  <!-- My Bag -->
  <div class="my-bag">
    <h2>My Bag</h2>
    <div class="list">
      <!-- Item 1 -->
      <div class="item">
        <div class="left-side">
          <div class="drink">
            <img src="https://raw.githubusercontent.com/roman-rr/cupertino-pane/master/playground/img/cup-1.png" />
            <div class="bg"></div>
          </div>
          <div class="desc">
            <div class="name">Caramel Frappuccino®</div>
            <div class="size">Size M</div>
            <div class="price">£ 4.85</div>
          </div>
        </div>
        <div class="amount">x 1</div>
      </div>
      <!-- Item 2 -->
      <div class="item">
        <div class="left-side">
          <div class="drink">
            <img src="https://raw.githubusercontent.com/roman-rr/cupertino-pane/master/playground/img/cup-2.png" />
            <div class="bg"></div>
          </div>
          <div class="desc">
            <div class="name">Mocha Frappuccino®</div>
            <div class="size">Size L</div>
            <div class="price">£ 3.70</div>
          </div>
        </div>
        <div class="amount">x 1</div>
      </div>
    </div>
    <div class="footer">
      <div class="line">
        <div class="text">
          Total
        </div>
        <div class="amount">
          £ <span id="total-amount"></span>.70
        </div>
      </div>
      <ion-button expand="block">
        Confirm Order
      </ion-button>
    </div>
  </div>
</ion-drawer>
</ion-content>

Apply a new styles for bottom pane

.pane ion-drawer {
  background: rgb(0, 112, 74) !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}

ion-drawer .first-step {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-left: 20px;
  margin-right: 20px;
  transition: all 150ms ease-in-out;
  opacity: 1;
}

ion-drawer .first-step .price {
  display: flex;
  align-items: center;
  font-size: 26px;
  font-weight: 600;
  color: #ffffff;
}

ion-drawer .first-step .drinks {
  display: flex;
  justify-content: center;
  align-items: center;
}

.first-step .drinks .drink {
  width: 48px;
  height: 48px;
  border-radius: 3px;
  margin-right: 7px;
  position: relative;
}

.first-step .drinks .bg {
  position: absolute;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgb(30, 74, 52);
  bottom: 0;
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
}

.first-step .drinks .size-drink {
  position: absolute;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #ffffff;
  font-weight: 700;
  right: -3px;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
}

.first-step .drinks img {
  display: block;
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  margin-left: auto;
  margin-right: auto;
  bottom: 6px;
}

My Bag state styles

These styles also should be added in your <styles> block which will make My Bag container looks in order.

ion-drawer .my-bag {
  margin-left: 20px;
  margin-right: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  opacity: 0;
  transition: all 150ms ease-in-out;
}

ion-drawer .my-bag h2 {
  font-weight: 800;
  color: #ffffff;
  margin-top: -60px;
  font-size: 28px;
  will-change: transform, opacity;
  transform: translate3d(0px, 60px, 0px);
  transition: all 150ms ease-in-out;
}

ion-drawer .my-bag .list {
  width: 100%;
  will-change: transform, opacity;
  transform: translate3d(0px, 60px, 0px);
  transition: all 150ms ease-in-out;
}

ion-drawer .my-bag .item {
  display: flex;
  justify-content: space-between;
  margin-top: 25px;
}

ion-drawer .my-bag .left-side {
  display: flex;
  align-items: center;
}

ion-drawer .my-bag .drink {
  width: 48px;
  height: 48px;
  border-radius: 3px;
  margin-right: 20px;
  position: relative;
  transform: scale(1.2);
}

.my-bag .drink .bg {
  position: absolute;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgb(30, 74, 52);
  bottom: 0;
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
}

.my-bag .drink img {
  display: block;
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  margin-left: auto;
  margin-right: auto;
  bottom: 6px;
}

.my-bag .item .amount {
  font-size: 22px;
  font-weight: 700;
  color: #ffffff;
  display: flex;
  align-items: center;
}

.my-bag .item .desc .name {
  color: #fff;
  font-weight: 600;
  font-size: 17px;
}

.my-bag .item .desc .size {
  color: #fff;
  font-size: 14px;
  margin-top: 2px;
}

.my-bag .item .desc .price {
  color: #88afa2;
  font-size: 16px;
  margin-top: 10px;
}

.my-bag .footer {
  border-top: 1px solid #ffffff2b;
  position: absolute;
  width: calc(100% - 40px);
  bottom: 0;
  padding-bottom: 35px;
  background: #00704a;
}

.my-bag .footer .line {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 20px;
  margin-bottom: 20px;
}

.my-bag .footer .line .text,
.my-bag .footer .line .amount {
  font-weight: 700;
  color: #ffffff;
  font-size: 26px;
}

.my-bag .footer ion-button {
  --border-radius: 30px;
  font-weight: 700;
  --background: #fff;
  color: #00704a;
  font-size: 17px;
  letter-spacing: 0.1px;
}

.my-bag .footer ion-button:active {
  --background: #effffa;
}

Finalize scripts

And Finalize JavaScript part which will execute Cupertino Pane library, present pane, handle add to Bag Button, some transitions and Pane behavior.

<script>
const translateYRegex = /\.*translateY\((.*)px\)/i;
let paneY;
let paneEl;
let totalprice = 0;
let itemprice = 3;
let size = 'S';
const contentEl = document.querySelector('.content');
const firstStep = document.querySelector('.first-step');
const myBag = document.querySelector('.my-bag');
const myBagH2 = document.querySelector('.my-bag h2');
const myBagList = document.querySelector('.my-bag .list');

const firstHeight = 120;
firstStep.style.height = `${firstHeight - 30}px`;
contentEl.style.marginTop = `-${firstHeight + firstHeight/2}px`;
contentEl.style.paddingTop = `${firstHeight/2}px`;
contentEl.style.transform = `translateY(${firstHeight}px) translateZ(0px)`;
contentEl.style.height = `calc(100% + ${firstHeight/2}px + 30px)`;

function checkTransformations() {
  paneEl = document.querySelector('.pane');
  if (!paneEl) return;
  paneY = parseFloat(translateYRegex.exec(paneEl.style.transform)[1]);

  if (window.innerHeight - paneY - 30 > firstHeight) {
    myBagH2.style.transform = 'translate3d(0px, 0px, 0px)';
    myBagList.style.transform = 'translate3d(0px, 0px, 0px)';
    myBag.style.opacity = 1;
    firstStep.style.opacity = 0;
  } else {
    myBagH2.style.transform = 'translate3d(0px, 60px, 0px)';
    myBagList.style.transform = 'translate3d(0px, 60px, 0px)';
    myBag.style.opacity = 0;
    firstStep.style.opacity = 1;
  }
}

let drawer = new CupertinoPane('ion-drawer', {
  followerElement: '.content',
  breaks: {
    middle: {
      enabled: true,
      height: firstHeight
    },
    bottom: {
      enabled: true,
      height: 20
    }
  },
  buttonClose: false,
  showDraggable: false,
  bottomClose: true,
  draggableOver: true,
  lowerThanBottom: false,
  dragBy: ['.cupertino-pane-wrapper .pane', '.content'],
  onDrag: () => checkTransformations(),
  onTransitionEnd: () => checkTransformations()
});

function presentPane(e) {
  drawer.present({
    animate: true
  });

  // Total price
  totalprice += itemprice;
  document.getElementsByClassName('big')[1].innerHTML = totalprice;
  document.getElementById('total-amount').innerHTML = totalprice;
  document.getElementsByClassName('size-drink')[1].innerHTML = size;

  // Button animation
  let icon = document.querySelector('#button-add ion-icon');
  let text = document.querySelector('#button-add .button-text');
  text.style.opacity = 0;

  setTimeout(() => {
    icon.style.opacity = 1;
    text.innerHTML = 'Add 1 more'
  }, 200);

  setTimeout(() => {
    icon.style.opacity = 0;
  }, 1000);

  setTimeout(() => {
    text.style.opacity = 1;
  }, 1300);
}

function setActive(e, n, kfc, s) {
  itemprice = n;
  size = s;
  let frame = document.querySelector('.active-frame ');
  frame.style.transform = `translate3d(${55 * kfc}px, 0px, 0px)`;

  let elems = document.getElementsByClassName('size');
  for (var i = 0; i < elems.length; i++) {
    elems[i].classList.remove('active');
  }
  e.classList.add('active');
  document.getElementsByClassName('big')[0].innerHTML = itemprice;
}
</script>

Conclusions

starbucks
Live demo results
Code sources results

Thanks

Dribble project
Android version

Top comments (2)

Collapse
 
robole profile image
Rob OLeary

Hi roman, I could not interact with the live code example on a desktop browser, you cannot scroll down. You need to allow scrolling on the y-axis, this is disabled currently. Changing the attribute scroll-y did the trick.

 <ion-app>
    <ion-content scroll-y="true">

It'd be nice to see the contents of it all in one screen. Atm you can only see the coffee image fill the entire screen..

Collapse
 
romanrr profile image
Roman Antonov

Thanks for the trick. Mainly this tutorial is for Mobile development, and i disable scroll consciously to prevent some errors and make flow smooth in demonstration.