Cards
News Responsive
Zoom: 50% | 100% →
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% →
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% →
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% →
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% →
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% →
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>