This is a simple transition: detail page slide in from the right.
ReactDOM.render(
<Router history={hashHistory}>
<Route path="/" component={Home}>
<IndexRoute component={ListPage} />
<Route path="/detail/:itemId" component={DetailPage} />
</Route>
</Router>, document.getElementById('app'));
export default class ListPage extends React.Component {
...
render() {
return (
<div className="transition-item list-page">
{this.state.items.map(item => (
<Link
key={item.id}
className="list-item"
to={`/detail/${item.id}`}
>
<Item {...item} />
</Link>
))}
</div>
);
}
import React from 'react';
import ReactDom from 'react-dom';
import PageTransition from 'react-router-page-transition';
export default (props) => (
<div>
<PageTransition>
{props.children}
</PageTransition>
</div>
);
Add class transition-item
to your root element, we will use this to animate the page
when route change.
export default class DetailPage extends React.Component {
render() {
return (
<div className="transition-item detail-page">
<Link to="/">Back</Link>
<h1>
Detail {this.props.params.itemId}
</h1>
</div>
);
}
}
Define animation using CSS
.transition-wrapper {
position: relative;
z-index: 1;
.transition-item {
position: fixed;
top: 0;
right: 0;
left: 0;
}
}
.detail-page {
overflow: auto;
box-sizing: border-box;
padding: 20px;
height: 100vh;
background-color: #03a9f4;
transition: transform 0.5s, opacity 0.5s;
&.transition-appear {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
&.transition-appear.transition-appear-active {
opacity: 1;
transform: translate3d(0, 0, 0);
}
&.transition-leave {
opacity: 1;
transform: translate3d(0, 0, 0);
}
&.transition-leave.transition-leave-active {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
}
.list-page {
overflow: auto;
box-sizing: border-box;
padding: 20px;
height: 100vh;
background-color: #fff;
transition: transform 0.5s, opacity 0.5s;
transform: translate3d(0, 0, 0);
&.transition-appear {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
&.transition-appear.transition-appear-active {
opacity: 1;
transform: translate3d(0, 0, 0);
}
&.transition-leave {
opacity: 1;
transform: translate3d(0, 0, 0);
}
&.transition-leave.transition-leave-active {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
}
From a cool idea of Material Motion that provide meaningful transition between pages.
In order to implement this, we need to pass custom data into the detail page, which requires a data flow management for the app. In this example, I'll use RxJS to pass data between ListPage component, Home component and ItemDetalPage component, but you can use another library to handle the data (like Flux or Redux).
See the onClick
method, we send the clicked item's data (color and position) to the action.
render() {
return (
<div className="transition-item list-page">
{this.state.items.map(item => (
<Link
key={item.id}
className="list-item"
onClick={e => action.onNext({
name: 'CLICKED_ITEM_DATA',
data: {
position: e.target.getBoundingClientRect(),
color: item.color,
},
})}
to={`/detail/${item.id}`}
>
<Item {...item} />
</Link>
))}
</div>
);
}
Home component receives the data and pass it to the PageTransition
component.
export default class Home extends React.Component {
constructor(...args) {
super(...args);
this.state = {
clickedItemData: null,
};
}
componentDidMount() {
// Receive data from the clicked item
this.obsClickedItemData = action
.filter(a => a.name === 'CLICKED_ITEM_DATA')
.map(a => a.data)
.subscribe(clickedItemData => this.setState({ clickedItemData }));
}
componentWillUnmount() {
this.obsClickedItemData.dispose();
}
render() {
return (
<div>
<PageTransition
data={{ clickedItemData: this.state.clickedItemData }}
>
{this.props.children}
</PageTransition>
</div>
);
}
}
This component will receive the callbacks with data from PageTransition
component, then we can use tihs to animate our page as we want.
export default class DetailPage extends React.Component {
constructor(...args) {
super(...args);
this.state = {
doTransform: false,
position: null,
color: null,
};
}
onTransitionWillStart(data) {
// Start position of the page
this.setState({
doTransform: true,
position: data.clickedItemData.position,
color: data.clickedItemData.color,
});
}
onTransitionDidEnd() {
// Transition is done, do your stuff here
}
transitionManuallyStart() {
// When this method exsits, `transition-appear-active` will not be added to the dom
// we will define our animation manually.
this.setState({
position: {
top: 0,
height: '100%',
left: 0,
right: 0,
},
doTransform: true,
});
}
transitionManuallyStop() {
// When this method exsits, `transition-appear-active` will not be removed
this.setState({
doTransform: false,
});
}
render() {
return (
<div
style={{
transform: this.state.doTransform ?
`translate3d(0, ${this.state.position.top}px, 0)` :
undefined,
height: this.state.doTransform ?
this.state.position.height : null,
left: this.state.doTransform ?
this.state.position.left : null,
right: this.state.doTransform ?
this.state.position.left : null,
backgroundColor: this.state.color,
}}
className="transition-item detail-page"
>
<Link to="/">
Item {this.props.params.itemId}
</Link>
<h1>
Detail page here
</h1>
<Link to="/">
Back
</Link>
</div>
);
}
.transition-wrapper {
position: relative;
z-index: 1;
.transition-item {
position: fixed;
top: 0;
left: 0;
right: 0;
}
}
.detail-page {
padding: 10px 10px;
background-color: #03a9f4;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
a {
color: white;
}
&.transition-appear {
transition: transform 1s cubic-bezier(0.7, 0, 0.25, 1),
left 1s cubic-bezier(0.7, 0, 0.25, 1),
right 1s cubic-bezier(0.7, 0, 0.25, 1),
height 1s cubic-bezier(0.7, 0, 0.25, 1);
}
&.transition-appear.transition-appear-active {
}
}
Similar to the material example, we use border-radius
to animate the circle.