<a href="https://twitter.com/irkopal" class="twitter" target="_blank">
<span class="twitter__icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="640" height="640" viewBox="0 0 640 640">
<path d="M554.112 199.872c0.256 5.184 0.352 10.432 0.352 15.616 0 159.68-121.504 343.744-343.68 343.744-68.256 0-131.712-20-185.184-54.304 9.472 1.12 19.072 1.696 28.8 1.696 56.64 0 108.704-19.328 150.016-51.68-52.832-0.992-97.472-35.872-112.832-83.872 7.36 1.376 14.944 2.112 22.72 2.112 11.040 0 21.728-1.44 31.84-4.192-55.264-11.136-96.896-59.936-96.896-118.496 0-0.512 0-0.992 0-1.504 16.288 9.056 34.944 14.496 54.72 15.136-32.416-21.696-53.76-58.624-53.76-100.576 0-22.112 5.952-42.88 16.384-60.736 59.552 73.12 148.608 121.184 248.992 126.24-2.048-8.864-3.104-18.048-3.104-27.552 0-66.688 54.048-120.736 120.768-120.736 34.752 0 66.144 14.624 88.192 38.112 27.488-5.44 53.344-15.488 76.704-29.312-9.024 28.192-28.192 51.872-53.12 66.816 24.448-2.944 47.68-9.376 69.376-19.008-16.192 24.256-36.672 45.504-60.288 62.496z"></path>
</svg>
</span>
<span class="twitter__name">
@irkopal
</span>
</a>
<div id="app"></div>
@import url('https://fonts.googleapis.com/css?family=Roboto+Condensed:300,400');
$spot-color: #cf00f1;
*, *::after, *::before {
box-sizing: border-box;
}
html, body {
height: 100%;
min-height: 100%;
}
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Roboto Condensed', sans-serif;
background: linear-gradient(15deg, #00a9f1, #cf00f1);
background-repeat: no-repeat;
background-attachment: fixed;
font-smoothing: antialiased;
}
[v-cloak] { display:none; }
.twitter {
position: absolute;
top: 1em;
left: 1em;
text-decoration: none;
color: rgba(#524ad0, .8);
display: flex;
align-items: center;
> * {
line-height: 1;
}
&__icon {
width: 32px;
height: 32px;
background: #fff;
padding: 5px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
&__name {
display: inline-block;
margin-left: .5em;
color: #fff;
}
svg {
fill: currentColor;
width: 1em;
height: 1em;
}
}
.icon {
display: inline-block;
width: 1em;
height: 1em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
.app {
width: 90vw;
box-shadow: 0 .7em 1em rgba(#000, .2);
font-weight: 300;
width: 95vw;
@media (min-width: 768px) {
width: 770px;
}
}
.image-hotspot {
position: relative;
padding-top: 66.6%;
overflow: hidden;
&.is-selected {
> img {
filter: blur(5px);
transition: all .5s 1s;
}
}
> img {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: auto;
transition: all .5s;
}
}
.hotspot-point {
z-index: 2;
position: absolute;
display: block;
span {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 1.8em;
height: 1.8em;
background: $spot-color;
border-radius: 50%;
animation: pulse 3s ease infinite;
transition: background .3s;
box-shadow: 0 2px 10px rgba(#000, .2);
&:after {
content: attr(data-price);
position: absolute;
bottom: 130%;
left: 50%;
color: white;
text-shadow: 0 1px black;
font-weight: 600;
font-size: 1.2em;
opacity: 0;
transform: translate(-50%, 10%) scale(.5);
transition: all .25s;
}
}
svg {
opacity: 0;
color: $spot-color;
font-size: 1.4em;
transition: opacity .2s;
}
&:before,
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
pointer-events: none;
}
&:before {
z-index: -1;
border: .15em solid rgba(#fff, .9);
opacity: 0;
transform: scale(2);
transition: transform .25s, opacity .2s;
}
&:after {
z-index: -2;
background:#fff;
animation: wave 3s linear infinite;
}
&:hover{
span {
animation: none;
background: #fff;
&:after {
opacity: 1;
transform: translate(-50%, 0) scale(1);
}
}
svg {
opacity: 1;
}
&:before {
opacity: 1;
transform: scale(1.5);
animation: borderColor 2s linear infinite;
}
&:after {
animation: none;
opacity: 0;
}
}
}
.hotspots-enter-active {
transition: all .5s 1s;
}
.hotspots-leave-active {
transition: all .5s;
}
.hotspots-enter, .hotspots-leave-to {
opacity: 0;
transform: scale(.3);
}
@keyframes pulse{
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes borderColor{
0% {
border-color: #fff;
}
50% {
border-color: $spot-color;
}
100% {
border-color: #fff;
}
}
@keyframes wave{
0% {
opacity: 1;
transform: scale(.8);
}
100% {
opacity: 0;
transform: scale(2);
}
}
.hotspot-details {
--top: 0;
--left: 0;
z-index: 5;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
will-change: clip-path;
transform: translateZ(0);
&.is-loaded {
.hotspot-details__content {
opacity: 1;
transform: scale(1);
transition: opacity .3s, transform .3s;
backface-visibility: hidden;
}
.hotspot-details__nav-item {
transform: translate(0, 0);
transition: transform .3s;
backface-visibility: hidden;
@for $i from 2 through 3 {
&:nth-child(#{$i}) {
transition-delay: 75ms * $i;
}
}
}
}
&__left,
&__right {
position: absolute;
top: 0;
width: 100%;
height: 100%;
background: rgba(#00a9f1, 0.8);
will-change: background;
}
&__left {
clip-path: polygon(0 0, 52.1% 0, 47.1% 100%, 0% 100%);
.hotspot-details__content {
padding: 0;
}
}
&__right {
background: rgba(#fff, 0.8);
clip-path: polygon(52% 0, 100% 0, 100% 100%, 47% 100%);
.hotspot-details__content {
position: relative;
left: 50%;
padding-left: 2.5em;
}
}
&__content {
opacity: 0;
width: 50%;
height: 100%;
padding: 4em 2em 2em 2em;
transform: scale(.9);
transition: opacity .3s .3s, transform .3s .3s;
img {
width: 80%;
}
}
&__title {
margin-top: 0;
font-weight: 300;
font-size: 24px;
@media (min-width: 768px) {
font-size: 38px;
}
}
&__description {
margin-bottom: 32px;
font-size: 14px;
@media (min-width: 768px) {
font-size: 16px;
}
}
&__price {
font-size: 38px;
font-weight: 400;
color: #00a9f1;
text-shadow: 1px 1px white;
}
&__nav {
position: absolute;
bottom: 1em;
left: 0;
width: 47%;
display: flex;
}
&__nav-item {
width: 33%;
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
transform: translate(0, 120%);
transition: transform .3s;
@for $i from 2 through 3 {
&:nth-child(#{$i}) {
transition-delay: 75ms * $i;
}
}
&:hover {
img {
transform: scale(1.2);
opacity: 1;
}
}
img {
width: 35px;
opacity: .8;
transform: scale(1);
transition: transform .2s, opacity .2s;
backface-visibility: hidden;
}
}
&__close {
display: block;
z-index: 2;
position: absolute;
top: 10px;
right: 10px;
color: black;
font-size: 2em;
line-height: 1;
text-decoration: none;
&:hover,
&:active {
color: #000;
transition: transform .3s;
}
&:active {
transform: scale(1.1);
}
}
}
.detail-enter-active {
animation: detailsReveal .8s cubic-bezier(0.645, 0.045, 0.355, 1);
.hotspot-details__left {
background: rgba(#cf00f1, .8);
}
.hotspot-details__right {
background: rgba(#cf00f1, .8);
}
}
.detail-enter-to {
.hotspot-details__left {
background: rgba(#00a9f1, 0.8);
transition: background .3s .2s;
}
.hotspot-details__right {
background: rgba(#fff, 0.8);
transition: background .3s .2s;
}
}
.detail-leave-active {
animation: detailsReveal .8s .5s cubic-bezier(0.645, 0.045, 0.355, 1) reverse;
.hotspot-details__left {
background: rgba(#cf00f1, .8);
transition: background .3s .7s;
}
.hotspot-details__right {
background: rgba(#cf00f1, .8);
transition: background .3s .7s;
}
}
.product-fade-enter-active {
transition: all .25s ease;
@for $i from 2 through 3 {
&.animated:nth-child(#{$i}) {
transition-delay: 50ms * $i;
}
}
}
.product-fade-leave-active {
transition: all .25s ease;
@for $i from 2 through 3 {
&.animated:nth-child(#{$i}) {
transition-delay: 50ms * $i;
}
}
}
.product-fade-enter, .product-fade-leave-to {
transform: translate3d(0,10px,0);
opacity: 0;
}
@keyframes detailsReveal {
0% {
clip-path: circle(.9em at calc(var(--left) + .9em) calc(var(--top) + .9em));
}
30% {
clip-path: circle(5vw at 50% 50%);
}
100% {
clip-path: circle(130% at 50% 50%);
}
}
/*! Flickity v2.0.8
http://flickity.metafizzy.co
---------------------------------------------- */
.flickity-enabled {
position: relative;
}
.flickity-enabled:focus { outline: none; }
.flickity-viewport {
overflow: hidden;
position: relative;
height: 100%;
}
.flickity-slider {
position: absolute;
width: 100%;
height: 100%;
}
/* draggable */
.flickity-enabled.is-draggable {
tap-highlight-color: transparent;
tap-highlight-color: transparent;
user-select: none;
user-select: none;
user-select: none;
user-select: none;
}
.flickity-enabled.is-draggable .flickity-viewport {
cursor: move;
cursor: grab;
cursor: grab;
}
.flickity-enabled.is-draggable .flickity-viewport.is-pointer-down {
cursor: grabbing;
cursor: grabbing;
}
.carousel {
height: 100%;
}
.carousel-cell {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
transform: scale(.6);
opacity: 0;
backface-visibility: hidden;
transition: transform .3s, opacity .3s;
will-change: transform, opacity;
&.is-selected {
opacity: 1;
transform: scale(1);
transition: transform .3s .2s, opacity .3s .1s;
}
}
View Compiled
const hotspots = [
{
id: 1,
title: 'iMac 27"',
description: 'Accuracy. Brightness. Clarity. Regardless of how you measure the quality of a display, Retina is in a class by itself. The pixel density is so high that you won’t detect a single one while using iMac. Text is so sharp, you’ll feel like you’re reading email and documents on a printed page.',
price: '$1,299.00',
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/437221/imac.png',
position: { top: '20%', left: '38%'}
},
{
id: 2,
title: 'Magic Mouse 2',
description: 'Featuring a new design, Magic Mouse 2 is completely rechargeable, so you’ll eliminate the use of traditional batteries. It’s lighter, has fewer moving parts thanks to its built-in battery and continuous bottom shell, and has an optimized foot design.',
price: '$99.00',
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/437221/magic-mouse.png',
position: { top: '85%', left: '75%'}
},
{
id: 3,
title: 'Magic Keyboard',
description: 'The Magic Keyboard combines a sleek new design with a built-in rechargeable battery and enhanced key features. With an improved scissor mechanism beneath each key for increased stability, as well as optimized key travel and a lower profile, the Magic Keyboard provides a remarkably comfortable and precise typing experience.',
price: '$79.00',
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/437221/magic-keyboard.png',
position: { top: '85%', left: '48%'}
}
]
const HotspotDetails = {
name: 'HotspotDetails',
template: `
<transition
name="detail"
@before-enter="beforeEnter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
>
<div class="hotspot-details">
<a href="#" @click.prevent="close" class="hotspot-details__close">
<svg class="icon icon-close" viewBox="0 0 24 24">
<path d="M18.984 6.422l-5.578 5.578 5.578 5.578-1.406 1.406-5.578-5.578-5.578 5.578-1.406-1.406 5.578-5.578-5.578-5.578 1.406-1.406 5.578 5.578 5.578-5.578z"></path>
</svg>
</a>
<div class="hotspot-details__left">
<div class="hotspot-details__content">
<div class="carousel" ref="carousel">
<div class="carousel-cell" v-for="item in allItems">
<img :src="item.image" width="80%" />
</div>
</div>
</div>
</div>
<div class="hotspot-details__right">
<div class="hotspot-details__content">
<transition name="product-fade" mode="out-in">
<h3 class="hotspot-details__title animated" :key="selectedItem.id">{{ selectedItem.title }}</h3>
</transition>
<transition name="product-fade" mode="out-in">
<div class="hotspot-details__description animated" :key="selectedItem.id">{{ selectedItem.description }}</div>
</transition>
<transition name="product-fade" mode="out-in">
<div class="hotspot-details__price animated" :key="selectedItem.id">{{ selectedItem.price }}</div>
</transition>
</div>
</div>
<div class="hotspot-details__nav">
<a href="#" @click.prevent="selectProduct(index)" class="hotspot-details__nav-item" v-for="(item, index) in allItems">
<img :src="item.image" width="80%" />
</a>
</div>
</div>
</transition>
`,
props: {
item: { type: Object },
selectedIndex: { type: Number },
allItems: { type: Array }
},
data() {
return {
selectedItem: this.item
}
},
mounted() {
this.carousel = new Flickity(this.$refs.carousel, {
cellAlign: 'left',
contain: true,
draggable: false,
initialIndex: this.selectedIndex,
imagesLoaded: true,
prevNextButtons: false,
pageDots: false
});
this.carousel.on('select', this.onProductSelected);
},
beforeDestroy() {
setTimeout(() => {
this.carousel.off('select', this.onProductSelected);
this.carousel.destroy();
}, 600);
},
methods: {
close() {
this.$emit('close');
},
selectProduct(index) {
this.carousel.select(index);
},
onProductSelected() {
this.selectedItem = this.allItems[this.carousel.selectedIndex];
},
beforeEnter(el) {
el.style.setProperty(`--top`, this.item.position.top);
el.style.setProperty(`--left`, this.item.position.left);
},
afterEnter(el) {
el.classList.add('is-loaded');
},
beforeLeave(el) {
el.classList.remove('is-loaded');
}
}
}
const App = {
name: 'app',
components: {
HotspotDetails
},
template: `
<div class="app">
<div class="image-hotspot" :class="{'is-selected': open }">
<hotspot-details
:item="selectedHotspot"
:selected-index="selectedIndex"
:all-items="hotspots"
@close="closeDetails"
v-if="open"
></hotspot-details>
<transition-group name="hotspots">
<a
href="#"
class="hotspot-point"
v-for="(hotspot, index) in hotspotItems"
:style="{ top: hotspot.position.top, left: hotspot.position.left }"
@click.prevent="hotspotClicked(hotspot, index)"
:key="index"
>
<span :data-price="hotspot.price">
<svg class="icon icon-close" viewBox="0 0 24 24">
<path d="M18.984 12.984h-6v6h-1.969v-6h-6v-1.969h6v-6h1.969v6h6v1.969z"></path>
</svg>
</span>
</a>
</transition-group>
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/437221/hotspot-image.jpg" alt="" @click="closeDetails">
</div>
</div>
`,
data () {
return {
hotspots,
open: false,
hotspotPosition: null,
selectedHotspot: null
}
},
computed: {
hotspotItems() {
return this.open ? [] : this.hotspots;
}
},
methods: {
closeDetails() {
this.open = false;
},
hotspotClicked(hotspot, index) {
this.selectedHotspot = hotspot;
this.selectedIndex = index;
this.open = true;
}
}
}
new Vue({
el: '#app',
components: {
App
},
render: h => h(App)
})
View Compiled
This Pen doesn't use any external CSS resources.