Realistic 3D Photo Card Gallery

By | November 27, 2019

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
Pug View Compiled HTML Pug
.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/")
  
View Compiled HTML
<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
Sass View Compiled CSS Sass
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
View Compiled 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;
}
#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
Babel View Compiled JS Babel
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"
});
View Compiled JS
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" });
Author: CV

I am a Front-end Developer, graduate of Information Technology, and founder of w3tweaks.com. I have 12+ years commercial experience providing front-end development, producing high quality responsive websites and exceptional user experience.