1. Media

Realistic 3D Photo Card Gallery

Cool 3d photo gallery developed using HTML (Pug), CSS (Scss), JS (Babel) and Vue.Js. To see the effect, mouse hover any image and see the 3d effects when pointer is zig zagged.

Developed by Jouan Marcel and 3D photo gallery in a semi skeuomorphism style with realistic lighting reflection on mouse / cursor hover. Created with Vue.Js as web component for portability.

Find the demo below

See the Pen Realistic 3D Photo Cards (Hover Effect, Vue.Js) by Jouan Marcel (@jouanmarcel) on CodePen.

Find the code below

HTML
.grid#grid
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/37f408a5a9bfcb4b0e2de07499a560c9/5E632BE6/t51.2885-15/e35/c0.135.1080.1080a/s480x480/40552940_106792203550584_1954235443271646170_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=100" link="https://www.instagram.com/p/BnrVNA_F95p/")
  
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/398cfc6bf836e3e227ee922936836bbe/5E4D6E72/t51.2885-15/e35/s480x480/36955358_287866911973922_429811318774562816_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=102" link="https://www.instagram.com/p/BloI_FblUVy/")
  
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/889f95936f24056538c7168915d639c3/5E51E9AC/t51.2885-15/e35/c135.0.809.809a/s480x480/41557300_447533602406412_6139564580899339847_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=100" link="https://www.instagram.com/p/BoKSUPGg5al/")
  
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/ec6d1a1d6f459aba44c4a8cfd462f1ad/5E64975E/t51.2885-15/sh0.08/e35/c89.0.902.902a/s640x640/69834747_986466475037071_2879916583938753044_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=107" link="https://www.instagram.com/p/B1rXN_roTv4/")
  
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/9212c0823c2e2c82431843d44890db22/5E3EAC0D/t51.2885-15/e35/s480x480/47585211_2233880496884813_4296136872377091209_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=105" link="https://www.instagram.com/p/BsdpIh6BxPB/")
  
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/cc45be94aa39443f8f485e149e420718/5E5CC0A1/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/39956004_317471385681570_6819068332604391424_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=106" link="https://www.instagram.com/p/BnT_d4XFuJJ/")
  
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/dd0a7fe266706839b5ddd0fe4142c9c0/5E406CFA/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37684559_242343189922222_578856764234006528_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=110" link="https://www.instagram.com/p/Bl8mOXKlp7N/")
  
  photo-card(img="https://scontent-dus1-1.cdninstagram.com/vp/ad1d8f6eca2a3116c6da1aad08406ce8/5E449EC6/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37189542_2061235184195776_8978574457354321920_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&_nc_cat=104" link="https://www.instagram.com/p/BlYb96KlH99/")
  
<div class="grid" id="grid">
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/37f408a5a9bfcb4b0e2de07499a560c9/5E632BE6/t51.2885-15/e35/c0.135.1080.1080a/s480x480/40552940_106792203550584_1954235443271646170_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=100" link="https://www.instagram.com/p/BnrVNA_F95p/"></photo-card>
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/398cfc6bf836e3e227ee922936836bbe/5E4D6E72/t51.2885-15/e35/s480x480/36955358_287866911973922_429811318774562816_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=102" link="https://www.instagram.com/p/BloI_FblUVy/"></photo-card>
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/889f95936f24056538c7168915d639c3/5E51E9AC/t51.2885-15/e35/c135.0.809.809a/s480x480/41557300_447533602406412_6139564580899339847_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=100" link="https://www.instagram.com/p/BoKSUPGg5al/"></photo-card>
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/ec6d1a1d6f459aba44c4a8cfd462f1ad/5E64975E/t51.2885-15/sh0.08/e35/c89.0.902.902a/s640x640/69834747_986466475037071_2879916583938753044_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=107" link="https://www.instagram.com/p/B1rXN_roTv4/"></photo-card>
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/9212c0823c2e2c82431843d44890db22/5E3EAC0D/t51.2885-15/e35/s480x480/47585211_2233880496884813_4296136872377091209_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=105" link="https://www.instagram.com/p/BsdpIh6BxPB/"></photo-card>
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/cc45be94aa39443f8f485e149e420718/5E5CC0A1/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/39956004_317471385681570_6819068332604391424_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=106" link="https://www.instagram.com/p/BnT_d4XFuJJ/"></photo-card>
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/dd0a7fe266706839b5ddd0fe4142c9c0/5E406CFA/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37684559_242343189922222_578856764234006528_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=110" link="https://www.instagram.com/p/Bl8mOXKlp7N/"></photo-card>
  <photo-card img="https://scontent-dus1-1.cdninstagram.com/vp/ad1d8f6eca2a3116c6da1aad08406ce8/5E449EC6/t51.2885-15/sh0.08/e35/c135.0.809.809a/s640x640/37189542_2061235184195776_8978574457354321920_n.jpg?_nc_ht=scontent-dus1-1.cdninstagram.com&amp;_nc_cat=104" link="https://www.instagram.com/p/BlYb96KlH99/"></photo-card>
</div>
CSS
body
  margin: 0
  min-height: 100vh
  display: flex
  flex-direction: column
  align-items: center
  justify-content: center
  background-image: radial-gradient(circle, #fff 30%, #ccc)
  padding: 0 40px
  font-family: "Source Sans Pro", Helvetica, sans-serif
  font-weight: 300
  
#grid
  display: grid
  grid-template-columns: repeat(auto-fill, 150px)
  grid-column-gap: 30px
  grid-row-gap: 30px
  align-items: center
  justify-content: center
  width: 100%
  max-width: 700px
  .card
    background-color: #ccc
    width: 150px
    height: 150px
    transition: all .1s ease
    border-radius: 3px
    position: relative
    z-index: 1
    box-shadow: 0 0 5px rgba(0, 0, 0, 0)
    overflow: hidden
    cursor: pointer

    &:hover
      transform: scale(2)
      z-index: 2
      box-shadow: 0 10px 20px rgba(0, 0, 0, .4)
      img
        filter: grayscale(0)
    .reflection
      position: absolute
      width: 100%
      height: 100%
      z-index: 2
      left: 0
      top: 0
      transition: all .1s ease
      opacity: 0
      mix-blend-mode: soft-light
    img
      width: 100%
      height: 100%
      object-fit: cover
      filter: grayscale(.65)
      transition: all .3s ease
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-image: radial-gradient(circle, #fff 30%, #ccc);
  padding: 0 40px;
  font-family: "Source Sans Pro", Helvetica, sans-serif;
  font-weight: 300;
}

#grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, 150px);
  grid-column-gap: 30px;
  grid-row-gap: 30px;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 700px;
}
#grid .card {
  background-color: #ccc;
  width: 150px;
  height: 150px;
  transition: all 0.1s ease;
  border-radius: 3px;
  position: relative;
  z-index: 1;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0);
  overflow: hidden;
  cursor: pointer;
}
#grid .card:hover {
  -webkit-transform: scale(2);
          transform: scale(2);
  z-index: 2;
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
}
#grid .card:hover img {
  -webkit-filter: grayscale(0);
          filter: grayscale(0);
}
#grid .card .reflection {
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: 2;
  left: 0;
  top: 0;
  transition: all 0.1s ease;
  opacity: 0;
  mix-blend-mode: soft-light;
}
#grid .card img {
  width: 100%;
  height: 100%;
  -o-object-fit: cover;
     object-fit: cover;
  -webkit-filter: grayscale(0.65);
          filter: grayscale(0.65);
  transition: all 0.3s ease;
}
JavaScript
Vue.component("photo-card", {
  template: `<a class="card"
                :href="link"
                target="_blank"
                ref="card"
                @mousemove="move"
                @mouseleave="leave"
                @mouseover="over">
                  <div class="reflection" ref="refl"></div>
                  <img :src="img"/>
            </a>`,
  props: ["img", "link"],
  mounted() {},
  data: () => ({
    debounce: null,
  }),
  methods: {
    over() {
      const refl = this.$refs.refl;
      refl.style.opacity = 1;
    },
    leave() {
      const card = this.$refs.card;
      const refl = this.$refs.refl;
      card.style.transform = `perspective(500px) scale(1)`;
      refl.style.opacity = 0;
    },

    move() {
      const card = this.$refs.card;
      const refl = this.$refs.refl;

      const relX = (event.offsetX + 1) / card.offsetWidth;
      const relY = (event.offsetY + 1) / card.offsetHeight;
      const rotY = `rotateY(${(relX - 0.5) * 60}deg)`;
      const rotX = `rotateX(${(relY - 0.5) * -60}deg)`;
      card.style.transform = `perspective(500px) scale(2) ${rotY} ${rotX}`;

      const lightX = this.scale(relX, 0, 1, 150, -50);
      const lightY = this.scale(relY, 0, 1, 30, -100);
      const lightConstrain = Math.min(Math.max(relY, 0.3), 0.7);
      const lightOpacity = this.scale(lightConstrain, 0.3, 1, 1, 0) * 255;
      const lightShade = `rgba(${lightOpacity}, ${lightOpacity}, ${lightOpacity}, 1)`;
      const lightShadeBlack = `rgba(0, 0, 0, 1)`;
      refl.style.backgroundImage = `radial-gradient(circle at ${lightX}% ${lightY}%, ${lightShade} 20%, ${lightShadeBlack})`;
    },
    scale: (val, inMin, inMax, outMin, outMax) =>
      outMin + (val - inMin) * (outMax - outMin) / (inMax - inMin)
  }
});

const app = new Vue({
  el: "#grid"
});
Vue.component("photo-card", {
  template: `<a class="card"
                :href="link"
                target="_blank"
                ref="card"
                @mousemove="move"
                @mouseleave="leave"
                @mouseover="over">
                  <div class="reflection" ref="refl"></div>
                  <img :src="img"/>
            </a>`,
  props: ["img", "link"],
  mounted() {},
  data: () => ({
    debounce: null }),

  methods: {
    over() {
      const refl = this.$refs.refl;
      refl.style.opacity = 1;
    },
    leave() {
      const card = this.$refs.card;
      const refl = this.$refs.refl;
      card.style.transform = `perspective(500px) scale(1)`;
      refl.style.opacity = 0;
    },

    move() {
      const card = this.$refs.card;
      const refl = this.$refs.refl;

      const relX = (event.offsetX + 1) / card.offsetWidth;
      const relY = (event.offsetY + 1) / card.offsetHeight;
      const rotY = `rotateY(${(relX - 0.5) * 60}deg)`;
      const rotX = `rotateX(${(relY - 0.5) * -60}deg)`;
      card.style.transform = `perspective(500px) scale(2) ${rotY} ${rotX}`;

      const lightX = this.scale(relX, 0, 1, 150, -50);
      const lightY = this.scale(relY, 0, 1, 30, -100);
      const lightConstrain = Math.min(Math.max(relY, 0.3), 0.7);
      const lightOpacity = this.scale(lightConstrain, 0.3, 1, 1, 0) * 255;
      const lightShade = `rgba(${lightOpacity}, ${lightOpacity}, ${lightOpacity}, 1)`;
      const lightShadeBlack = `rgba(0, 0, 0, 1)`;
      refl.style.backgroundImage = `radial-gradient(circle at ${lightX}% ${lightY}%, ${lightShade} 20%, ${lightShadeBlack})`;
    },
    scale: (val, inMin, inMax, outMin, outMax) =>
    outMin + (val - inMin) * (outMax - outMin) / (inMax - inMin) } });

const app = new Vue({
  el: "#grid" });
https://www.w3tweaks.com/
Do you like CV's articles? Follow on social!