<div id="root"></div>
@import url('https://fonts.googleapis.com/css?family=Mali:400,500');
/** Animation classes */
.fromRight {
animation: fromRight 0.5s ease-out;
}
@keyframes fromRight {
from {
transform: translate3d(100%, 0, 0);
}
}
.toRight {
animation: toRight 0.5s ease-out;
}
@keyframes toRight {
to {
transform: translate3d(100%, 0, 0);
}
}
.fromLeft {
animation: fromLeft 0.5s ease-out;
}
@keyframes fromLeft {
from {
transform: translate3d(-100%, 0, 0);
}
}
.toLeft {
animation: toLeft 0.5s ease-out;
}
@keyframes toLeft {
to {
transform: translate3d(-100%, 0, 0);
}
}
.fromTop {
animation: fromTop 0.5s ease-out;
}
@keyframes fromTop {
from {
transform: translate3d(0, -100%, 0);
}
}
.toTop {
animation: toTop 0.5s ease-out;
}
@keyframes toTop {
to {
transform: translate3d(0, -100%, 0);
}
}
.fromBottom {
animation: fromBottom 0.5s ease-out;
}
@keyframes fromBottom {
from {
transform: translate3d(0, 100%, 0);
}
}
.toBottom {
animation: toBottom 0.5s ease-out;
}
@keyframes toBottom {
to {
transform: translate3d(0, 100%, 0);
}
}
.fromRightFade {
animation: fromRightFade 0.5s ease-out;
}
@keyframes fromRightFade {
from {
opacity: 0.2;
transform: translate3d(100%, 0, 0);
}
}
.toRightFade {
animation: toRightFade 0.5s ease-out;
}
@keyframes toRightFade {
to {
opacity: 0.2;
transform: translate3d(100%, 0, 0);
}
}
.fromLeftFade {
animation: fromLeftFade 0.5s ease-out;
}
@keyframes fromLeftFade {
from {
opacity: 0.2;
transform: translate3d(-100%, 0, 0);
}
}
.toLeftFade {
animation: toLeftFade 0.5s ease-out;
}
@keyframes toLeftFade {
to {
opacity: 0.2;
transform: translate3d(-100%, 0, 0);
}
}
.fromTopFade {
animation: fromTopFade 0.5s ease-out;
}
@keyframes fromTopFade {
from {
opacity: 0.2;
transform: translate3d(0, -100%, 0);
}
}
.toTopFade {
animation: toTopFade 0.5s ease-out;
}
@keyframes toTopFade {
to {
opacity: 0.2;
transform: translate3d(0, -100%, 0);
}
}
.fromBottomFade {
animation: fromBottomFade 0.5s ease-out;
}
@keyframes fromBottomFade {
from {
opacity: 0.2;
transform: translate3d(0, 100%, 0);
}
}
.toBottomFade {
animation: toBottomFade 0.5s ease-out;
}
@keyframes toBottomFade {
to {
opacity: 0.2;
transform: translate3d(0, 100%, 0);
}
}
.toLeftEasing {
animation: toLeftFade 0.5s ease-in-out both;
}
.toRightEasing {
animation: toRightFade 0.5s ease-in-out both;
}
.toTopEasing {
animation: toTopFade 0.5s ease-in-out both;
}
.toBottomEasing {
animation: toBottomFade 0.5s ease-in-out both;
}
/* Scales */
.scaleDown {
animation: scaleDown 0.5s ease both;
}
@keyframes scaleDown {
from {
}
to {
opacity: 0;
transform: scale(0.8);
}
}
.scaleUp {
animation: scaleUp 0.5s ease both;
}
@keyframes scaleUp {
from {
opacity: 0;
transform: scale(0.8);
}
}
.cubeLeftOut {
transform-origin: 100% 50%;
animation: cubeLeftOut 0.5s ease-in both;
}
@keyframes cubeLeftOut {
0% {
}
50% {
animation-timing-function: ease-out;
transform: translateX(-50%) translateZ(-200px) rotateY(-45deg);
}
100% {
opacity: 0.3;
transform: translateX(-100%) rotateY(-90deg);
}
}
.cubeLeftIn {
transform-origin: 0% 50%;
animation: cubeLeftIn 0.5s ease-in both;
}
@keyframes cubeLeftIn {
0% {
opacity: 0.3;
transform: translateX(100%) rotateY(90deg);
}
50% {
animation-timing-function: ease-out;
transform: translateX(50%) translateZ(-200px) rotateY(45deg);
}
}
.cubeRightOut {
transform-origin: 0% 50%;
animation: cubeRightOut 0.5s ease-in both;
}
@keyframes cubeRightOut {
0% {
}
50% {
animation-timing-function: ease-out;
transform: translateX(50%) translateZ(-200px) rotateY(45deg);
}
100% {
opacity: 0.3;
transform: translateX(100%) rotateY(90deg);
}
}
.cubeRightIn {
transform-origin: 100% 50%;
animation: cubeRightIn 0.5s ease-in both;
}
@keyframes cubeRightIn {
0% {
opacity: 0.3;
transform: translateX(-100%) rotateY(-90deg);
}
50% {
animation-timing-function: ease-out;
transform: translateX(-50%) translateZ(-200px) rotateY(-45deg);
}
}
/** utilities */
.isActive {
visibility: visible !important;
}
.onTop {
z-index: 999;
}
/* resets and defaults */
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
height: 100%;
padding: 0;
margin: 0;
}
body {
text-align: center;
background: linear-gradient(45deg, #b066fe, #63e2ff);
color: #292929;
font-family: 'Mali', sans-serif;
}
.app {
display: grid;
grid-template: auto / 1fr;
padding: 20px;
margin: 0 auto;
width: 50%;
}
@media only screen and (max-width: 680px) {
.app {
width: 85%;
}
}
.view-container {
background: #f1f1f1;
border: 0;
padding: 16px;
grid-gap: 16px;
box-shadow: 0 3px 5px 1px rgba(55, 55, 55, 0.25);
}
button {
margin: 4px;
padding: 8px 12px;
border: none;
border-radius: 3px;
text-transform: uppercase;
background: linear-gradient(45deg, #2f80ed, #56ccf2);
font-family: inherit;
color: white;
cursor: pointer;
outline: none;
transition: all 0.2s ease-out;
// box-shadow: 0 1px 1px 1px rgba(55, 55, 55, 0.25);
&:hover {
transform: translateY(-1px);
// box-shadow: 0 2px 5px 2px rgba(55, 55, 55, 0.25);
}
}
#root {
height: 100%;
}
h3 {
margin: 0 0 16px;
}
.view-slide {
padding: 8px 16px;
}
View Compiled
// --------------------------------------------------------------------
// APP - Please read for usage:
// Change these with any keys from animationTypes array below
// or pass these pre selected ones into ViewSlides
const demoAnimationsOne = { nextAnimation: 'cubeToLeft', prevAnimation: 'toRightEasing' };
const demoAnimationsTwo = { nextAnimation: 'toLeftEasing', prevAnimation: 'cubeToRight' };
const demoAnimationsThree = { nextAnimation: 'scaleDownFromRight', prevAnimation: 'scaleDownFromTop' };
const demoAnimationsFour = { nextAnimation: 'toLeftEasing', prevAnimation: 'toRightEasing' };
// Pass either of these to control if the slide or controls render on top etc
const slidesOnTop = { templateArea: ['slide', 'controls'] };
const controlsOnTop = { templateArea: ['controls', 'slide']};
// Or create your own custom grid template area.
const customControls = { templateArea: ['', '']};
// You can also forego this, and nest the controls inside the slide to use in each slide.
// ViewContainer allows render={(args =>)} or child as function
const App = () => (
<div className="app">
<ViewContainer
// PASS IN GRID TEMPLATE AREAS HERE:
gridProperties={slidesOnTop}
slideTotal={3}
render={({ prevSlide, nextSlide, ...props }) => (
<React.Fragment>
{items.map((item, index) => (
<ViewSlide
key={item.title}
slideIndex={index}
// PASS IN ANIMATIONS HERE:
animationType={demoAnimationsFour}
slideClassname="view-slide"
{...props}
>
<React.Fragment>
<h3>{item.title}</h3>
<div>{item.content}</div>
</React.Fragment>
</ViewSlide>
))}
<ViewControls>
<button onClick={prevSlide}>Prev</button>
<button onClick={nextSlide}>Next</button>
</ViewControls>
</React.Fragment>
)}
/>
</div>
);
const items = [
{
title: 'Title One',
content: (
<div>
Horses can sleep both lying down and standing up. Domestic horses have a
lifespan of around 25 years. A 19th century horse named 'Old Billy' is
said to have lived 62 years.
</div>
)
},
{
title: 'Title Two',
content: (
<div>
Rhino skin maybe thick but it can be quite sensitive to sunburns and
insect bites which is why they like wallow so much – when the mud dries
it acts as protection from the sunburns and insects.
</div>
)
},
{
title: 'Title Three',
content: (
<div>
If you’re looking to hunt a unicorn, but don’t know where to begin, try
Lake Superior State University in Sault Ste. Marie, Michigan. Since
1971, the university has issued permits to unicorn questers.
</div>
)
}
];
// --------------------------------------------------------------------
// VIEW CONTAINER
const defaultProps = {
gridProperties: { columnSizes: '1fr', rowSizes: 'auto' }
};
const initialState = {
prevActiveIndex: 0,
activeIndex: 0,
isAnimating: false
};
class ViewContainer extends React.Component {
state = initialState;
static defaultProps = defaultProps;
// ref for container node
container;
shouldComponentUpdate(nextProps, nextState) {
return (
nextState.activeIndex !== this.state.activeIndex ||
nextState.isAnimating !== this.state.isAnimating ||
nextProps.slideTotal !== this.props.slideTotal
);
}
render() {
const { children, render } = this.props;
const { activeIndex, prevActiveIndex, isAnimating } = this.state;
// todo: move this out of render
const renderProps = {
activeIndex,
prevActiveIndex,
isAnimating,
handleAnimationEnd: this.handleAnimationEnd,
prevSlide: this.onPrevSlide,
nextSlide: this.onNextSlide
};
const defaultStyles = {
display: 'grid',
position: 'relative',
overflow: 'hidden',
perspective: '1200px'
};
const propStyles = this.assignGridValues();
const finalStyles = {
...defaultStyles,
...propStyles
};
return (
<div
style={finalStyles}
className="view-container"
ref={el => (this.container = el)}
>
{render
? render(renderProps)
: isFunction(children)
? children(renderProps)
: null}
</div>
);
}
assignGridValues = () => {
const { columnSizes, rowSizes, templateArea } = this.props.gridProperties;
const obj = {
gridTemplateColumns: columnSizes,
gridTemplateRows: rowSizes
};
// For grid-template-areas prop, syntax needs to be "'value' 'value'" etc to be interpreted correctly.
if (templateArea.length) {
let templateString = '';
templateArea.forEach(val => (templateString += `'${val}' `));
return {
...obj,
gridTemplateAreas: `${templateString}`
};
}
return obj;
};
onPrevSlide = () => {
if (this.state.isAnimating || this.props.slideTotal < 2) {
return;
}
this.setState(prevState => {
if (prevState.activeIndex === 0) {
return prevState;
} else {
return {
...prevState,
isAnimating: true,
prevActiveIndex: prevState.activeIndex,
activeIndex: prevState.activeIndex - 1
};
}
});
};
// handle next slide
onNextSlide = () => {
if (this.state.isAnimating || this.props.slideTotal < 2) {
return;
}
this.setState(prevState => {
if (prevState.activeIndex === this.props.slideTotal - 1) {
return prevState;
} else {
return {
...prevState,
isAnimating: true,
prevActiveIndex: prevState.activeIndex,
activeIndex: prevState.activeIndex + 1
};
}
});
};
handleAnimationEnd = () => {
this.setState({
isAnimating: false
});
};
}
// ---------------------------------------------------------------------------
// VIEW SLIDE
const defaultSlideProps = {
animationType: {
nextAnimation: 'fromRightFade',
prevAnimation: 'fromLeftFade',
},
gridAreaValue: 'slide'
};
class ViewSlide extends React.Component {
static defaultProps = defaultSlideProps;
slide;
render() {
const {
activeIndex,
slideIndex,
slideClassname,
gridAreaValue,
children
} = this.props;
const isActive = activeIndex === slideIndex;
const slideStyles = {
gridArea: gridAreaValue,
overflow: 'hidden',
visibility: 'hidden',
transformStyle: 'preserve-3d',
backfaceVisibility: 'hidden',
willChange: 'transform',
zIndex: isActive ? 1 : 0
};
// Assign prop, animation and direction classnames.
const slideClassNames = pipe(
this.assignPropClassnames,
this.assignAnimationClassnames
)(slideClassname).join(' ');
return (
<div
style={slideStyles}
className={slideClassNames.length ? slideClassNames : undefined}
onAnimationEnd={this.handleAnimationEnd}
ref={el => (this.slide = el)}
>
{children}
</div>
);
}
// Assign any classnames passed as props.
assignPropClassnames = (slideClassname) => {
let classes = [];
if (slideClassname) {
// handle array of classes
if (Array.isArray(slideClassname)) {
classes = [...slideClassname];
} else {
classes.push(slideClassname);
}
}
return classes;
};
// Assign relavant animation classes based on slide direction
// and which animation is applied for both prev and next actions
assignAnimationClassnames = (classes) => {
const { activeIndex, prevActiveIndex, slideIndex } = this.props;
const isExitingSlide = slideIndex === prevActiveIndex;
const isEnteringSlide = slideIndex === activeIndex;
const directionalClass = this.getDirectionalClassname(isEnteringSlide);
let animationClasses = [];
if (this.props.isAnimating) {
if (isExitingSlide) {
animationClasses = [...classes, ...directionalClass, 'isActive'];
} else if (isEnteringSlide) {
animationClasses = [...classes, ...directionalClass, 'isActive'];
}
} else if (isEnteringSlide) {
animationClasses = [...classes, 'isActive'];
} else {
return classes;
}
return animationClasses;
};
getDirectionalClassname = (isEnteringSlide) => {
const { activeIndex, prevActiveIndex } = this.props;
const isAnimatingForward = activeIndex > prevActiveIndex ? true : false;
const { enteringClass, exitingClass } = this.getAnimationType(
isAnimatingForward
);
if (isEnteringSlide) {
return enteringClass;
} else {
return exitingClass;
}
};
// Retrieve the correct enter and exiting class names from animation object
getAnimationType = (isAnimatingForward) => {
const { nextAnimation, prevAnimation } = this.props.animationType;
// Get Animation key based on if prev or next slide is called
const animationKey = isAnimatingForward ? nextAnimation : prevAnimation;
// Once we have the key -> get the containing object for that key and retrieve classnames
const animationObj = animations.find(arr => arr[animationKey]);
const { enteringClass, exitingClass } = animationObj[animationKey];
return {
enteringClass,
exitingClass
};
};
// Handle resetting of slides and any callback on animEnd
handleAnimationEnd = (e) => {
const { handleAnimationEnd } = this.props;
if (e && e.target !== this.slide) return;
this.resetSlides();
if (handleAnimationEnd) {
handleAnimationEnd();
}
};
// Need to find better way to handle reset?? (too implicit?)
resetSlides = () => {
const { prevActiveIndex, slideIndex } = this.props;
const wasPreviousActive = prevActiveIndex === slideIndex;
if (wasPreviousActive) {
if (this.slide.classList.contains('isActive')) {
this.slide.classList.remove('isActive');
}
}
};
}
// -------------------------------------------------------------------------------------
// VIEW CONTROLS
function ViewControls({ children, gridAreaValue }) {
const propStyles = {
position: 'relative',
gridArea: gridAreaValue
};
return <div style={propStyles}>{children}</div>;
}
// --------------------------------------------------------------------------------------
// HELPERS
const pipe = (...fns) => (x) => fns.reduce((v, fn) => fn(v), x);
const isFunction = (value) => typeof value === 'function';
// --------------------------------------------------------------------------------------
// ANIMATIONS
const animations = [
{
fromRight: {
enteringClass: ['fromRight'],
exitingClass: ['toLeft']
}
},
{
fromLeft: {
enteringClass: ['fromLeft'],
exitingClass: ['toRight']
}
},
{
fromBottom: {
enteringClass: ['fromBottom'],
exitingClass: ['toTop']
}
},
{
fromTop: {
enteringClass: ['fromTop'],
exitingClass: ['toBottom']
}
},
{
fromRightFade: {
enteringClass: ['fromRightFade'],
exitingClass: ['toLeftFade']
}
},
{
fromLeftFade: {
enteringClass: ['fromLeftFade'],
exitingClass: ['toRightFade']
}
},
{
fromBottomFade: {
enteringClass: ['fromBottomFade'],
exitingClass: ['toTopFade']
}
},
{
fromTopFade: {
enteringClass: ['fromTopFade'],
exitingClass: ['toBottomFade']
}
},
{
toLeftEasing: {
enteringClass: ['fromRight'],
exitingClass: ['toLeftEasing', 'onTop']
}
},
{
toRightEasing: {
enteringClass: ['fromLeft'],
exitingClass: ['toRightEasing', 'onTop']
}
},
{
toTopEasing: {
enteringClass: ['fromBottom'],
exitingClass: ['toTopEasing', 'onTop']
}
},
{
toBottomEasing: {
enteringClass: ['fromTop'],
exitingClass: ['toBottomEasing', 'onTop']
}
},
{
scaleDownFromRight: {
enteringClass: ['fromRight', 'onTop'],
exitingClass: ['scaleDown']
}
},
{
scaleDownFromLeft: {
enteringClass: ['fromLeft', 'onTop'],
exitingClass: ['scaleDown']
}
},
{
scaleDownFromTop: {
exitingClass: ['scaleDown'],
enteringClass: ['fromTop', 'onTop']
}
},
{
scaleDownFromBottom: {
exitingClass: ['scaleDown'],
enteringClass: ['fromBottom', 'onTop']
}
},
{
cubeToLeft: {
enteringClass: ['cubeLeftIn'],
exitingClass: ['cubeLeftOut', 'onTop']
}
},
{
cubeToRight: {
enteringClass: ['cubeRightIn'],
exitingClass: ['cubeRightOut', 'onTop']
}
},
{
cubeToTop: {
enteringClass: ['cubeTopIn'],
exitingClass: ['cubeTopOut', 'onTop']
}
},
{
cubeToBottom: {
enteringClass: ['cubeBottomIn'],
exitingClass: ['cubeBottomOut', 'onTop']
}
}
];
ReactDOM.render(
<App />,
document.querySelector('#root')
);
View Compiled
This Pen doesn't use any external CSS resources.