#app
View Compiled
@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 }
}
View Compiled
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'));
View Compiled
This Pen doesn't use any external CSS resources.