Avatar

Avatar Small (Stories)

Zoom: 50% | 100% →

Stories

Stories on top of your feed!

import=recipes/avatar/recipes-avatar-small.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h1>Stories</h1>
      <p>Stories on top of your feed!</p>
    </div>

    <div class="feed">
      <div class="stories">
        <vue-horizontal class="horizontal" :button-between="false" snap="none">
          <div class="item" v-for="item in items" :key="item.id">
            <div class="ring">
              <div class="avatar" :style="{background: `url(${item.img})`}">
              </div>
            </div>
          </div>
        </vue-horizontal>
      </div>

      <div class="content">
        <div v-for="i in [0,1,2,3,4,5,6]" :key="i" class="feed-item">
          <placeholder-component></placeholder-component>
        </div>
      </div>
    </div>
  </main>
</template>

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

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

<!-- Content Design -->
<style scoped>
.item {
  padding: 14px 6px;
}

.item:first-child {
  padding-left: 21px;
}

.item:last-child {
  padding-right: 21px;
}

.ring {
  width: 64px;
  height: 64px;
  border-radius: 32px;

  border: 3px solid transparent;
  background: #e2e8f0;

  cursor: pointer;
  transition: All 0.3s ease;
  overflow: hidden;
}

.ring:hover {
  /* This is a very simple trick to animation a ring of colors, you should go find a library for this*/
  transform: rotate(9deg) scale(1.05) translate(1px);
  animation: colors 1s ease infinite;
  background-size: 200% 200%;
  background-color: #663dff;
  border: 4px solid transparent;
  animation-direction: alternate;
  background-image: linear-gradient(319deg, #7d5fee 0%, #b72bff 33%, #ff2eb0 66%, #7eee40 100%);
}

@keyframes colors {
  0% {
    background-position: 10% 0
  }
  100% {
    background-position: 91% 100%
  }
}

.avatar {
  background-position: center !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  height: 100%;
  width: 100%;
}

/* You can add your own button or you could just, */
/* Override default button design to make it smaller. */
.horizontal >>> .v-hl-btn svg {
  padding: 2px;
  height: 24px;
  width: 24px;
}
</style>

<!-- Parent CSS (.container) and other stuff -->
<style scoped>
main {
  padding: 24px;
  max-width: 700px;
  margin-left: auto;
  margin-right: auto;
}

.stories {
  border-bottom: 1px solid #e2e8f0;
}

.feed {
  border-radius: 5px;
  border: 1px solid #e2e8f0;
}

.content {
  padding: 12px;
  display: flex;
  flex-wrap: wrap;
  opacity: 0.25;
}

.feed-item {
  padding: 12px;
  width: 50%;
}

.header {
  margin-bottom: 24px;
}

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

Responsive Avatar

Zoom: 50% | 100% →

Large Avatar

Images of people in a responsive grid

Jessica Felicio

Janko Ferlič

Alexander Krivitskiy

Shane Devlin

Amir Seilsepour

Seth Doyle

Qasim Sadiq

Julian Wan

Shayan Rostami

Ahmadreza Najafi

import=recipes/avatar/recipes-avatar-large.vue padding=0 zoom
<template>
  <main>
    <div class="header">
      <h1>Large Avatar</h1>
      <p>Images of people in a responsive grid</p>
    </div>

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

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

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

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

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

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

.content > * {
  color: white;
  line-height: 1.25;
}
</style>

<!-- Parent CSS (.container) -->
<style scoped>
main {
  padding: 24px;
}

.header {
  margin-bottom: 24px;
}

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

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

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

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

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

@media (min-width: 1280px) {
  .horizontal {
    --gap: 32px;
    --count: 8;
  }
}
</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>