<div id="app"></div>
<a href="https://www.craftedbygc.com/" target="_blank" class="footer-link">
  Inspired By
</a>
<a href="https://twitter.com/NikolayTalanov" target="_blank" class="footer-link footer-link--twitter">
  <img src="https://cdn1.iconfinder.com/data/icons/logotypes/32/twitter-128.png">
</a>
*, *:before, *:after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Roboto', Helvetica, Arial, sans-serif;
  background: #000;
}

$baseClass: '.distorted-gallery';
$imgHeight: 70vh;
$imgRatio: 1.5;
$transATMS: 800;
$transAT: $transATMS * 1ms;
$hoverAT: 0.3s;
$easing: cubic-bezier(.97,.13,.34,1.15);

// initial settings for inactive images before one of them becomes active
$inactiveX: 170%;
$inactiveY: 0;
$inactiveDepth: -30px; // based on 1000px perspective
$inactiveRotateX: 8deg;
$inactiveRotateY: 35deg;
$inactiveOriginY: 50%;
$inactiveScale: scale(2, 0.7);

#{$baseClass} {
  --transition-time: #{$transATMS};
  
  overflow: hidden;
  position: relative;
  height: 100vh;
  perspective: 1000px;
  transform-style: preserve-3d;
  
  &__image {
    $width: $imgHeight / $imgRatio;
    
    position: absolute;
    left: 50%;
    top: 50%;
    width: $width;
    height: $imgHeight;
    margin-left: $width / -2;
    margin-top: $imgHeight / -2;
    object-fit: cover;
    object-position: 50% 50%;
    opacity: 0;
    transition-timing-function: ease-in-out;
    will-change: transform, opacity;
    
    &.s--left {
      transform-origin: 0 $inactiveOriginY;
      transform: rotateX($inactiveRotateX * -1) rotateY($inactiveRotateY * -1) translate3d($inactiveX * -1, $inactiveY * -1, $inactiveDepth) $inactiveScale;
    }
    
    &.s--right {
      transform-origin: 100% $inactiveOriginY;
      transform: rotateX($inactiveRotateX) rotateY($inactiveRotateY) translate3d($inactiveX, $inactiveY, $inactiveDepth) $inactiveScale;
    }
    
    &.s--prev {
      opacity: 0;
      transition: all $transAT * 0.7;
    }
    
    &.s--prev-left {
      transform-origin: 0 100%;
      transform: rotate(-5deg) translate3d(-50%, 30%, 0);
    }
    
    &.s--prev-right {
      transform-origin: 100% 100%;
      transform: rotate(5deg) translate3d(50%, 30%, 0);
    }
    
    &.s--active {
      opacity: 1;
      transform: translate3d(0, 0, 0);
      transition: transform $transAT * 0.8 $transAT * 0.2 $easing, opacity $transAT * 0.4 $transAT * 0.2 ease-in;
    }
    
    #{$baseClass}.s--no-transition & {
      transition: all 0s 0s;
    }
  }
  
  &__control {
    $size: 50px;
    
    z-index: 100;
    position: absolute;
    left: 50px;
    top: 50%;
    width: $size;
    height: $size;
    margin-top: $size/-2;
    border-radius: 50%;
    background: rgba(255,255,255,0.4);
    cursor: pointer;
    
    &:before {
      content: '';
      position: absolute;
      left: 50%;
      top: 50%;
      width: 20px;
      height: 20px;
      margin-left: -10px;
      margin-top: -10px;
      border: 2px solid #000;
      border-bottom: none;
      border-right: none;
      transform: translateX(5px) rotate(-45deg);
    }
    
    &--right {
      left: auto;
      right: 50px;
      
      &:before {
        transform: translateX(-5px) rotate(135deg);
      }
    }
  }
}

.footer-link {
  z-index: 100;
  position: absolute;
  left: 5px;
  bottom: 5px;
  font-size: 16px;
  color: #fff;

  img {
    width: 32px;
    vertical-align: top;
  }

  &--twitter {
    left: auto;
    right: 5px;
  }
}
View Compiled
class DistortedGallery extends React.PureComponent {
  constructor(props) {
    super(props);
    
    this.changeTO = null;
    this.animatingTO = null;
    this.AUTOCHANGE_TIME = 4000;
    this.transitionTime = 1000; // default value, updated in DidMount
    
    this.state = {
      isAnimating: false,
      activeImg: 0,
      leftImg: this.getLimitIndex(),
      rightImg: 1,
      prevImgLeft: -1,
      prevImgRight: -1,
      noTransition: false,
    };
  }
  
  componentWillUnmount() {
    window.clearTimeout(this.changeTO);
    window.clearTimeout(this.animatingTO);
  }
  
  componentDidMount() {
    this.transitionTime = Number(getComputedStyle(this.galleryRef).getPropertyValue('--transition-time'));
    //this.runAutochangeTO();
  }
  
  runAutochangeTO = () => {
    this.changeTO = setTimeout(() => {
      this.changeImages();
      this.runAutochangeTO();
    }, this.AUTOCHANGE_TIME);
  }
  
  runAnimatingTO = () => {
    this.animatingTO = setTimeout(() => (
      this.setState({ noTransition: true, prevImgLeft: -1, prevImgRight: -1 }, () => {
        const layoutTrigger = this.galleryRef.offsetTop;
        this.setState({ noTransition: false, isAnimating: false });
      })
    ), this.transitionTime);
  }
  
  getLimitIndex = () => this.props.images.length - 1
  
  getImgIndex = (targetIndex) => {
    const limitIndex = this.getLimitIndex();
    if (targetIndex < 0) return limitIndex;
    if (targetIndex > limitIndex) return 0;
    return targetIndex;
  }
  
  changeImages = (back = false) => {
    window.clearTimeout(this.changeTO);
    if (this.state.isAnimating) return;
    const change = back ? -1 : 1;
    this.setState((st) => {
      const activeImg = this.getImgIndex(st.activeImg + change);
      return {
        isAnimating: true,
        activeImg: activeImg,
        leftImg: this.getImgIndex(activeImg - 1),
        rightImg: this.getImgIndex(activeImg + 1),
        ...(back ? { prevImgRight: st.activeImg } : { prevImgLeft: st.activeImg }),
      };
    }, this.runAnimatingTO);
  }
  
  render() {
    return (
      <div
        className={classNames(
          'distorted-gallery',
          { 's--no-transition': this.state.noTransition },
        )}
        ref={(_gal) => { this.galleryRef = _gal; }}
        >
        <div className="distorted-gallery__images">
          {this.props.images.map((src, i) => (
            <img
              className={classNames(
                'distorted-gallery__image',
                {
                  's--active': i === this.state.activeImg,
                  's--left': i === this.state.leftImg,
                  's--right': i === this.state.rightImg,
                  's--prev': i === this.state.prevImgLeft || i === this.state.prevImgRight,
                  's--prev-left': i === this.state.prevImgLeft,
                  's--prev-right': i === this.state.prevImgRight,
                },
              )}
              src={src}
              alt={`Cute Cat №${i + 1}`}
            />
          ))}
        </div>
        <div
          className="distorted-gallery__control"
          onClick={() => this.changeImages(true)}
          />
        <div
          className="distorted-gallery__control distorted-gallery__control--right"
          onClick={() => this.changeImages()}
          />
      </div>
    );
  }
}

const NUM_OF_IMAGES = 7;
const images = Array.from(Array(NUM_OF_IMAGES).keys()).map(i => (
  `https://s3-us-west-2.amazonaws.com/s.cdpn.io/142996/dis-slider-cat${i + 1}.jpg`
));

ReactDOM.render(<DistortedGallery images={images} />, document.querySelector('#app'));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.6/index.min.js