A slider/carousel built with React. The x and y coordinates of the current slide are set to CSS variables to create dynamic transition effects on mouseover.
A Pen by Ryan Mulligan on CodePen.
| #app |
A slider/carousel built with React. The x and y coordinates of the current slide are set to CSS variables to create dynamic transition effects on mouseover.
A Pen by Ryan Mulligan on CodePen.
| const slideData = [ | |
| { | |
| index: 0, | |
| headline: 'New Fashion Apparel', | |
| button: 'Shop now', | |
| src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/225363/fashion.jpg' | |
| }, | |
| { | |
| index: 1, | |
| headline: 'In The Wilderness', | |
| button: 'Book travel', | |
| src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/225363/forest.jpg' | |
| }, | |
| { | |
| index: 2, | |
| headline: 'For Your Current Mood', | |
| button: 'Listen', | |
| src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/225363/guitar.jpg' | |
| }, | |
| { | |
| index: 3, | |
| headline: 'Focus On The Writing', | |
| button: 'Get Focused', | |
| src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/225363/typewriter.jpg' | |
| } | |
| ] | |
| // ========================= | |
| // Slide | |
| // ========================= | |
| class Slide extends React.Component { | |
| constructor(props) { | |
| super(props) | |
| this.handleMouseMove = this.handleMouseMove.bind(this) | |
| this.handleMouseLeave = this.handleMouseLeave.bind(this) | |
| this.handleSlideClick = this.handleSlideClick.bind(this) | |
| this.imageLoaded = this.imageLoaded.bind(this) | |
| this.slide = React.createRef() | |
| } | |
| handleMouseMove(event) { | |
| const el = this.slide.current | |
| const r = el.getBoundingClientRect() | |
| el.style.setProperty('--x', event.clientX - (r.left + Math.floor(r.width / 2))) | |
| el.style.setProperty('--y', event.clientY - (r.top + Math.floor(r.height / 2))) | |
| } | |
| handleMouseLeave(event) { | |
| this.slide.current.style.setProperty('--x', 0) | |
| this.slide.current.style.setProperty('--y', 0) | |
| } | |
| handleSlideClick(event) { | |
| this.props.handleSlideClick(this.props.slide.index) | |
| } | |
| imageLoaded(event) { | |
| event.target.style.opacity = 1 | |
| } | |
| render() { | |
| const { src, button, headline, index } = this.props.slide | |
| const current = this.props.current | |
| let classNames = 'slide' | |
| if (current === index) classNames += ' slide--current' | |
| else if (current - 1 === index) classNames += ' slide--previous' | |
| else if (current + 1 === index) classNames += ' slide--next' | |
| return ( | |
| <li | |
| ref={this.slide} | |
| className={classNames} | |
| onClick={this.handleSlideClick} | |
| onMouseMove={this.handleMouseMove} | |
| onMouseLeave={this.handleMouseLeave} | |
| > | |
| <div className="slide__image-wrapper"> | |
| <img | |
| className="slide__image" | |
| alt={headline} | |
| src={src} | |
| onLoad={this.imageLoaded} | |
| /> | |
| </div> | |
| <article className="slide__content"> | |
| <h2 className="slide__headline">{headline}</h2> | |
| <button className="slide__action btn">{button}</button> | |
| </article> | |
| </li> | |
| ) | |
| } | |
| } | |
| // ========================= | |
| // Slider control | |
| // ========================= | |
| const SliderControl = ({ type, title, handleClick }) => { | |
| return ( | |
| <button className={`btn btn--${type}`} title={title} onClick={handleClick}> | |
| <svg className="icon" viewBox="0 0 24 24"> | |
| <path d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" /> | |
| </svg> | |
| </button> | |
| ) | |
| } | |
| // ========================= | |
| // Slider | |
| // ========================= | |
| class Slider extends React.Component { | |
| constructor(props) { | |
| super(props) | |
| this.state = { current: 0 } | |
| this.handlePreviousClick = this.handlePreviousClick.bind(this) | |
| this.handleNextClick = this.handleNextClick.bind(this) | |
| this.handleSlideClick = this.handleSlideClick.bind(this) | |
| } | |
| handlePreviousClick() { | |
| const previous = this.state.current - 1 | |
| this.setState({ | |
| current: (previous < 0) | |
| ? this.props.slides.length - 1 | |
| : previous | |
| }) | |
| } | |
| handleNextClick() { | |
| const next = this.state.current + 1; | |
| this.setState({ | |
| current: (next === this.props.slides.length) | |
| ? 0 | |
| : next | |
| }) | |
| } | |
| handleSlideClick(index) { | |
| if (this.state.current !== index) { | |
| this.setState({ | |
| current: index | |
| }) | |
| } | |
| } | |
| render() { | |
| const { current, direction } = this.state | |
| const { slides, heading } = this.props | |
| const headingId = `slider-heading__${heading.replace(/\s+/g, '-').toLowerCase()}` | |
| const wrapperTransform = { | |
| 'transform': `translateX(-${current * (100 / slides.length)}%)` | |
| } | |
| return ( | |
| <div className='slider' aria-labelledby={headingId}> | |
| <ul className="slider__wrapper" style={wrapperTransform}> | |
| <h3 id={headingId} class="visuallyhidden">{heading}</h3> | |
| {slides.map(slide => { | |
| return ( | |
| <Slide | |
| key={slide.index} | |
| slide={slide} | |
| current={current} | |
| handleSlideClick={this.handleSlideClick} | |
| /> | |
| ) | |
| })} | |
| </ul> | |
| <div className="slider__controls"> | |
| <SliderControl | |
| type="previous" | |
| title="Go to previous slide" | |
| handleClick={this.handlePreviousClick} | |
| /> | |
| <SliderControl | |
| type="next" | |
| title="Go to next slide" | |
| handleClick={this.handleNextClick} | |
| /> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| } | |
| ReactDOM.render(<Slider heading="Example Slider" slides={slideData} />, document.getElementById('app')); |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react-transition-group/4.2.1/react-transition-group.min.js"></script> |
| @import url('https://fonts.googleapis.com/css?family=Playfair+Display:700|IBM+Plex+Sans:500&display=swap'); | |
| :root { | |
| --color-primary: #6B7A8F; | |
| --color-secondary: #101118; | |
| --color-accent: #1D1F2F; | |
| --color-focus: #6D64F7; | |
| --base-duration: 600ms; | |
| --base-ease: cubic-bezier(0.25, 0.46, 0.45, 0.84); | |
| } | |
| // ========================= | |
| // Global | |
| // ========================= | |
| *, *:before, *:after { | |
| box-sizing: border-box; | |
| } | |
| html, body { | |
| height: 100%; | |
| } | |
| body { | |
| font-family: 'IBM Plex Sans', sans-serif; | |
| background-color: var(--color-secondary); | |
| } | |
| #app { | |
| align-items: center; | |
| display: flex; | |
| height: 100%; | |
| justify-content: center; | |
| overflow-x: hidden; | |
| width: 100%; | |
| } | |
| h1, h2, h3 { | |
| font-family: 'Playfair Display', serif; | |
| } | |
| .visuallyhidden { | |
| clip: rect(1px, 1px, 1px, 1px); | |
| height: 1px; | |
| overflow: hidden; | |
| position: absolute !important; | |
| white-space: nowrap; | |
| width: 1px; | |
| } | |
| // ========================= | |
| // Icons | |
| // ========================= | |
| .icon { | |
| fill: var(--color-primary); | |
| width: 100%; | |
| } | |
| // ========================= | |
| // Buttons | |
| // ========================= | |
| .btn { | |
| background-color: var(--color-primary); | |
| border: none; | |
| border-radius: 0.125rem; | |
| color: white; | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-size: inherit; | |
| padding: 1rem 2.5rem 1.125rem; | |
| &:focus { | |
| outline-color: var(--color-focus); | |
| outline-offset: 2px; | |
| outline-style: solid; | |
| outline-width: 3px; | |
| } | |
| &:active { | |
| transform: translateY(1px); | |
| } | |
| } | |
| // ========================= | |
| // Slider controls | |
| // ========================= | |
| .slider__controls { | |
| display: flex; | |
| justify-content: center; | |
| position: absolute; | |
| top: calc(100% + 1rem); | |
| width: 100%; | |
| .btn { | |
| --size: 3rem; | |
| align-items: center; | |
| background-color: transparent; | |
| border: 3px solid transparent; | |
| border-radius: 100%; | |
| display: flex; | |
| height: var(--size); | |
| padding: 0; | |
| width: var(--size); | |
| &:focus { | |
| border-color: var(--color-focus); | |
| outline: none; | |
| } | |
| &--previous > * { | |
| transform: rotate(180deg); | |
| } | |
| } | |
| } | |
| // ========================= | |
| // Slider | |
| // ========================= | |
| .slider { | |
| --slide-size: 70vmin; | |
| --slide-margin: 4vmin; | |
| height: var(--slide-size); | |
| margin: 0 auto; | |
| position: relative; | |
| width: var(--slide-size); | |
| } | |
| .slider__wrapper { | |
| display: flex; | |
| margin: 0 calc(var(--slide-margin) * -1); | |
| position: absolute; | |
| transition: transform var(--base-duration) cubic-bezier(0.25, 1, 0.35, 1); | |
| } | |
| // ========================= | |
| // Slide | |
| // ========================= | |
| .slide { | |
| align-items: center; | |
| color: white; | |
| display: flex; | |
| flex: 1; | |
| flex-direction: column; | |
| height: var(--slide-size); | |
| justify-content: center; | |
| margin: 0 var(--slide-margin); | |
| opacity: 0.25; | |
| position: relative; | |
| text-align: center; | |
| transition: | |
| opacity calc(var(--base-duration) / 2) var(--base-ease), | |
| transform calc(var(--base-duration) / 2) var(--base-ease); | |
| width: var(--slide-size); | |
| z-index: 1; | |
| &--previous, | |
| &--next { | |
| &:hover { | |
| opacity: 0.5; | |
| } | |
| } | |
| &--previous { | |
| cursor: w-resize; | |
| &:hover { | |
| transform: translateX(2%); | |
| } | |
| } | |
| &--next { | |
| cursor: e-resize; | |
| &:hover { | |
| transform: translateX(-2%); | |
| } | |
| } | |
| } | |
| .slide--current { | |
| --x: 0; | |
| --y: 0; | |
| --d: 50; | |
| opacity: 1; | |
| pointer-events: auto; | |
| user-select: auto; | |
| @media (hover: hover) { | |
| &:hover .slide__image-wrapper { | |
| transform: | |
| scale(1.025) | |
| translate( | |
| calc(var(--x) / var(--d) * 1px), | |
| calc(var(--y) / var(--d) * 1px) | |
| ); | |
| } | |
| } | |
| } | |
| .slide__image-wrapper { | |
| background-color: var(--color-accent); | |
| border-radius: 1%; | |
| height: 100%; | |
| left: 0%; | |
| overflow: hidden; | |
| position: absolute; | |
| top: 0%; | |
| transition: transform calc(var(--base-duration) / 4) var(--base-ease); | |
| width: 100%; | |
| } | |
| .slide__image { | |
| --d: 20; | |
| height: 110%; | |
| left: -5%; | |
| object-fit: cover; | |
| opacity: 0; | |
| pointer-events: none; | |
| position: absolute; | |
| top: -5%; | |
| transition: | |
| opacity var(--base-duration) var(--base-ease), | |
| transform var(--base-duration) var(--base-ease); | |
| user-select: none; | |
| width: 110%; | |
| @media (hover: hover) { | |
| .slide--current & { | |
| transform: | |
| translate( | |
| calc(var(--x) / var(--d) * 1px), | |
| calc(var(--y) / var(--d) * 1px) | |
| ); | |
| } | |
| } | |
| } | |
| .slide__headline { | |
| font-size: 8vmin; | |
| font-weight: 600; | |
| position: relative; | |
| } | |
| .slide__content { | |
| --d: 60; | |
| opacity: 0; | |
| padding: 4vmin; | |
| position: relative; | |
| transition: transform var(--base-duration) var(--base-ease); | |
| visibility: hidden; | |
| .slide--current & { | |
| animation: fade-in calc(var(--base-duration) / 2) var(--base-ease) forwards; | |
| visibility: visible; | |
| @media (hover: hover) { | |
| transform: | |
| translate( | |
| calc(var(--x) / var(--d) * -1px), | |
| calc(var(--y) / var(--d) * -1px) | |
| ); | |
| } | |
| } | |
| > * + * { | |
| margin-top: 2rem; | |
| } | |
| } | |
| // ========================= | |
| // Animations | |
| // ========================= | |
| @keyframes fade-in { | |
| from { opacity: 0 } | |
| to { opacity: 1 } | |
| } |