<!-- //// -->
<!-- Tile -->
<!-- //// -->
<div class="tile">
<!-- Why the extra first and last class when you could use :first-child and :last-child you ask? -->
<!-- It's so that I don't have to load a extra JS file to allow Greensock to target pseudo elements. -->
<!-- This way is a little uglier but more performant. -->
<img alt="" class="tile__img tile__img--first" />
<img alt="" class="tile__img tile__img--last" />
<!-- img tags don't validate without src tag :(, let's pretend like we didn't see that ;-P... -->
<!-- ...because this will be implemented into a static site generator, right? riggght? :X -->
<div class="title">
<br />
<div class="title__container">
<div class="title__text title__text--first"></div>
<div class="title__text title__text--last"></div>
</div>
</div>
</div>
<!-- /////////// -->
<!-- Next button -->
<!-- /////////// -->
<button class="next-tile">
<span class="next-tile__details">
<span class="next-tile__heading">Up next</span>
<span class="next-tile__title">
<br />
<span class="next-tile__title__text next-tile__title__text--first"></span>
<span class="next-tile__title__text next-tile__title__text--last"></span>
</span>
<svg class="next-tile__arrow" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 42.6 20.1" style="enable-background:new 0 0 42.6 20.1;" xml:space="preserve">
<path class="st0" d="M0,8.2h35.5l-5.6-5.6L32.5,0l10.1,10.1L32.5,20.1l-2.6-2.6l5.6-5.6H0V8.2z"/>
</svg>
<div class="test-arrow"></div>
</span>
<span class="next-tile__preview">
<img class="next-tile__preview__img next-tile__preview__img--first" alt="" />
<img class="next-tile__preview__img next-tile__preview__img--last" alt="" />
</span>
</button>
html,
body {
height: 100%;
}
html {
box-sizing: border-box;
font-size: 62.5%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: 'Open Sans', sans-serif;
background: #000;
}
/* ---- */
/* Tile */
/* ---- */
.tile {
width: 100%;
height: 100%;
background: #000;
position: relative;
overflow: hidden;
}
.title {
position: absolute;
top: 50%;
left: 10rem;
color: #fff;
font-size: 5.4rem;
font-weight: 700;
line-height: 1.3;
text-shadow: 0 0.1rem 0 rgba(0,0,0, 0.15);
letter-spacing: -0.02rem;
overflow: hidden;
}
.title--last {
opacity: 0;
}
.title__container {
position: absolute;
top: 0;
left: 0;
}
.tile__img {
position: absolute;
width: 100%;
height: auto;
}
/* --------- */
/* Next tile */
/* --------- */
.next-tile {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
display: flex;
border-top-left-radius: 0.1rem;
border-bottom-left-radius: 0.1rem;
overflow: hidden;
padding: 0;
background: transparent;
border: 0;
cursor: pointer;
outline: none;
z-index: 100;
margin: 0;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
.next-tile__details {
width: 20rem;
height: 33rem;
background: #fff;
text-align: left;
display: flex;
justify-content: flex-end;
flex-direction: column;
padding: 5rem 3.5rem;
z-index: 10;
position: relative;
box-shadow: 0.3rem 0 1rem 0 rgba(0,0,0,0.26);
}
.next-tile__heading {
margin-bottom: 4.5rem;
text-transform: uppercase;
color: #b3b3b3;
font-weight: 600;
display: block;
}
.next-tile__title {
margin-bottom: 6rem;
font-size: 2rem;
font-weight: 700;
line-height: 1.3;
letter-spacing: -0.05rem;
color: #222222;
display: block;
position: relative;
}
.next-tile__title__text {
position: absolute;
top: 0;
left: 0;
}
.next-tile__title__text--last {
opacity: 0;
}
.next-tile__arrow {
fill: #b3b3b3;
width: 2.4rem;
display: block;
}
.next-tile__preview {
width: 16rem;
height: 33rem;
background: #000;
position: relative;
overflow: hidden;
display: block;
}
.next-tile__preview img {
position: absolute;
width: 100%;
height: auto;
}
img.next-tile__preview__img--last {
opacity: 0;
transform: translateY(-50%) scale(1.6);
transform-origin: 50% 50%;
}
/* This was a very quick mockup of the mobile view, if this was a client project I... */
/* ...would put a little more thought into it and try to flush out something more usable on mobile... */
/* ...as it sits the image wastes a lot of space, something better can be done here. */
/* Oh and do it mobile first, this will prevent you from having to overwrite so many properties */
@media (max-height: 400px) {
.next-tile__preview {
height: 23rem;
}
.next-tile__details {
height: 23rem;
padding: 3rem 2.5rem;
}
.next-tile__title {
margin-bottom: 4rem;
}
}
@media (max-width: 1180px) {
html { font-size: 52.5%; }
}
@media (max-width: 990px) {
html { font-size: 42.5%; }
}
@media (max-width: 900px) {
.title {
font-size: 3.8rem;
top: 37%;
left: 10%;
}
.next-tile {
top: auto;
bottom: 4rem;
transform: translateX(0);
}
.next-tile__preview {
height: 100%;
overflow: visible;
}
.next-tile__details {
height: auto;
padding: 2rem;
}
.next-tile__heading {
margin-bottom: 1rem;
}
.next-tile__title {
margin-bottom: 1rem;
}
}
const tiles = [
{
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/ui8-alex-shutin-kKvQJ6rK6S4-unsplash.jpg',
thumb: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/ui8-alex-shutin-kKvQJ6rK6S4-unsplash--thumb.jpg',
title: 'Summer treads on <br />the heels of spring.',
nextTitle: 'Blue <br />Mountains'
},
{
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/ui8-marat-gilyadzinov-MYadhrkenNg-unsplash.jpg',
thumb: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/ui8-marat-gilyadzinov-MYadhrkenNg-unsplash--thumb.jpg',
title: 'Jellyfish make <br />everything better.',
nextTitle: 'Squishy <br />Jellies'
},
{
image: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/ui8-luca-bravo-bTxMLuJOff4-unsplash.jpg',
thumb: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/ui8-luca-bravo-bTxMLuJOff4-unsplash--thumb.jpg',
title: 'Design adds value <br />faster than it adds costs.',
nextTitle: 'Paper <br />Cut'
},
];
let activeIndex = 0;
const nextButton = document.querySelector('.next-tile');
updateTileRatio();
populateInitialData();
nextButton.addEventListener('click', nextTile);
// ---------------------
// Populate initial data
// ---------------------
function populateInitialData() {
// It would be better to target the individual elements as you can't be sure that the arrays below...
// ...will only contain 2 items. But it's my pen and I'm sureee that there's only 2 elements ;-P
const tileImages = document.querySelectorAll('.tile__img');
tileImages[0].src = `${tiles[activeIndex].image}`;
tileImages[1].src = `${tiles[getNextIndex()].image}`;
const tileTitles = document.querySelectorAll('.title__text');
tileTitles[0].innerHTML = tiles[activeIndex].title;
tileTitles[1].innerHTML = tiles[getNextIndex()].title;
const nextButtonImages = document.querySelectorAll('.next-tile__preview__img');
nextButtonImages[0].src = `${tiles[getNextIndex()].thumb}`;
nextButtonImages[1].src = `${tiles[getNextIndex(1)].thumb}`;
const nextButtonTitles = document.querySelectorAll('.next-tile__title__text');
nextButtonTitles[0].innerHTML = tiles[getNextIndex()].nextTitle;
nextButtonTitles[1].innerHTML = tiles[getNextIndex(1)].nextTitle;
}
// ------------------------
// Set the tile image ratio
// ------------------------
// Why are we doing this and not just using object: cover in CSS?
// Large images, cover, Chrome, and Greensock don't play well together. On the first tile transition...
// ...you will see a noticable studder. This disappears on initial transitions but it's enough to prevent me from using it...
// If anybody knows a workaround to prevent the studder, please let me know!
function updateTileRatio() {
const browserWidth = document.body.clientWidth;
const browserHeight = document.body.clientHeight;
const browserRatio = browserWidth/browserHeight;
const imageWidth = 3000; // Yeah yeah yeah, magic numbers... let's just say this is what my spec is set to - if we have to use a different size we will find another way to get the values
const imageHeight = 2000;
const imageRatio = imageWidth/imageHeight;
const tileImages = document.querySelectorAll('.tile__img');
// This could be a bit better if we checked to see if we even need to fire the stuff below...
// ...if the ratio is still the same with a browser resize we should just skip over all of this code. #laziness #itsjustapen
if (browserRatio < imageRatio) {
for(let i = 0; i < tileImages.length; i++) {
tileImages[i].style.width = 'auto';
tileImages[i].style.height = '100%';
}
} else {
for(let i = 0; i < tileImages.length; i++) {
tileImages[i].style.width = '100%';
tileImages[i].style.height = 'auto';
}
}
}
// ---------------
// Screen resized!
// ---------------
window.addEventListener('resize', screenResized);
// You might want to use a debouncer or something to prevent this function from firing too many times...
// ...but for this demo we will leave it (https://davidwalsh.name/javascript-debounce-function)
function screenResized() {
updateTileRatio();
}
// ---------------
// Title animation
// ---------------
const titleAnimation = new TimelineMax({ paused: true })
.to('.title__container', 0.8, {ease: Power2.easeOut, yPercent: -50}, 'titleAnimation')
.to('.title__text--first', 0.5, {opacity: 0}, 'titleAnimation')
.eventCallback('onComplete', () => {
// Update the titles and reset the animation so that we could...
// ...just play the same animation on next click
titleAnimation.progress(0).pause();
const titles = document.querySelectorAll('.title__text');
titles[0].innerHTML = tiles[activeIndex].title;
titles[1].innerHTML = tiles[getNextIndex()].title;
});
// --------------------------
// Next tile button animation
// --------------------------
// Mixing css set properties with Greensock properties causes rendering issues...
// ...so it's best to set positioning of anything that will change using .set()
// https://greensock.com/forums/topic/20822-animation-co-ordinates-wrong-after-resize/?tab=comments#comment-97600
TweenMax.set('.next-tile__preview img', {top: '50%', right: '0', y: '-50%'});
TweenMax.set('.tile__img', {top: '50%', left: '50%', x: '-50%', y: '-50%'});
TweenMax.set('.tile__img--last', {scale: 1.2, opacity: 0.001}); // Setting opacity 0 here causes lag on initial play, this dissapears later on, will open a ticket and see if this is a known issue
TweenMax.set('.tile__img--first, .title__img--last', {yPercent: -50, xPercent: -50});
TweenMax.set('.title', {y: '-50%', width: '100%'});
TweenMax.set('.title__container', {width: '100%'});
// Text change animation
const nextTextAnimation = new TimelineMax({ paused: true })
.to('.next-tile__title__text--first', 0.4, {opacity: 0}, 'textChange')
.to('.next-tile__title__text--last', 0.4, {opacity: 1}, 'textChange');
// Slide next tile to reveal new image
const titles = document.querySelectorAll('.next-tile__title__text');
const tileImages = document.querySelectorAll('.tile__img');
const previewImages = document.querySelectorAll('.next-tile__preview__img');
const nextButtonAnimation = new TimelineMax({ paused: true })
.to('.next-tile__details', 0.6, {ease: Power1.easeOut, xPercent: 80})
.to('.tile__img--last', 0.6, {ease: Sine.easeOut, opacity: 1, scale: 1}, 0)
.to('.next-tile__preview__img--first', 0, {opacity: 0}, 'sliderClosed')
.to('.next-tile__preview__img--last', 0.6, {ease: Sine.easeOut, opacity: 1, scale: 1}, 'sliderClosed')
.to('.next-tile__details', 0.5, {ease: Sine.easeOut, xPercent: 0}, 'sliderClosed+=0.15')
.add(() => nextTextAnimation.play(), '-=0.5')
.eventCallback('onComplete', () => {
nextButtonAnimation.progress(0).pause();
nextTextAnimation.progress(0).pause();
tileImages[0].src = `${tiles[activeIndex].image}`;
tileImages[1].src = `${tiles[getNextIndex()].image}`;
previewImages[0].src = `${tiles[getNextIndex()].thumb}`;
previewImages[1].src = `${tiles[getNextIndex(1)].thumb}`;
titles[0].innerHTML = tiles[getNextIndex()].nextTitle;
titles[1].innerHTML = tiles[getNextIndex(1)].nextTitle;
});
// -------
// Helpers
// -------
function getNextIndex(skipSteps = 0) {
let newIndex = activeIndex;
incrementIndex();
for (let i = 0; i < skipSteps; i++) {
incrementIndex();
}
function incrementIndex() {
if (newIndex >= tiles.length - 1) {
newIndex = 0
} else {
newIndex = newIndex + 1
}
}
return newIndex;
}
// -----------
// Tile Change
// -----------
function nextTile() {
// We want to prevent clicking on the next tile button if an animation is active...
// ...to prevent the animations from being interupted mid animation.
if (
!titleAnimation.isActive() &&
!nextButtonAnimation.isActive() &&
!nextTextAnimation.isActive()
) {
activeIndex = getNextIndex();
titleAnimation.play();
nextButtonAnimation.play();
}
}
// ------------------------------
// Initialize all timeline values
// ------------------------------
titleAnimation.progress(1).progress(0);
nextButtonAnimation.progress(1).progress(0);
nextTextAnimation.progress(1).progress(0);
View Compiled
This Pen doesn't use any external CSS resources.