Cards

News Responsive

Zoom: 50% | 100% →

Top Stories

Responsive sizing, relative to the viewport.

Consectetur adipiscing
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
1 week ago
Nunc et ante
Vestibulum pretium mauris sit amet sodales sagittis.
1 week ago
Praesent auctor
Aliquam euismod nisi quis orci sodales, a tincidunt massa luctus.
1 week ago
Integer at nisl
Phasellus eget ex pulvinar, commodo sapien sed, finibus risus.
1 week ago
Aenean condimentum
Aliquam gravida tellus ut dui posuere suscipit.
1 week ago
Sollicitudin tincidunt
Morbi eget tellus vitae eros ultrices condimentum eget at libero.
1 week ago
Fermentum efficitur
Suspendisse quis ante molestie, dictum purus at, ultrices orci.
1 week ago
Ligula laoreet gravida
Aliquam tristique purus in odio blandit, id aliquet leo consectetur.
1 week ago
import=recipes/cards/recipes-cards-news.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h3>Top Stories</h3>
      <p>Responsive sizing, relative to the viewport.</p>
    </div>

    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.id">
        <div class="card">
          <div class="image" :style="{background: `url(${item.img.srcset.sm})`}"></div>
          <div class="content">
            <div>
              <div class="brand">
                <svg class="icon" viewBox="0 0 24 24">
                  <path
                    d="M19,5v14H5V5H19 M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3L19,3z"/>
                  <path d="M14,17H7v-2h7V17z M17,13H7v-2h10V13z M17,9H7V7h10V9z"/>
                </svg>
                <div class="name">{{ item.subtitle }}</div>
              </div>

              <div class="title">{{ item.description }}</div>
            </div>

            <div class="date">
              1 week ago
            </div>
          </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
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.card {
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid #e2e8f0;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.image {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  padding-top: 50%;
}

.content {
  padding: 12px 16px;
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.brand {
  display: flex;
  align-items: center;
  color: #333333;
}

.brand .icon {
  flex-shrink: 0;
  height: 20px;
  width: 20px;
  fill: currentColor;
}

.brand .name {
  margin-left: 4px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.5;
}

.title {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.6;
  margin-top: 8px;
  margin-bottom: 8px;
}

.date {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.5;
}
</style>

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

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: 6;
  }
}
</style>

<!--
## Responsive Logic
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.
Note that this responsive logic is hyper sensitive to your design choices, it's not a one size fit all solution.
var() has only 95% cross browser compatibility, you should convert it to fixed values.

There are 2 set of logic:
0-768 for peeking optimized for touch scrolling.
>768 for navigation via buttons for desktop/laptop users.
-->
<style scoped>
@media (max-width: 767.98px) {
  .item {
    width: calc((100% - (var(--margin) * 2) + var(--gap)) / var(--count));
    padding: 0 calc(var(--gap) / 2);
  }

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

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

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

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

  .horizontal >>> .v-hl-container {
    scroll-padding: 0 calc(var(--margin) - (var(--gap) / 2));
  }

  .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>

News Responsive + Fixed

Zoom: 50% | 100% →

Top Stories

Responsive sizing, relative to the viewport. Fixed once the viewport width gets too small.

Consectetur adipiscing
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
1 week ago
Nunc et ante
Vestibulum pretium mauris sit amet sodales sagittis.
1 week ago
Praesent auctor
Aliquam euismod nisi quis orci sodales, a tincidunt massa luctus.
1 week ago
Integer at nisl
Phasellus eget ex pulvinar, commodo sapien sed, finibus risus.
1 week ago
Aenean condimentum
Aliquam gravida tellus ut dui posuere suscipit.
1 week ago
Sollicitudin tincidunt
Morbi eget tellus vitae eros ultrices condimentum eget at libero.
1 week ago
Fermentum efficitur
Suspendisse quis ante molestie, dictum purus at, ultrices orci.
1 week ago
Ligula laoreet gravida
Aliquam tristique purus in odio blandit, id aliquet leo consectetur.
1 week ago
import=recipes/cards/recipes-cards-news-fixed.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h3>Top Stories</h3>
      <p>Responsive sizing, relative to the viewport. Fixed once the viewport width gets too small.</p>
    </div>

    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.id">
        <div class="card">
          <div class="image" :style="{background: `url(${item.img.srcset.sm})`}"></div>
          <div class="content">
            <div>
              <div class="brand">
                <svg class="icon" viewBox="0 0 24 24">
                  <path
                    d="M19,5v14H5V5H19 M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3L19,3z"/>
                  <path d="M14,17H7v-2h7V17z M17,13H7v-2h10V13z M17,9H7V7h10V9z"/>
                </svg>
                <div class="name">{{ item.subtitle }}</div>
              </div>

              <div class="title">{{ item.description }}</div>
            </div>

            <div class="date">
              1 week ago
            </div>
          </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
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.card {
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid #e2e8f0;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.image {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  padding-top: 50%;
}

.content {
  padding: 12px 16px;
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.brand {
  display: flex;
  align-items: center;
  color: #333333;
}

.brand .icon {
  flex-shrink: 0;
  height: 20px;
  width: 20px;
  fill: currentColor;
}

.brand .name {
  margin-left: 4px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.5;
}

.title {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.6;
  margin-top: 8px;
  margin-bottom: 8px;
}

.date {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.5;
}
</style>

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

main {
  padding: 24px;
}

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

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

@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;
  }
}

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

<!--
## Responsive Logic
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.
Note that this responsive logic is hyper sensitive to your design choices, it's not a one size fit all solution.
var() has only 95% cross browser compatibility, you should convert it to fixed values.

There are 2 set of logic:
0-768 for peeking optimized for touch scrolling.
>768 for navigation via buttons for desktop/laptop users.
-->
<style scoped>
@media (max-width: 767.98px) {
  .item {
    width: var(--fixed);
    padding: 0 calc(var(--gap) / 2);
  }

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

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

  .item:only-child {
    width: calc(var(--fixed) + var(--margin) * 2 - var(--gap));
  }

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

  .horizontal >>> .v-hl-container {
    scroll-padding: 0 calc(var(--margin) - (var(--gap) / 2));
  }

  .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>

Videos Aside

Zoom: 50% | 100% →

Article

Bla bla bla

import=recipes/cards/recipes-cards-videos-aside.vue padding=0 zoom
<template>
  <main>
    <div class="layout">
      <article>
        <h2>Article</h2>
        <p>Bla bla bla</p>
        <div class="placeholders">
          <div v-for="i in [0,1,2,3,4]" :key="i" class="placeholder">
            <placeholder-component></placeholder-component>
          </div>
        </div>
      </article>

      <aside>
        <div class="header">
          <h3>Top Videos</h3>
          <p>Responsive sizing, relative to the viewport on the side.
            Fixed once the viewport width gets too small.
          </p>
        </div>

        <vue-horizontal class="horizontal">
          <div class="item" v-for="item in items" :key="item.id">
            <div class="card">
              <div class="image" :style="{background: `url(${item.img.srcset.sm})`}">
                <div class="playback">
                  <svg viewBox="0 0 24 24" width="24" height="24">
                    <path
                      d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 13.5v-7c0-.41.47-.65.8-.4l4.67 3.5c.27.2.27.6 0 .8l-4.67 3.5c-.33.25-.8.01-.8-.4z"/>
                  </svg>
                </div>
              </div>
              <div class="content">
                <div>
                  <div class="title">{{ item.description }}</div>
                </div>

                <div class="date">
                  <b>ABC</b><br>
                  TubeYou • Jan 9 2019
                </div>
              </div>
            </div>
          </div>
        </vue-horizontal>
      </aside>
    </div>
  </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
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.card {
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid #e2e8f0;
  height: 100%;
  display: flex;
  flex-direction: column;
  cursor: pointer;
}

.image {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  padding-top: 50%;
  position: relative;
}

.image .playback {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.image svg {
  height: 48px;
  width: 48px;
  fill: currentColor;
  color: #ffffff99;
}

.card:hover svg {
  color: white;
}

.content {
  padding: 12px 16px;
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.title {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.6;
  margin-bottom: 8px;
}

.date {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.5;
}
</style>

<!-- Parent CSS (main, header, layout) -->
<style scoped>
.header {
  margin-bottom: 16px;
}

main {
  padding: 24px;
}

.placeholders {
  margin: -16px;
  padding: 16px 0;
}

.placeholder {
  padding: 16px;
  opacity: 0.25;
}

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

<!-- Responsive Breakpoints -->
<style scoped>
.layout {
  display: flex;
  flex-wrap: wrap;
  margin: -32px;
}

article, aside {
  flex-grow: 1;
  width: 100%;
  padding: 32px;
}

.horizontal {
  --fixed: 200px;
  --count: 1;
  --gap: 12px;
  --margin: 24px;
}

@media (min-width: 768px) {
  .layout {
    flex-wrap: nowrap;
  }

  .horizontal {
    --count: 2;
    --margin: 0;
    --gap: 16px;
  }

  aside {
    width: 40%;
  }
}

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

  aside {
    width: 40%;
  }
}

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

  aside {
    width: 50%;
  }
}
</style>

<!--
## Responsive Logic
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.
Note that this responsive logic is hyper sensitive to your design choices, it's not a one size fit all solution.
var() has only 95% cross browser compatibility, you should convert it to fixed values.

There are 2 set of logic:
0-768 for peeking optimized for touch scrolling.
>768 for navigation via buttons for desktop/laptop users.
-->
<style scoped>
@media (max-width: 767.98px) {
  .item {
    width: var(--fixed);
    padding: 0 calc(var(--gap) / 2);
  }

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

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

  .item:only-child {
    width: calc(var(--fixed) + var(--margin) * 2 - var(--gap));
  }

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

  .horizontal >>> .v-hl-container {
    scroll-padding: 0 calc(var(--margin) - (var(--gap) / 2));
  }

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

@media (min-width: 768px) {
  .item {
    width: var(--fixed);
    margin-right: var(--gap);
  }
}

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

Repository

Zoom: 50% | 100% →
vue-horizontal
An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)
Vue
999
99
vue-horizontal-list
An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)
Vue
999
99
vue-masonry-wall
A pure vue responsive masonry layout without direct dom manipulation and ssr support.
Vue
999
99
vue-horizontal.fuxing.dev
An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)
Vue
999
99
vue-horizontal-1
An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)
Vue
999
99
vue-horizontal-2
An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)
Vue
999
99
import=recipes/cards/recipes-cards-repository.vue padding=0 zoom
<template>
  <main>
    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.name">
        <div class="card">
          <div>
            <div class="title">
              <svg height="24" viewBox="0 0 24 24" width="24">
                <path
                  d="M14.17,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V9.83c0-0.53-0.21-1.04-0.59-1.41l-4.83-4.83 C15.21,3.21,14.7,3,14.17,3L14.17,3z M8,15h8c0.55,0,1,0.45,1,1v0c0,0.55-0.45,1-1,1H8c-0.55,0-1-0.45-1-1v0C7,15.45,7.45,15,8,15z M8,11h8c0.55,0,1,0.45,1,1v0c0,0.55-0.45,1-1,1H8c-0.55,0-1-0.45-1-1v0C7,11.45,7.45,11,8,11z M8,7h5c0.55,0,1,0.45,1,1v0 c0,0.55-0.45,1-1,1H8C7.45,9,7,8.55,7,8v0C7,7.45,7.45,7,8,7z"/>
              </svg>
              <div>{{ item.name }}</div>
            </div>
            <div class="description">
              {{ item.description }}
            </div>
          </div>
          <div class="stats">
            <div>
              <div class="circle"></div>
              <span>
                {{ item.lang }}
              </span>
            </div>
            <div>
              <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 20 20">
                <path
                  d="M13.0375,7.05882353 L11.2,1.36470588 C10.8375,0.247058824 9.1625,0.247058824 8.8125,1.36470588 L6.9625,7.05882353 L1.4,7.05882353 C0.1875,7.05882353 -0.3125,8.52941176 0.675,9.18823529 L5.225,12.2470588 L3.4375,17.6705882 C3.075,18.7647059 4.425,19.6470588 5.3875,18.9529412 L10,15.6588235 L14.6125,18.9647059 C15.575,19.6588235 16.925,18.7764706 16.5625,17.6823529 L14.775,12.2588235 L19.325,9.2 C20.3125,8.52941176 19.8125,7.07058824 18.6,7.07058824 L13.0375,7.07058824 L13.0375,7.05882353 Z"/>
              </svg>
              <span>{{ item.stars }}</span>
            </div>
            <div>
              <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 20 20">
                <path
                  d="M14.4444444,4 L14.4444444,10 C14.4444444,11.1 15.4444444,12 16.6666667,12 L17.7777778,12 L17.7777778,19 C17.7777778,19.55 18.2777778,20 18.8888889,20 C19.5,20 20,19.55 20,19 L20,1.13 C20,0.48 19.3222222,8.8817842e-16 18.6222222,0.15 C16.2222222,0.68 14.4444444,2.51 14.4444444,4 Z M8.88888889,7 L6.66666667,7 L6.66666667,1 C6.66666667,0.45 6.16666667,0 5.55555556,0 C4.94444444,0 4.44444444,0.45 4.44444444,1 L4.44444444,7 L2.22222222,7 L2.22222222,1 C2.22222222,0.45 1.72222222,0 1.11111111,0 C0.5,0 0,0.45 0,1 L0,7 C0,9.21 1.98888889,11 4.44444444,11 L4.44444444,19 C4.44444444,19.55 4.94444444,20 5.55555556,20 C6.16666667,20 6.66666667,19.55 6.66666667,19 L6.66666667,11 C9.12222222,11 11.1111111,9.21 11.1111111,7 L11.1111111,1 C11.1111111,0.45 10.6111111,0 10,0 C9.38888889,0 8.88888889,0.45 8.88888889,1 L8.88888889,7 Z"/>
              </svg>
              <span>{{ item.forks }}</span>
            </div>
          </div>
        </div>
      </div>
    </vue-horizontal>
  </main>
</template>

<script>
export default {
  data() {
    return {
      items: [
        {
          name: "vue-horizontal",
          stars: 999,
          forks: 99,
          lang: "Vue",
          description: "An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)"
        },
        {
          name: "vue-horizontal-list",
          stars: 999,
          forks: 99,
          lang: "Vue",
          description: "An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)"
        },
        {
          name: "vue-masonry-wall",
          stars: 999,
          forks: 99,
          lang: "Vue",
          description: "A pure vue responsive masonry layout without direct dom manipulation and ssr support."
        },
        {
          name: "vue-horizontal.fuxing.dev",
          stars: 999,
          forks: 99,
          lang: "Vue",
          description: "An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)"
        },
        {
          name: "vue-horizontal-1",
          stars: 999,
          forks: 99,
          lang: "Vue",
          description: "An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)"
        },
        {
          name: "vue-horizontal-2",
          stars: 999,
          forks: 99,
          lang: "Vue",
          description: "An ultra simple pure vue horizontal layout for modern responsive web with zero dependencies. (SPA/SSG/SSR)"
        },
      ]
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.card {
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid #e2e8f0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 16px;
  cursor: pointer;
  height: 100%;
}

svg {
  fill: currentColor;
}

.title {
  font-weight: 600;
  color: #444;
  font-size: 16px;
  line-height: 1.5;
  display: flex;
  margin-bottom: 8px;
}

.title svg {
  margin-right: 8px;
}

.description, .stats {
  font-weight: 500;
  color: #555;
  font-size: 14px;
  line-height: 1.5;
}

.stats {
  display: flex;
  align-items: center;
  margin: -4px;
  padding-top: 12px;
}

.stats > * {
  display: flex;
  align-items: center;
  height: 12px;
  margin: 4px;
  line-height: 1 !important;
  font-weight: 600;
}

.stats .circle {
  height: 10px;
  width: 10px;
  border-radius: 10px;
  background: black;
  margin-right: 4px;
}

.stats svg {
  margin-right: 4px;
}

.stats > *:first-child {
  margin-right: 8px;
}
</style>

<!-- Parent CSS (.container) -->
<style scoped>
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: 3;
  }
}

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

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

<!--
## Responsive Logic
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.
Note that this responsive logic is hyper sensitive to your design choices, it's not a one size fit all solution.
var() has only 95% cross browser compatibility, you should convert it to fixed values.

There are 2 set of logic:
0-768 for peeking optimized for touch scrolling.
>768 for navigation via buttons for desktop/laptop users.
-->
<style scoped>
@media (max-width: 767.98px) {
  .item {
    width: calc((100% - (var(--margin) * 2) + var(--gap)) / var(--count));
    padding: 0 calc(var(--gap) / 2);
  }

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

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

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

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

  .horizontal >>> .v-hl-container {
    scroll-padding: 0 calc(var(--margin) - (var(--gap) / 2));
  }

  .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>

Slanted

Zoom: 50% | 100% →

Top Stories (Slanted)

Responsive sizing, relative to the viewport. Fixed once the viewport width gets too small.

1 week ago
Consectetur adipiscing
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
1 week ago
Nunc et ante
Vestibulum pretium mauris sit amet sodales sagittis.
1 week ago
Praesent auctor
Aliquam euismod nisi quis orci sodales, a tincidunt massa luctus.
1 week ago
Integer at nisl
Phasellus eget ex pulvinar, commodo sapien sed, finibus risus.
1 week ago
Aenean condimentum
Aliquam gravida tellus ut dui posuere suscipit.
1 week ago
Sollicitudin tincidunt
Morbi eget tellus vitae eros ultrices condimentum eget at libero.
1 week ago
Fermentum efficitur
Suspendisse quis ante molestie, dictum purus at, ultrices orci.
1 week ago
Ligula laoreet gravida
Aliquam tristique purus in odio blandit, id aliquet leo consectetur.
import=recipes/cards/recipes-cards-slanted.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h3>Top Stories (Slanted)</h3>
      <p>Responsive sizing, relative to the viewport. Fixed once the viewport width gets too small.</p>
    </div>

    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.id">
        <div class="card">
          <div class="wrapper">
            <div class="image" :style="{background: `url(${item.img.srcset.sm})`}"></div>
            <div class="date">1 week ago</div>
          </div>
          <div class="content">
            <div>
              <div class="brand">
                <svg class="icon" viewBox="0 0 24 24">
                  <path
                    d="M19,5v14H5V5H19 M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3L19,3z"/>
                  <path d="M14,17H7v-2h7V17z M17,13H7v-2h10V13z M17,9H7V7h10V9z"/>
                </svg>
                <div class="name">{{ item.subtitle }}</div>
              </div>

              <div class="title">{{ item.description }}</div>
            </div>
          </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
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.card {
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid #e2e8f0;
  height: 100%;
  display: flex;
  flex-direction: column;
  cursor: pointer;
}

.wrapper {
  position: relative;
}

.image {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  padding-top: 60%;
  clip-path: polygon(100% 0, 100% 85%, 0% 100%, 0 0%);
  transition: 0.3s;
}

.card:hover .image {
  clip-path: polygon(100% 0, 100% 82%, 0% 98%, 0 0%);
}

.content {
  padding: 10px 16px 12px 16px;
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.brand {
  display: flex;
  align-items: center;
  color: #333333;
}

.brand .icon {
  flex-shrink: 0;
  height: 20px;
  width: 20px;
  fill: currentColor;
}

.brand .name {
  margin-left: 4px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.5;
}

.title {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.6;
  margin-top: 8px;
}

.date {
  font-size: 10px;
  font-weight: 700;
  color: black;
  line-height: 1;
  position: absolute;
  bottom: 0;
  right: 12px;
}
</style>

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

main {
  padding: 24px;
}

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

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

@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;
  }
}

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

<!--
## Responsive Logic
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.
Note that this responsive logic is hyper sensitive to your design choices, it's not a one size fit all solution.
var() has only 95% cross browser compatibility, you should convert it to fixed values.

There are 2 set of logic:
0-768 for peeking optimized for touch scrolling.
>768 for navigation via buttons for desktop/laptop users.
-->
<style scoped>
@media (max-width: 767.98px) {
  .item {
    width: var(--fixed);
    padding: 0 calc(var(--gap) / 2);
  }

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

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

  .item:only-child {
    width: calc(var(--fixed) + var(--margin) * 2 - var(--gap));
  }

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

  .horizontal >>> .v-hl-container {
    scroll-padding: 0 calc(var(--margin) - (var(--gap) / 2));
  }

  .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>

Dark Theme

Zoom: 50% | 100% →

Top Stories

Responsive sizing, relative to the viewport.

Consectetur adipiscing
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nunc et ante
Vestibulum pretium mauris sit amet sodales sagittis.
Praesent auctor
Aliquam euismod nisi quis orci sodales, a tincidunt massa luctus.
Integer at nisl
Phasellus eget ex pulvinar, commodo sapien sed, finibus risus.
Aenean condimentum
Aliquam gravida tellus ut dui posuere suscipit.
Sollicitudin tincidunt
Morbi eget tellus vitae eros ultrices condimentum eget at libero.
Fermentum efficitur
Suspendisse quis ante molestie, dictum purus at, ultrices orci.
Ligula laoreet gravida
Aliquam tristique purus in odio blandit, id aliquet leo consectetur.
import=recipes/cards/recipes-cards-dark.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h3>Top Stories</h3>
      <p>Responsive sizing, relative to the viewport.</p>
    </div>

    <vue-horizontal class="horizontal">
      <div class="item" v-for="item in items" :key="item.id">
        <div class="card">
          <div class="image" :style="{background: `url(${item.img.srcset.sm})`}"></div>
          <div class="content">
            <div class="name">{{ item.subtitle }}</div>
            <div class="title">{{ item.description }}</div>
          </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
    }
  }
}
</script>

<!-- Content Design -->
<style scoped>
.card {
  border-radius: 8px;
  overflow: hidden;
  height: 100%;
  position: relative;
}

.image {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  padding-top: 60%;
}

.content {
  padding: 6px 12px;
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: #000000BB;
  /* backdrop-filter drops FPS if it's in a modal due to fixed position repainting. */
  backdrop-filter: blur(3px);
  border-bottom-left-radius: 8px;
  border-bottom-right-radius: 8px;
}

.name {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.5;
}

.title {
  font-size: 12px;
  font-weight: 500;
  line-height: 18px;
  height: 18px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
</style>

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

main {
  padding: 24px;
  background: black;
}

main * {
  color: white !important;
}

@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: 6;
  }
}
</style>

<!--
## Responsive Logic
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.
Note that this responsive logic is hyper sensitive to your design choices, it's not a one size fit all solution.
var() has only 95% cross browser compatibility, you should convert it to fixed values.

There are 2 set of logic:
0-768 for peeking optimized for touch scrolling.
>768 for navigation via buttons for desktop/laptop users.
-->
<style scoped>
@media (max-width: 767.98px) {
  .item {
    width: calc((100% - (var(--margin) * 2) + var(--gap)) / var(--count));
    padding: 0 calc(var(--gap) / 2);
  }

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

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

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

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

  .horizontal >>> .v-hl-container {
    scroll-padding: 0 calc(var(--margin) - (var(--gap) / 2));
  }

  .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>