Banner

Getting Started

Keep a look out on this space. Constantly adding more recipes over the next few months. Give this repo a star, it will help out greatly!
  • Vue Horizontal is required, and you need to install it .
  • Recipes follow the design language defined in the design principle .
  • You should be able to extract the code snippet and use it as a single file component.
  • If you are consistently recycling a design pattern, you should abstract your SFC.
  • It might look different on your website due various parent CSS rules. Tailwind is used under the hood with normalizing, that might be different from your default settings.

Recipes are designed:

  • For the responsive web, using default breakpoints .
  • For mobile first design, peeking navigation will be used on the mobile. Assumption is made that you have a padding of 24px on the left and right on the mobile viewport. The 24px is then removed and added into the <vue-horizontal> as scroll padding.
  • For broad usage pattern, you should take it and edit it your needs.

Why is it so complex? Why is it not shipped together with vue-horizontal?

  • You control how to structure your content with HTML
  • You control how it looks with CSS
  • To give you greater control of your website and thus your code, vue-horizontal is merely a small component to horizontally align your content while fixing all the nasty quirks related to horizontal control (nav/scroll/touch). It also contains a few methods and event emitter that are optimized to make your life easier. It doesn't dictate how you structure your HTML or style your CSS. This responsive design logic is merely a skeleton of logic that is merely useful in that context. And every recipe may requires a different set of logic.

Responsive Banner 1 to 3

Zoom: 50% | 100% →

Your Banner Header

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Marina Bay Sands

Gardens by the Bay

Fireworks

Cloud Forest

Jewel

Chinatown

Urban

Town

import=recipes/banner/recipes-banner-1-3.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h1>Your Banner Header</h1>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
    </div>

    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.id">
        <div class="content" :style="{background: `url(${item.img})`}">
          <div class="aspect-ratio"></div>
          <div class="overlay">
            <h2>{{ item.title }}</h2>
          </div>
        </div>
      </div>
    </vue-horizontal>
  </main>
</template>

<script>
// For convenience sake, I import a collection of images from unsplash.
import {singapore} from '../../../../assets/img'

export default {
  data() {
    return {
      items: singapore.items.map(({id, title, img: {srcset: {sm}}}) => {
        return {
          id: id,
          title: title,
          img: sm
        };
      })
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.content {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  position: relative;
  border-radius: 5px;
  overflow: hidden;
}

.aspect-ratio {
  padding-top: 60%;
}

.overlay {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: #00000010;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 24px;
}

.overlay > * {
  color: white;
}
</style>

<!-- Parent CSS (Container) -->
<style scoped>
.header {
  margin-bottom: 24px;
}

main {
  padding: 24px;
}

@media (min-width: 768px) {
  main {
    padding: 48px;
  }
}
</style>

<!-- Responsive Breakpoints -->
<style scoped>
.horizontal {
  --count: 1;
  --gap: 16px;
  --margin: 24px;
}

@media (min-width: 640px) {
  .horizontal {
    --count: 2;
  }
}

@media (min-width: 768px) {
  .horizontal {
    --count: 2;
    --margin: 0;
  }
}

@media (min-width: 1024px) {
  .horizontal {
    --count: 3;
  }
}

@media (min-width: 1280px) {
  .horizontal {
    --gap: 24px;
  }
}
</style>

<!-- Responsive Logic -->
<style scoped>
@media (max-width: 767.98px) {
  /* The --margin removes the padding from the parent container and add it into vue-horizontal.
   If the gap is less than margin, this causes overflow to show and peeks into the next content for better UX.
   You can replace this section entirely for basic responsive CSS logic if you don't want this "peeking" experience
   for the mobile web. */
  .item {
    width: calc((100% - (var(--gap) * 2)) / var(--count));
    padding: 0 calc(var(--gap) / 2);
  }

  .item:first-child {
    width: calc(var(--gap) + (100% - (var(--gap) * 2)) / var(--count));
    padding-left: var(--margin);
  }

  .item:last-child {
    width: calc(var(--gap) + (100% - (var(--gap) * 2)) / var(--count));
    padding-right: var(--margin);
  }

  .item:only-child {
    width: calc((var(--gap) * 2) + (100% - (var(--gap) * 2)) / var(--count));
  }

  .horizontal {
    margin: 0 calc(var(--margin) * -1);
  }

  .horizontal >>> .v-hl-container {
    scroll-padding-left: var(--gap);
    scroll-padding-right: var(--gap);
  }

  .horizontal >>> .v-hl-btn {
    display: none;
  }
}

@media (min-width: 768px) {
  .item {
    width: calc((100% - ((var(--count) - 1) * var(--gap))) / var(--count));
    margin-right: var(--gap);
  }
}
</style>

Responsive Banner 1 to 5

Zoom: 50% | 100% →

Your Banner Header

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Marina Bay Sands

Gardens by the Bay

Fireworks

Cloud Forest

Jewel

Chinatown

Urban

Town

import=recipes/banner/recipes-banner-1-5.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h1>Your Banner Header</h1>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
    </div>

    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.id">
        <div class="content" :style="{background: `url(${item.img})`}">
          <div class="aspect-ratio"></div>
          <div class="overlay">
            <h2>{{ item.title }}</h2>
          </div>
        </div>
      </div>
    </vue-horizontal>
  </main>
</template>

<script>
// For convenience sake, I import a collection of images from unsplash.
import {singapore} from '../../../../assets/img'

export default {
  data() {
    return {
      items: singapore.items.map(({id, title, img: {srcset: {sm}}}) => {
        return {
          id: id,
          title: title,
          img: sm
        };
      })
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.content {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  position: relative;
  border-radius: 5px;
  overflow: hidden;
}

.aspect-ratio {
  padding-top: 60%;
}

.overlay {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: #00000010;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 24px;
}

.overlay > * {
  color: white;
}
</style>

<!-- Parent CSS (Container) -->
<style scoped>
.header {
  margin-bottom: 24px;
}

main {
  padding: 24px;
}

@media (min-width: 768px) {
  main {
    padding: 48px;
  }
}
</style>

<!-- Responsive Breakpoints -->
<style scoped>
.horizontal {
  --count: 1;
  --gap: 16px;
  --margin: 24px;
}

@media (min-width: 640px) {
  .horizontal {
    --count: 2;
  }
}

@media (min-width: 768px) {
  .horizontal {
    --count: 3;
    --margin: 0;
  }
}

@media (min-width: 1024px) {
  .horizontal {
    --count: 4;
  }
}

@media (min-width: 1280px) {
  .horizontal {
    --gap: 24px;
    --count: 5;
  }
}
</style>

<!-- Responsive Logic -->
<style scoped>
@media (max-width: 767.98px) {
  /* The --margin removes the padding from the parent container and add it into vue-horizontal.
   If the gap is less than margin, this causes overflow to show and peeks into the next content for better UX.
   You can replace this section entirely for basic responsive CSS logic if you don't want this "peeking" experience
   for the mobile web. */
  .item {
    width: calc((100% - (var(--gap) * 2)) / var(--count));
    padding: 0 calc(var(--gap) / 2);
  }

  .item:first-child {
    width: calc(var(--gap) + (100% - (var(--gap) * 2)) / var(--count));
    padding-left: var(--margin);
  }

  .item:last-child {
    width: calc(var(--gap) + (100% - (var(--gap) * 2)) / var(--count));
    padding-right: var(--margin);
  }

  .item:only-child {
    width: calc((var(--gap) * 2) + (100% - (var(--gap) * 2)) / var(--count));
  }

  .horizontal {
    margin: 0 calc(var(--margin) * -1);
  }

  .horizontal >>> .v-hl-container {
    scroll-padding-left: var(--gap);
    scroll-padding-right: var(--gap);
  }

  .horizontal >>> .v-hl-btn {
    display: none;
  }
}

@media (min-width: 768px) {
  .item {
    width: calc((100% - ((var(--count) - 1) * var(--gap))) / var(--count));
    margin-right: var(--gap);
  }
}
</style>

Responsive Banner 2 to 8

Zoom: 50% | 100% →

Your Banner Header

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Marina Bay Sands

Gardens by the Bay

Fireworks

Cloud Forest

Jewel

Chinatown

Urban

Town

import=recipes/banner/recipes-banner-2-8.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h2>Your Banner Header</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
    </div>

    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.id">
        <div class="content" :style="{background: `url(${item.img})`}">
          <div class="aspect-ratio"></div>
          <div class="overlay">
            <h4>{{ item.title }}</h4>
          </div>
        </div>
      </div>
    </vue-horizontal>
  </main>
</template>

<script>
// For convenience sake, I import a collection of images from unsplash.
import {singapore} from '../../../../assets/img'

export default {
  data() {
    return {
      items: singapore.items.map(({id, title, img: {srcset: {sm}}}) => {
        return {
          id: id,
          title: title,
          img: sm
        };
      })
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.content {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  position: relative;
  border-radius: 5px;
  overflow: hidden;
}

.aspect-ratio {
  padding-top: 50%;
}

.overlay {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: #00000010;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 24px;
}

.overlay > * {
  color: white;
  line-height: 1.3;
}
</style>

<!-- Parent CSS (Container) -->
<style scoped>
.header {
  margin-bottom: 24px;
}

main {
  padding: 24px;
}

@media (min-width: 768px) {
  main {
    padding: 48px;
  }
}
</style>

<!-- Responsive Breakpoints -->
<style scoped>
.horizontal {
  --count: 2;
  --gap: 16px;
  --margin: 24px;
}

@media (min-width: 640px) {
  .horizontal {
    --count: 3;
  }
}

@media (min-width: 768px) {
  .horizontal {
    --count: 4;
    --margin: 0;
  }
}

@media (min-width: 1024px) {
  .horizontal {
    --count: 6;
  }
}

@media (min-width: 1280px) {
  .horizontal {
    --count: 8;
  }
}
</style>

<!-- Responsive Logic -->
<style scoped>
@media (max-width: 767.98px) {
  /* The --margin removes the padding from the parent container and add it into vue-horizontal.
   If the gap is less than margin, this causes overflow to show and peeks into the next content for better UX.
   You can replace this section entirely for basic responsive CSS logic if you don't want this "peeking" experience
   for the mobile web. */
  .item {
    width: calc((100% - (var(--gap) * 2)) / var(--count));
    padding: 0 calc(var(--gap) / 2);
  }

  .item:first-child {
    width: calc(var(--gap) + (100% - (var(--gap) * 2)) / var(--count));
    padding-left: var(--margin);
  }

  .item:last-child {
    width: calc(var(--gap) + (100% - (var(--gap) * 2)) / var(--count));
    padding-right: var(--margin);
  }

  .item:only-child {
    width: calc((var(--gap) * 2) + (100% - (var(--gap) * 2)) / var(--count));
  }

  .horizontal {
    margin: 0 calc(var(--margin) * -1);
  }

  .horizontal >>> .v-hl-container {
    scroll-padding-left: var(--gap);
    scroll-padding-right: var(--gap);
  }

  .horizontal >>> .v-hl-btn {
    display: none;
  }
}

@media (min-width: 768px) {
  .item {
    width: calc((100% - ((var(--count) - 1) * var(--gap))) / var(--count));
    margin-right: var(--gap);
  }
}
</style>